From 9e37fa2ca7e0fbf1c615644312a134f0baafe047 Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Mon, 19 Oct 2026 18:40:56 +0200 Subject: [PATCH 01/12] Terminal strip editor: cable cells worked out from the project cables The Cable and Color / wire number columns now show what the cables of the project say (designation and core colour or number), worked out from the conductors wired to the terminal, instead of only the values typed in by hand (patch by Laurent); Values typed in by hand are still shown, and the cells still editable, when no cable of the project is wired to the terminal; A value worked out from a cable is never written back into the element, so a cable renamed or removed cannot leave a stale value behind. --- sources/TerminalStrip/realterminal.cpp | 82 ++++++++++++++++++- sources/TerminalStrip/realterminal.h | 1 + sources/TerminalStrip/ui/modelTerminalData.h | 2 + .../TerminalStrip/ui/terminalstripeditor.cpp | 9 +- .../TerminalStrip/ui/terminalstripmodel.cpp | 6 +- 5 files changed, 95 insertions(+), 5 deletions(-) diff --git a/sources/TerminalStrip/realterminal.cpp b/sources/TerminalStrip/realterminal.cpp index 3ec7ad462..95647961f 100644 --- a/sources/TerminalStrip/realterminal.cpp +++ b/sources/TerminalStrip/realterminal.cpp @@ -23,6 +23,50 @@ #include "terminalcatalog.h" #include "../qetgraphicsitem/conductor.h" #include "../qetinformation.h" +#include "../qetgraphicsitem/terminal.h" +#include "../diagram.h" +#include "../cable/cable.h" +#include "../cable/cablemanager.h" + +namespace +{ + /** + * The cable cores wired to one terminal element, worked out from + * the cables of the project (PR #1438). A core knows its + * conductor, and CableManager::refreshLabels() writes the cable + * and the core into the properties of that conductor, so reading + * the conductors of the element is enough. + * Each pair is (cable designation, core colour or number). + */ + QList> wiredCores(Element *element) + { + QList> cores; + if (!element || !element->diagram()) { + return cores; + } + const auto project = element->diagram()->project(); + for (const auto terminal : element->terminals()) + { + for (const auto conductor : terminal->conductors()) + { + const auto properties = conductor->ownProperties(); + const auto cable = CableManager::cableByUuid(project, properties.m_cable_uuid); + if (!cable || cable->designation().isEmpty()) { + continue; + } + auto wire = cable->colorOfCore(properties.m_cable_slot); + if (wire.isEmpty() && properties.m_cable_slot >= 0) { + wire = QString::number(properties.m_cable_slot + 1); + } + const QPair core(cable->designation(), wire); + if (!cores.contains(core)) { + cores.append(core); + } + } + } + return cores; + } +} /** * @brief RealTerminal @@ -291,18 +335,52 @@ QString RealTerminal::Xref() const * @return The name of the cable (hose) connected to this terminal. * The value is stored in the informations of the element. */ -QString RealTerminal::cable() const { +QString RealTerminal::cable() const +{ + const auto cores = wiredCores(m_element.data()); + if (!cores.isEmpty()) + { + QStringList cables; + for (const auto &core : cores) { + if (!cables.contains(core.first)) { + cables.append(core.first); + } + } + return cables.join(QStringLiteral(", ")); + } + if (hasData()) { return terminalData().m_informations.value(cableInfoKey()).toString(); } return QString(); } +/** + * @brief RealTerminal::cableIsAutomatic + * @return true when the cable and the core of this terminal come from + * a cable of the project wired to it, false when they are the values + * typed in by the user in the terminal strip editor. + */ +bool RealTerminal::cableIsAutomatic() const { + return !wiredCores(m_element.data()).isEmpty(); +} + /** * @brief RealTerminal::cableWire * @return The color / number of the wire of the cable, connected to this terminal. */ -QString RealTerminal::cableWire() const { +QString RealTerminal::cableWire() const +{ + const auto cores = wiredCores(m_element.data()); + if (!cores.isEmpty()) + { + QStringList wires; + for (const auto &core : cores) { + wires.append(core.second); + } + return wires.join(QStringLiteral(", ")); + } + if (hasData()) { return terminalData().m_informations.value(cableWireInfoKey()).toString(); } diff --git a/sources/TerminalStrip/realterminal.h b/sources/TerminalStrip/realterminal.h index bf8d39bbd..a5c89fd3d 100644 --- a/sources/TerminalStrip/realterminal.h +++ b/sources/TerminalStrip/realterminal.h @@ -73,6 +73,7 @@ class RealTerminal QString Xref() const; QString cable() const; QString cableWire() const; + bool cableIsAutomatic() const; bool isShield() const; QColor color() const; QColor stripeColor() const; diff --git a/sources/TerminalStrip/ui/modelTerminalData.h b/sources/TerminalStrip/ui/modelTerminalData.h index 2921b33af..e947d4dc5 100644 --- a/sources/TerminalStrip/ui/modelTerminalData.h +++ b/sources/TerminalStrip/ui/modelTerminalData.h @@ -35,6 +35,7 @@ struct modelRealTerminalData mrtd.Xref_ = real_t->Xref(); mrtd.cable_ = real_t->cable(); mrtd.cable_wire = real_t->cableWire(); + mrtd.cable_auto_ = real_t->cableIsAutomatic(); mrtd.conductor_ = real_t->conductor(); mrtd.led_ = real_t->isLed(); mrtd.shield_ = real_t->isShield(); @@ -60,6 +61,7 @@ struct modelRealTerminalData QString Xref_; QString cable_; QString cable_wire; + bool cable_auto_ = false; //True when cable_ and cable_wire come from the cables of the project QString conductor_; bool led_ = false; bool shield_ = false; diff --git a/sources/TerminalStrip/ui/terminalstripeditor.cpp b/sources/TerminalStrip/ui/terminalstripeditor.cpp index 3d748fc69..56016fb0b 100644 --- a/sources/TerminalStrip/ui/terminalstripeditor.cpp +++ b/sources/TerminalStrip/ui/terminalstripeditor.cpp @@ -245,8 +245,13 @@ void TerminalStripEditor::apply() current_data.m_informations.addValue(key, value); } }; - set_info(RealTerminal::cableInfoKey(), data_.cable_); - set_info(RealTerminal::cableWireInfoKey(), data_.cable_wire); + //A cable worked out from the cables of the project is + //never written in the element: it would stay there, + //out of date, once the cable is renamed or removed + if (!data_.cable_auto_) { + set_info(RealTerminal::cableInfoKey(), data_.cable_); + set_info(RealTerminal::cableWireInfoKey(), data_.cable_wire); + } set_info(RealTerminal::shieldInfoKey(), data_.shield_ ? QStringLiteral("true") : QString()); //The color and the section are stored with the dot as decimal separator, whatever the locale set_info(RealTerminal::colorInfoKey(), TerminalColor::toSpec(data_.color_, data_.stripe_color_)); diff --git a/sources/TerminalStrip/ui/terminalstripmodel.cpp b/sources/TerminalStrip/ui/terminalstripmodel.cpp index 4dfdb51e4..6ecadcd61 100644 --- a/sources/TerminalStrip/ui/terminalstripmodel.cpp +++ b/sources/TerminalStrip/ui/terminalstripmodel.cpp @@ -497,8 +497,12 @@ Qt::ItemFlags TerminalStripModel::flags(const QModelIndex &index) const Qt::ItemFlags flags = Qt::ItemIsEnabled | Qt::ItemIsSelectable; auto c = index.column(); + //The cable cells are only edited by hand when no cable + //of the project is wired to the terminal + if ((c == CABLE_CELL || c == CABLE_WIRE_CELL) + && !dataAtRow(index.row()).cable_auto_) + flags = flags | Qt::ItemIsEditable; if (c == LABEL_CELL || c == TYPE_CELL || c == FUNCTION_CELL - || c == CABLE_CELL || c == CABLE_WIRE_CELL || c == COLOR_CELL || c == SECTION_CELL || c == WIDTH_CELL) flags = flags | Qt::ItemIsEditable; if (c == LED_CELL || c == SHIELD_CELL || c == SEPARATOR_CELL) { From 94a929c8c00ff68dbef84f36848352bd80357d44 Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Mon, 19 Oct 2026 18:49:36 +0200 Subject: [PATCH 02/12] Add tst_terminalcableauto: the terminal strip cable cells from a cable Three cases against fixtures/every_feature.qet (strip X1, wired terminals, no cable): a wired terminal shows cable and core, the cells go read only and nothing is written into the element; a core without a colour is shown as its number; a value typed in by hand stays when no cable is wired and comes back unchanged once the cable is gone. --- tests/qttest/CMakeLists.txt | 5 + tests/qttest/tst_terminalcableauto.cpp | 201 +++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 tests/qttest/tst_terminalcableauto.cpp diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 644cf4725..ca8b577ec 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -290,6 +290,11 @@ qet_add_app_test(tst_assistantinfopaths tst_assistantinfopaths.cpp) # %T in a report or cross reference formula is the target folio's title. qet_add_app_test(tst_reportfoliotitle tst_reportfoliotitle.cpp) +# The cable cells of a terminal strip are worked out from the cables of +# the project: wired shows the cable and its core read only, unwired keeps +# what was typed in, and nothing stale is written into the element. +qet_add_app_test(tst_terminalcableauto tst_terminalcableauto.cpp) + # Escape during a picture's resize, rotate or pivot drag puts it back, keeps # it selected and records no undo step (#1414); the key goes to the view. qet_add_app_test(tst_imagehandleescape tst_imagehandleescape.cpp) diff --git a/tests/qttest/tst_terminalcableauto.cpp b/tests/qttest/tst_terminalcableauto.cpp new file mode 100644 index 000000000..c82a4c8cd --- /dev/null +++ b/tests/qttest/tst_terminalcableauto.cpp @@ -0,0 +1,201 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + 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 . +*/ +#include "qetapptest.h" +#include "TerminalStrip/realterminal.h" +#include "TerminalStrip/terminalstrip.h" +#include "TerminalStrip/ui/terminalstripmodel.h" +#include "cable/cable.h" +#include "qetgraphicsitem/conductor.h" +#include "qetgraphicsitem/element.h" +#include "qetgraphicsitem/terminal.h" +#include "qetproject.h" + +/** + The Cable and Color / wire number columns of the terminal strip editor + are worked out from the cables of the project: a terminal with a cable + wired to it shows that cable and its core, the cell is read only, and + nothing is written into the element -- so a cable renamed or removed + cannot leave a stale value behind. A terminal no cable is wired to + keeps the value typed in by hand, cell and all. + + fixtures/every_feature.qet has the strip X1 whose two terminals are + wired to conductors on the folio, and no cable at all. +*/ +class tst_terminalcableauto : public QObject +{ + Q_OBJECT + + // The wired terminal of strip X1: its element carries a conductor. + static QSharedPointer wiredTerminal(QETProject &project) + { + const QVector strips = project.terminalStrip(); + if (strips.isEmpty()) return {}; + for (const QSharedPointer &real_t + : strips.first()->realTerminals()) + { + Element *element = real_t ? real_t->element() : nullptr; + if (!element) continue; + for (Terminal *terminal : element->terminals()) + if (terminal && !terminal->conductors().isEmpty()) + return real_t; + } + return {}; + } + + static Conductor *conductorOf(const QSharedPointer &real_t) + { + Element *element = real_t->element(); + if (!element) return nullptr; + for (Terminal *terminal : element->terminals()) { + if (!terminal) continue; + const QList conductors = terminal->conductors(); + if (!conductors.isEmpty()) return conductors.first(); + } + return nullptr; + } + + // The cable the create dialog hands over: owned by the project, four + // cores, core 1 (blue) wired to the terminal's conductor. + static Cable *wireCable(QETProject &project, + const QSharedPointer &real_t, + int core = 1) + { + Cable *cable = project.newCable(); + cable->setDesignation(QStringLiteral("15W1")); + cable->setCoreCount(4); + cable->setCoreColors({QStringLiteral("bn"), QStringLiteral("bl"), + QStringLiteral("gn/ye"), QStringLiteral("gy")}); + CableCore assignment; + assignment.core = core; + assignment.conductor = conductorOf(real_t)->uuid(); + cable->setCore(assignment); + return cable; + } + +private slots: + void wiredCableIsWorkedOutAndReadsOnly() + { + const QString fixture = QFINDTESTDATA("fixtures/every_feature.qet"); + QVERIFY2(!fixture.isEmpty(), "fixture project not found"); + QETProject project(fixture); + QCOMPARE(project.state(), QETProject::Ok); + + const auto real_t = wiredTerminal(project); + QVERIFY2(!real_t.isNull(), "the fixture has a wired terminal"); + Element *element = real_t->element(); + + //Without a cable, the cells show nothing and may be typed in + QVERIFY(!real_t->cableIsAutomatic()); + TerminalStripModel model_before(project.terminalStrip().first()); + QVERIFY(model_before.rowCount() > 0); + QVERIFY(model_before.flags(model_before.index(0, TerminalStripModel::Cable)) + & Qt::ItemIsEditable); + + wireCable(project, real_t); + + //The terminal itself reads the cable and its core + QCOMPARE(real_t->cable(), QStringLiteral("15W1")); + QVERIFY(real_t->cableIsAutomatic()); + QCOMPARE(real_t->cableWire(), QStringLiteral("bl")); + + //The automatic value is never written into the element: nothing + //stays behind once the cable is renamed or removed + QVERIFY(element); + QVERIFY(element->elementData().m_informations + .value(RealTerminal::cableInfoKey()).toString().isEmpty()); + + //The model shows both cells and keeps them out of the edits + TerminalStripModel model(project.terminalStrip().first()); + QCOMPARE(model.rowCount(), model_before.rowCount()); + const QModelIndex cable_cell = model.index(0, TerminalStripModel::Cable); + const QModelIndex wire_cell = model.index(0, TerminalStripModel::CableWire); + QCOMPARE(model.data(cable_cell).toString(), QStringLiteral("15W1")); + QCOMPARE(model.data(wire_cell).toString(), QStringLiteral("bl")); + QVERIFY(!(model.flags(cable_cell) & Qt::ItemIsEditable)); + QVERIFY(!(model.flags(wire_cell) & Qt::ItemIsEditable)); + //The other cells go on being editable + QVERIFY(model.flags(model.index(0, TerminalStripModel::Label)) + & Qt::ItemIsEditable); + } + + void coreWithoutColorIsShownAsItsNumber() + { + const QString fixture = QFINDTESTDATA("fixtures/every_feature.qet"); + QETProject project(fixture); + QCOMPARE(project.state(), QETProject::Ok); + + const auto real_t = wiredTerminal(project); + QVERIFY(!real_t.isNull()); + + Cable *cable = project.newCable(); + cable->setDesignation(QStringLiteral("15W1")); + cable->setCoreCount(4); + //A type file which gives no colours: the number of the core is shown + CableCore assignment; + assignment.core = 1; + assignment.conductor = conductorOf(real_t)->uuid(); + cable->setCore(assignment); + + QCOMPARE(real_t->cable(), QStringLiteral("15W1")); + QCOMPARE(real_t->cableWire(), QStringLiteral("2")); + QVERIFY(real_t->cableIsAutomatic()); + } + + void typedValueStaysWhenNoCableIsThere() + { + const QString fixture = QFINDTESTDATA("fixtures/every_feature.qet"); + QETProject project(fixture); + QCOMPARE(project.state(), QETProject::Ok); + + const auto real_t = wiredTerminal(project); + QVERIFY(!real_t.isNull()); + Element *element = real_t->element(); + QVERIFY(element); + + //A cable designation typed in by hand in the editor + ElementData typed = element->elementData(); + typed.m_informations.addValue(RealTerminal::cableInfoKey(), + QStringLiteral("K9")); + element->setElementData(typed); + QCOMPARE(real_t->cable(), QStringLiteral("K9")); + QVERIFY(!real_t->cableIsAutomatic()); + + //Wired to a cable: the cable is shown, the typed value is set aside + Cable *cable = wireCable(project, real_t); + QCOMPARE(real_t->cable(), QStringLiteral("15W1")); + QVERIFY(real_t->cableIsAutomatic()); + QCOMPARE(real_t->cableWire(), QStringLiteral("bl")); + + //Cable removed: the typed value is back, no stale 15W1 anywhere + cable->removeCore(1); + QCOMPARE(real_t->cable(), QStringLiteral("K9")); + QVERIFY(!real_t->cableIsAutomatic()); + QVERIFY(element->elementData().m_informations + .value(RealTerminal::cableInfoKey()) + .toString() == QStringLiteral("K9")); + + TerminalStripModel model(project.terminalStrip().first()); + QVERIFY(model.flags(model.index(0, TerminalStripModel::Cable)) + & Qt::ItemIsEditable); + } +}; + +QET_APP_TEST_MAIN(tst_terminalcableauto) + +#include "tst_terminalcableauto.moc" From bab2707fff07a6e09840504b0a66e1bdb5f82b13 Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Mon, 19 Oct 2026 19:09:50 +0200 Subject: [PATCH 03/12] Terminal strip editor: the cable cells follow the cables The window stays open while the drawing is wired, so the table takes up what the cables say on its own: each cable of the project is watched, its change reloads the table after CableManager has worked the labels out again, and a cable taken away empties the cells right away. A reload never costs the user his own work: cell edits not applied yet are kept, and a cell being typed in is left alone. A cell worked out from a cable refuses to be written through setData(), so the apply button, the paste and the deletion of texts leave it alone too. --- .../TerminalStrip/ui/terminalstripeditor.cpp | 84 +++++++++++++++++ .../TerminalStrip/ui/terminalstripeditor.h | 7 ++ .../TerminalStrip/ui/terminalstripmodel.cpp | 8 ++ tests/qttest/tst_terminalcableauto.cpp | 90 ++++++++++++++++--- 4 files changed, 179 insertions(+), 10 deletions(-) diff --git a/sources/TerminalStrip/ui/terminalstripeditor.cpp b/sources/TerminalStrip/ui/terminalstripeditor.cpp index 56016fb0b..c5ea3f760 100644 --- a/sources/TerminalStrip/ui/terminalstripeditor.cpp +++ b/sources/TerminalStrip/ui/terminalstripeditor.cpp @@ -35,6 +35,7 @@ #include "../terminalcatalog.h" #include "../realterminal.h" #include "../../qetinformation.h" +#include "../../cable/cable.h" #include #include @@ -95,6 +96,10 @@ TerminalStripEditor::TerminalStripEditor(QETProject *project, QWidget *parent) : } } }); + + //The cable cells are worked out from the cables of the project, + //so the table follows them while the user wires the drawing + watchCableChanges(); } /** @@ -107,9 +112,88 @@ TerminalStripEditor::~TerminalStripEditor() { void TerminalStripEditor::setProject(QETProject *project) { m_project = project; + watchCableChanges(); setCurrentStrip(nullptr); } +/** + * @brief TerminalStripEditor::watchCableChanges + * Follow the cables of the project: the cable cells are worked out from + * them, so a cable renamed, given cores or taken away changes what the + * cells say, and the window stays open while the user wires the drawing. + * The project the editor followed before is let go first, since the + * window may be shown on another project. + */ +void TerminalStripEditor::watchCableChanges() +{ + if (m_watched_project) { + //Only the connections made here go to the project + m_watched_project->disconnect(nullptr, this); + for (Cable *cable : m_watched_project->cables()) { + cable->disconnect(nullptr, this); + } + } + m_watched_project = m_project; + if (!m_project) { + return; + } + + connect(m_project, &QETProject::cableAdded, this, [this](Cable *cable) { + watchCable(cable); + }); + connect(m_project, &QETProject::cableRemoved, this, [this](Cable *cable) { + //The project stopped feeding its labels already, so the + //cells show what is left: the typed in value, or nothing + if (cable) { + cable->disconnect(nullptr, this); + } + reloadOnCableChange(); + }); + for (Cable *cable : m_project->cables()) { + watchCable(cable); + } +} + +/** + * @brief TerminalStripEditor::watchCable + * Reload the table when @a cable changes. The project runs its own + * connection first (made when the cable entered it), so the labels of + * the conductors are worked out again before this reload reads them. + * @param cable + */ +void TerminalStripEditor::watchCable(Cable *cable) +{ + if (!cable) { + return; + } + cable->disconnect(nullptr, this); + connect(cable, &Cable::changed, this, &TerminalStripEditor::reloadOnCableChange); +} + +/** + * @brief TerminalStripEditor::reloadOnCableChange + * Take up a change of the cables: reload so the cable cells say what + * they now must, never at the price of the user's own work -- cell + * edits not applied yet, or a cell being typed in, are kept and catch + * up with the next reload of any kind. + */ +void TerminalStripEditor::reloadOnCableChange() +{ + if (!m_current_strip || !m_model) { + return; + } + if (!m_model->modifiedmodelRealTerminalData().isEmpty()) { + return; + } + //A cell being typed in keeps its editor: the reload would close + //it under the fingers of the user (the edit itself is picked up + //by the next reload once it has been made) + if (ui->m_table_widget->isAncestorOf(QApplication::focusWidget())) { + return; + } + reload(); +} + /** * @brief TerminalStripEditor::setCurrentStrip * Set the current terminal strip edited to \p strip_ diff --git a/sources/TerminalStrip/ui/terminalstripeditor.h b/sources/TerminalStrip/ui/terminalstripeditor.h index 4a38d0e34..8ba7a878e 100644 --- a/sources/TerminalStrip/ui/terminalstripeditor.h +++ b/sources/TerminalStrip/ui/terminalstripeditor.h @@ -27,6 +27,7 @@ namespace Ui { } class QETProject; +class Cable; class TerminalStrip; /** @@ -58,6 +59,9 @@ class TerminalStripEditor : public QWidget void spanMultiLevelTerminals(); void selectionChanged(); QSize setUpBridgeCellWidth(); + void watchCableChanges(); + void watchCable(Cable *cable); + void reloadOnCableChange(); TerminalStripModel::Column isSingleColumnSelected() const; QPair> singleColumnData() const; @@ -79,6 +83,9 @@ class TerminalStripEditor : public QWidget private: Ui::TerminalStripEditor *ui; QPointer m_project; + //The project whose cables this editor follows, which is not + //always m_project yet when watchCableChanges() runs again + QPointer m_watched_project; QPointer m_current_strip; TerminalStripModel *m_model {nullptr}; }; diff --git a/sources/TerminalStrip/ui/terminalstripmodel.cpp b/sources/TerminalStrip/ui/terminalstripmodel.cpp index 6ecadcd61..6c4274bae 100644 --- a/sources/TerminalStrip/ui/terminalstripmodel.cpp +++ b/sources/TerminalStrip/ui/terminalstripmodel.cpp @@ -302,6 +302,14 @@ bool TerminalStripModel::setData(const QModelIndex &index, const QVariant &value int modified_cell = -1; auto column_ = index.column(); + //A cell worked out from the cables of the project is read only: + //the element never holds that value, so writing one in would only + //show something the next reload takes away again + if ((column_ == CABLE_CELL || column_ == CABLE_WIRE_CELL) + && mrtd.cable_auto_) { + return false; + } + if (column_ == LEVEL_CELL && role == Qt::EditRole) { diff --git a/tests/qttest/tst_terminalcableauto.cpp b/tests/qttest/tst_terminalcableauto.cpp index c82a4c8cd..802068b28 100644 --- a/tests/qttest/tst_terminalcableauto.cpp +++ b/tests/qttest/tst_terminalcableauto.cpp @@ -19,6 +19,7 @@ #include "qetapptest.h" #include "TerminalStrip/realterminal.h" #include "TerminalStrip/terminalstrip.h" +#include "TerminalStrip/ui/terminalstripeditor.h" #include "TerminalStrip/ui/terminalstripmodel.h" #include "cable/cable.h" #include "qetgraphicsitem/conductor.h" @@ -26,6 +27,8 @@ #include "qetgraphicsitem/terminal.h" #include "qetproject.h" +#include + /** The Cable and Color / wire number columns of the terminal strip editor are worked out from the cables of the project: a terminal with a cable @@ -58,32 +61,44 @@ class tst_terminalcableauto : public QObject return {}; } - static Conductor *conductorOf(const QSharedPointer &real_t) + static QList conductorsOf(const QSharedPointer &real_t) { - Element *element = real_t->element(); - if (!element) return nullptr; + QList conductors; + Element *element = real_t ? real_t->element() : nullptr; + if (!element) return conductors; for (Terminal *terminal : element->terminals()) { if (!terminal) continue; - const QList conductors = terminal->conductors(); - if (!conductors.isEmpty()) return conductors.first(); + for (Conductor *conductor : terminal->conductors()) { + if (conductor && !conductors.contains(conductor)) + conductors.append(conductor); + } } - return nullptr; + return conductors; + } + + static Conductor *conductorOf(const QSharedPointer &real_t) + { + const QList conductors = conductorsOf(real_t); + return conductors.isEmpty() ? nullptr : conductors.first(); } // The cable the create dialog hands over: owned by the project, four - // cores, core 1 (blue) wired to the terminal's conductor. + // cores, one core wired to the terminal's conductor (or to @p on). static Cable *wireCable(QETProject &project, const QSharedPointer &real_t, - int core = 1) + int core = 1, + const QString &designation = QStringLiteral("15W1"), + Conductor *on = nullptr) { + if (!on) on = conductorOf(real_t); Cable *cable = project.newCable(); - cable->setDesignation(QStringLiteral("15W1")); + cable->setDesignation(designation); cable->setCoreCount(4); cable->setCoreColors({QStringLiteral("bn"), QStringLiteral("bl"), QStringLiteral("gn/ye"), QStringLiteral("gy")}); CableCore assignment; assignment.core = core; - assignment.conductor = conductorOf(real_t)->uuid(); + assignment.conductor = on->uuid(); cable->setCore(assignment); return cable; } @@ -194,6 +209,61 @@ private slots: QVERIFY(model.flags(model.index(0, TerminalStripModel::Cable)) & Qt::ItemIsEditable); } + + // The window stays open while the user wires the drawing: the cells + // follow a cable wired, renamed or taken away without any reload by + // hand. + void openEditorFollowsCableChanges() + { + const QString fixture = QFINDTESTDATA("fixtures/every_feature.qet"); + QETProject project(fixture); + QCOMPARE(project.state(), QETProject::Ok); + + const auto real_t = wiredTerminal(project); + QVERIFY(!real_t.isNull()); + + TerminalStripEditor editor(&project); + editor.setCurrentStrip(project.terminalStrip().first()); + const QAbstractItemView *table = editor.findChild( + QStringLiteral("m_table_widget")); + QVERIFY2(table && table->model(), "the editor has its table"); + const QAbstractItemModel *model = table->model(); + const QModelIndex cable_cell = model->index(0, TerminalStripModel::Cable); + QVERIFY(model->data(cable_cell).toString().isEmpty()); + + //A cable wired to the terminal fills the cell in + Cable *cable = wireCable(project, real_t); + QCOMPARE(model->data(cable_cell).toString(), QStringLiteral("15W1")); + + //Renaming it follows at once, the drawing is not stale behind + cable->setDesignation(QStringLiteral("16W1")); + QCOMPARE(model->data(cable_cell).toString(), QStringLiteral("16W1")); + + //Taking it away empties the cell: nothing stale stays behind + QVERIFY(project.removeCable(cable)); + QVERIFY(model->data(cable_cell).toString().isEmpty()); + } + + // A terminal wired to two cables shows them both, comma separated. + void twoCablesOnOneTerminalAreBothShown() + { + const QString fixture = QFINDTESTDATA("fixtures/every_feature.qet"); + QETProject project(fixture); + QCOMPARE(project.state(), QETProject::Ok); + + const auto real_t = wiredTerminal(project); + QVERIFY(!real_t.isNull()); + const QList conductors = conductorsOf(real_t); + QVERIFY2(conductors.size() >= 2, + "the fixture terminal carries two wires"); + + wireCable(project, real_t, 1, QStringLiteral("15W1"), conductors.at(0)); + wireCable(project, real_t, 1, QStringLiteral("16W1"), conductors.at(1)); + + QCOMPARE(real_t->cable(), QStringLiteral("15W1, 16W1")); + QCOMPARE(real_t->cableWire(), QStringLiteral("bl, bl")); + QVERIFY(real_t->cableIsAutomatic()); + } }; QET_APP_TEST_MAIN(tst_terminalcableauto) From 123c56bb130340b6daf2df9fb6e88f99509e7efb Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Mon, 19 Oct 2026 20:48:06 +0200 Subject: [PATCH 04/12] Sort terminal strips into the plan's own order The strip sorted by label: X1:1, X1:2 ... with X1:N and X1:PE at the end whatever the plan looks like, so a strip drawn across pages came out in an order no cable follows. Sort it the way the plan is drawn instead -- folio by folio, then left to right -- in both places that order a strip: the auto import's own sort of the terminals it groups, and SortTerminalStripCommand, which the import pushes for a strip it just creates and which the editor's sort button and qet.sortTerminalStrip() push as well. The two share one comparator (SortTerminalStripCommand::planOrderLessThan), so they cannot fight each other. A spare stands on no folio, has no place in the plan order and goes last, by label; the label breaks the remaining ties, so two terminals at the very same spot keep a stable order. --- .../UndoCommand/sortterminalstripcommand.cpp | 70 ++++- .../UndoCommand/sortterminalstripcommand.h | 21 ++ .../TerminalStrip/terminalstripautoimport.cpp | 21 +- sources/scripting/qetscriptapi.cpp | 5 +- tests/qttest/CMakeLists.txt | 6 + tests/qttest/tst_terminalstriporder.cpp | 272 ++++++++++++++++++ 6 files changed, 381 insertions(+), 14 deletions(-) create mode 100644 tests/qttest/tst_terminalstriporder.cpp diff --git a/sources/TerminalStrip/UndoCommand/sortterminalstripcommand.cpp b/sources/TerminalStrip/UndoCommand/sortterminalstripcommand.cpp index 898ec8973..ce54bfb94 100644 --- a/sources/TerminalStrip/UndoCommand/sortterminalstripcommand.cpp +++ b/sources/TerminalStrip/UndoCommand/sortterminalstripcommand.cpp @@ -19,6 +19,9 @@ #include "../terminalstrip.h" #include "../physicalterminal.h" #include "../realterminal.h" +#include "../../diagram.h" +#include "../../positionorder.h" +#include "../../qetgraphicsitem/element.h" #include "../../utils/qetutils.h" SortTerminalStripCommand::SortTerminalStripCommand(TerminalStrip *strip, QUndoCommand *parent) : @@ -47,16 +50,73 @@ void SortTerminalStripCommand::redo() void SortTerminalStripCommand::sort() { - auto label_of = [](const QSharedPointer &t) -> QString + //The plan position of a physical terminal is the position of the + //level that sits on a folio; a terminal whose levels are all + //spares has no position and is ordered by label only. + auto position_of = [](const QSharedPointer &t_) -> QSharedPointer { - return t->realTerminalCount() ? t->realTerminals().constLast()->label() - : QString(); + if (!t_) { + return {}; + } + for (const auto &real_ : t_->realTerminals()) { + if (real_ && real_->element()) { + return real_; + } + } + return t_->realTerminalCount() ? t_->realTerminals().constFirst() + : QSharedPointer(); }; std::stable_sort(m_new_order.begin(), m_new_order.end(), - [&label_of](const QSharedPointer &arg1, + [&position_of](const QSharedPointer &arg1, const QSharedPointer &arg2) { - return QETUtils::naturalLessThan(label_of(arg1), label_of(arg2)); + return planOrderLessThan(position_of(arg1), position_of(arg2)); }); } + +/** + @brief SortTerminalStripCommand::planOrderLessThan + Order two real terminals the way the plan is drawn : folio by folio, + then left to right. Terminals with no folio position (spares) come + last, and the label only breaks ties, so the result stays stable and + predictable whatever the labels look like ("X1:N" and "X1:PE" stand + where they are drawn, not at the end). + Both the auto import and sort() use this, so the two orders of the + same strip can never fight each other. + @a a_ terminal to compare + @a b_ other terminal to compare + @return true if @a a_ comes before @a b_ +*/ +bool SortTerminalStripCommand::planOrderLessThan( + const QSharedPointer &a_, + const QSharedPointer &b_) +{ + if (!a_ || !b_) { + return a_ && !b_; //a missing terminal goes last + } + + const Element *ea = a_->element(); + const Element *eb = b_->element(); + auto folio_of = [](const Element *e_) -> int + { + return (e_ && e_->diagram()) ? e_->diagram()->folioIndex() : -1; + }; + + const int fa = folio_of(ea); + const int fb = folio_of(eb); + if (fa != fb) { + //A terminal with no folio has no place in the plan order + if (fa < 0) { + return false; + } + if (fb < 0) { + return true; + } + return fa < fb; + } + if (fa >= 0 && ea->pos() != eb->pos()) { + return PositionOrder::roundedXThenY(ea->pos(), eb->pos()); + } + return QETUtils::naturalLessThan(a_->label(), b_->label()); +} diff --git a/sources/TerminalStrip/UndoCommand/sortterminalstripcommand.h b/sources/TerminalStrip/UndoCommand/sortterminalstripcommand.h index eae51e782..514fa90bc 100644 --- a/sources/TerminalStrip/UndoCommand/sortterminalstripcommand.h +++ b/sources/TerminalStrip/UndoCommand/sortterminalstripcommand.h @@ -20,10 +20,12 @@ #include #include +#include #include class TerminalStrip; class PhysicalTerminal; +class RealTerminal; /** * @brief The SortTerminalStripCommand class @@ -38,6 +40,25 @@ class SortTerminalStripCommand : public QUndoCommand void undo() override; void redo() override; + /** + * @brief planOrderLessThan + * Order two real terminals the way the plan is drawn : folio by + * folio, then left to right (top to bottom only breaks ties + * within a column). A spare has no folio and comes last, by + * label, and the label breaks every remaining tie (two + * terminals at the very same spot), which keeps the result + * stable. + * + * The auto import sorts with it too, so the order it works out + * cannot be rewritten by the SortTerminalStripCommand it then + * pushes for the strip it just created. + * @param a_ : first terminal + * @param b_ : second terminal + * @return true if @a a_ comes before @a b_ + */ + static bool planOrderLessThan(const QSharedPointer &a_, + const QSharedPointer &b_); + private: void sort(); diff --git a/sources/TerminalStrip/terminalstripautoimport.cpp b/sources/TerminalStrip/terminalstripautoimport.cpp index 6e9508dda..808c7b2d8 100644 --- a/sources/TerminalStrip/terminalstripautoimport.cpp +++ b/sources/TerminalStrip/terminalstripautoimport.cpp @@ -128,7 +128,9 @@ namespace * whose label prefix or plant and location no longer match its strip. * * In both cases the order the user gave to a strip is never changed, - * only a strip created by the run itself is sorted. + * only a strip created by the run itself is sorted -- the way the plan + * is drawn : folio by folio, then left to right (see + * SortTerminalStripCommand::planOrderLessThan). */ TerminalStripAutoImport::ImportResult TerminalStripAutoImport::importFreeTerminals( QETProject *project, bool move_placed) @@ -187,16 +189,21 @@ TerminalStripAutoImport::ImportResult TerminalStripAutoImport::importFreeTermina for (auto it_ = groups_.begin(); it_ != groups_.end(); ++it_) { - auto by_label_ = [](const QSharedPointer &a_, - const QSharedPointer &b_) { - return QETUtils::naturalLessThan(a_->label(), b_->label()); + //The plan order : folio by folio, then left to right, so the + //strip comes out the way the plan is drawn instead of label + //by label ("N" and "PE" stay where they are on the sheet). + //SortTerminalStripCommand, pushed below for a strip this run + //creates, sorts with the same rule and cannot undo it. + auto by_plan_ = [](const QSharedPointer &a_, + const QSharedPointer &b_) { + return SortTerminalStripCommand::planOrderLessThan(a_, b_); }; std::sort(it_.value().free_terminals.begin(), - it_.value().free_terminals.end(), by_label_); + it_.value().free_terminals.end(), by_plan_); std::sort(it_.value().placed_terminals.begin(), it_.value().placed_terminals.end(), - [&by_label_](const auto &a_, const auto &b_) { - return by_label_(a_.first, b_.first); + [&by_plan_](const auto &a_, const auto &b_) { + return by_plan_(a_.first, b_.first); }); } diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index cde86eef6..13774c6a7 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -3314,8 +3314,9 @@ bool QetScriptApi::bridgeTerminals(int stripIndex, const QVariantList &realTermi /** @brief QetScriptApi::sortTerminalStrip - Reorder a strip's physical positions into the canonical order the - editor's own "sort" button computes, through SortTerminalStripCommand. + Reorder a strip's physical positions into the order the plan is + drawn in -- folio by folio, then left to right -- the same + SortTerminalStripCommand the editor's own "sort" button pushes. */ bool QetScriptApi::sortTerminalStrip(int stripIndex) { diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 644cf4725..097bd3533 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -332,6 +332,12 @@ target_link_libraries(tst_textresizehandles PRIVATE Qt::Test Qt::Widgets) # rectangle's does; a pivot placed by hand stays. qet_add_app_test(tst_linepivotresize tst_linepivotresize.cpp) +# The terminal strip sorts into the plan's own order -- folio by folio, then +# left to right -- instead of by label; the auto import and +# SortTerminalStripCommand share one comparator for it, so a strip the import +# creates cannot come out in another order than the sort it then pushes. +qet_add_app_test(tst_terminalstriporder tst_terminalstriporder.cpp) + # legacyelementtypes.h is header-only: the rewrite of element type names in # nomenclature queries saved before June 2022. add_executable(tst_legacyelementtypes tst_legacyelementtypes.cpp) diff --git a/tests/qttest/tst_terminalstriporder.cpp b/tests/qttest/tst_terminalstriporder.cpp new file mode 100644 index 000000000..1303f5b53 --- /dev/null +++ b/tests/qttest/tst_terminalstriporder.cpp @@ -0,0 +1,272 @@ +/* + 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 . +*/ +#include "qetapptest.h" + +#include "TerminalStrip/UndoCommand/sortterminalstripcommand.h" +#include "TerminalStrip/physicalterminal.h" +#include "TerminalStrip/realterminal.h" +#include "TerminalStrip/terminalstrip.h" +#include "diagram.h" +#include "properties/elementdata.h" +#include "qetgraphicsitem/element.h" +#include "qetproject.h" +#include "utils/qetutils.h" + +#include + +/** + The terminal strip sorts into the plan's own order -- folio by folio, + then left to right -- instead of by label, so "X1:N" and "X1:PE" + stand where they are drawn rather than at the end of the strip. The + auto import and SortTerminalStripCommand share the one comparator + (SortTerminalStripCommand::planOrderLessThan), so the order the + import works out cannot be rewritten by the sort it then pushes for + the strip it just created. + + fixtures/every_feature.qet has the strip X1 with element 891b70f2 + (x=300) left of 4861e268 (x=360) on the first folio, and 4861e268 + holds an empty label where 891b70f2 has a non-empty one: a label + sort would put 4861e268 first, which is the very swap this test + pins down. +*/ +class tst_terminalstriporder : public QObject +{ + Q_OBJECT + + QETProject *m_project = nullptr; + + Element *element(const QString &uuid) const + { + for (Diagram *d_ : m_project->diagrams()) { + for (Element *e_ : d_->elements()) { + if (e_->uuid().toString() == uuid) { + return e_; + } + } + } + return nullptr; + } + + TerminalStrip *strip() const + { + const auto strips_ = m_project->terminalStrip(); + return strips_.isEmpty() ? nullptr : strips_.first(); + } + + QSharedPointer terminalInStrip(const QString &element_uuid) const + { + auto *strip_ = strip(); + if (!strip_) { + return {}; + } + for (const auto &real_ : strip_->realTerminals()) { + if (real_ && real_->element() + && real_->element()->uuid().toString() == element_uuid) { + return real_; + } + } + return {}; + } + + /// The element uuids of the strip's physical positions, in strip order. + QStringList physicalOrder() const + { + QStringList order_; + auto *strip_ = strip(); + if (!strip_) { + return order_; + } + for (const auto &physical_ : strip_->physicalTerminal()) { + if (physical_ && physical_->realTerminalCount() + && physical_->realTerminals().first()->element()) { + order_ << physical_->realTerminals().first()->element()->uuid().toString(); + } + } + return order_; + } + +private slots: + void init(); + void cleanup(); + + void positionBeatsLabel(); + void folioComesBeforePosition(); + void spareWithoutFolioGoesLast(); + void sortKeepsThePlanOrder(); + void sortFollowsMovedElements(); +}; + +void tst_terminalstriporder::init() +{ + const QString fixture_ = QFINDTESTDATA("fixtures/every_feature.qet"); + QVERIFY(!fixture_.isEmpty()); + m_project = new QETProject(fixture_); + QCOMPARE(m_project->state(), QETProject::Ok); +} + +void tst_terminalstriporder::cleanup() +{ + delete m_project; + m_project = nullptr; +} + +/** + Two terminals of one folio, 891b70f2 left of 4861e268: the position + decides, and the label says the opposite (4861e268's is empty), so a + comparator by label would order them the wrong way round. +*/ +void tst_terminalstriporder::positionBeatsLabel() +{ + const auto a_ = terminalInStrip(QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + const auto b_ = terminalInStrip(QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}")); + QVERIFY(a_); + QVERIFY(b_); + QCOMPARE(a_->element()->pos().x() < b_->element()->pos().x(), true); + + QVERIFY(SortTerminalStripCommand::planOrderLessThan(a_, b_)); + QVERIFY(!SortTerminalStripCommand::planOrderLessThan(b_, a_)); + + //The labels would sort the other way round (b's is empty), which + //is exactly the order the plan must NOT follow + QVERIFY(QETUtils::naturalLessThan(b_->label(), a_->label())); +} + +/** + The page comes before the position ("page by page, then left to + right"): a terminal moved to the second folio sorts after every + terminal of the first one, even when it stands further left than + them. The element is put back on its own folio before the test + ends, so the project is left as it was. +*/ +void tst_terminalstriporder::folioComesBeforePosition() +{ + const auto a_ = terminalInStrip(QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + const auto b_ = terminalInStrip(QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}")); + QVERIFY(a_); + QVERIFY(b_); + Element *moved_ = a_->element(); + QVERIFY(moved_); + const QList folios_ = m_project->diagrams(); + QVERIFY(folios_.size() > 1); + Diagram *own_folio_ = moved_->diagram(); + Diagram *later_folio_ = folios_.at(1); + QVERIFY(later_folio_ != own_folio_); + const QPointF original_pos_ = moved_->pos(); + + //Put a on the second folio, left of where b stands on the first + own_folio_->removeItem(moved_); + later_folio_->addItem(moved_); + moved_->setPos(QPointF(50, 600)); + QVERIFY(moved_->diagram() == later_folio_); + + const bool folio_later_ = later_folio_->folioIndex() > own_folio_->folioIndex(); + const bool a_left_of_b_ = moved_->pos().x() < b_->element()->pos().x(); + const bool b_first_ = SortTerminalStripCommand::planOrderLessThan(b_, a_); + const bool a_first_ = SortTerminalStripCommand::planOrderLessThan(a_, b_); + + //Back to where it was, so the conductors drawn to it and the + //project's cleanup see the folio they were loaded from + later_folio_->removeItem(moved_); + own_folio_->addItem(moved_); + moved_->setPos(original_pos_); + QVERIFY(moved_->diagram() == own_folio_); + + QVERIFY(folio_later_); + QVERIFY(a_left_of_b_); //the position alone would put a first + QVERIFY(b_first_); //the page decides: b (first folio) first + QVERIFY(!a_first_); +} + +/** + A spare terminal stands on no folio: it has no place in the plan + order and goes last, whatever its label would say. +*/ +void tst_terminalstriporder::spareWithoutFolioGoesLast() +{ + ElementData data_; + data_.m_informations.addValue(QStringLiteral("label"), + QStringLiteral("AAA")); + const auto spare_ = RealTerminal::createSpare(data_); + QVERIFY(spare_); + QVERIFY(spare_->isSpare()); + + const auto a_ = terminalInStrip(QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + QVERIFY(a_); + //"AAA" would sort first by label alone + QVERIFY(QETUtils::naturalLessThan(spare_->label(), a_->label())); + + QVERIFY(SortTerminalStripCommand::planOrderLessThan(a_, spare_)); + QVERIFY(!SortTerminalStripCommand::planOrderLessThan(spare_, a_)); +} + +/** + The sort command, run on the fixture as it stands, keeps the strip + the way the plan is drawn: 891b70f2 (x=300) before 4861e268 + (x=360). The old label sort swapped them, since 4861e268's label is + the empty one. +*/ +void tst_terminalstriporder::sortKeepsThePlanOrder() +{ + auto *strip_ = strip(); + QVERIFY(strip_); + QCOMPARE(strip_->physicalTerminalCount(), 2); + + SortTerminalStripCommand command_(strip_); + command_.redo(); + + QCOMPARE(physicalOrder(), + QStringList({ + QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}"), + QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}"), + })); +} + +/** + The order follows the plan, not the strip's history: once the two + elements are swapped on the folio, sorting puts the strip the other + way round too. +*/ +void tst_terminalstriporder::sortFollowsMovedElements() +{ + auto *strip_ = strip(); + QVERIFY(strip_); + Element *a_ = element(QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + Element *b_ = element(QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}")); + QVERIFY(a_); + QVERIFY(b_); + + b_->setPos(100, 400); + a_->setPos(700, 400); + + SortTerminalStripCommand command_(strip_); + command_.redo(); + + QCOMPARE(physicalOrder(), + QStringList({ + QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}"), + QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}"), + })); +} + +int main(int argc, char **argv) +{ + QStandardPaths::setTestModeEnabled(true); + return QetAppTest::run(argc, argv); +} + +#include "tst_terminalstriporder.moc" From 29a8f018f47a4916223f2bed1505f7411897fdf9 Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Mon, 19 Oct 2026 21:43:27 +0200 Subject: [PATCH 05/12] Name where a cable goes at the end of a strip The strip graphic fanned the wires out again after the cable, which shows each core leaving the cable but says nothing about where it is going -- the far end was unnamed. Keep the bundle together at the end now: a short stub and a box with the Installation, Location and Label (BMK) of the piece the cable lands on, the same three fields the cable list computes for a target (cablelistrows.cpp), so the strip and the list can never disagree. The target is the first piece past the strip's own terminals the wires of the cable land on, in plan order; a report is no arrival (the cable continues on the linked sheet), several cores on one piece are one landing, and a cable which lands on nothing but the strip itself gets no box. The lines shrink to the end zone the Terminal plan settings give (Cable end length), so the box never grows past the strip the bounding rect reserved. RealTerminal::cableTarget() works the whole out: the cable is the one the wires drawn to the terminal carry, a name typed by hand is looked up among the cables of the project, and the box is drawn for the first wire of the cable group. tst_cabletarget checks the value rule (own strip no target, spare no target, no cable no target) and that painting with and without a target gives two different pictures. --- .../GraphicsItem/terminalstripdrawer.cpp | 59 +++- .../GraphicsItem/terminalstripdrawer.h | 6 + .../GraphicsItem/trueterminalstrip.cpp | 5 + .../GraphicsItem/trueterminalstrip.h | 1 + sources/TerminalStrip/realterminal.cpp | 146 +++++++++ sources/TerminalStrip/realterminal.h | 1 + tests/qttest/CMakeLists.txt | 5 + tests/qttest/tst_cabletarget.cpp | 307 ++++++++++++++++++ 8 files changed, 522 insertions(+), 8 deletions(-) create mode 100644 tests/qttest/tst_cabletarget.cpp diff --git a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp index f348bee7c..282729d30 100644 --- a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp +++ b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp @@ -270,14 +270,18 @@ struct CableCell QString cable; ///< name of the cable (hose) QString wire; ///< color / number of the wire bool shield{false}; ///< true if the terminal is the shield of the cable + QSharedPointer terminal; ///< to ask where the cable goes }; /** * @brief drawCables * Draw the cables under the terminals. Consecutive terminals who have the same cable name - * share the same cable: the wires go down from the terminals, join in the cable, - * and leave the cable with the same wire marks. The shield is linked to a dashed ellipse - * drawn around the cable. + * share the same cable: the wires go down from the terminals and join in the cable. + * The cable ends together -- a short stub, then a box naming where the cable goes + * (Installation, Location and Label of the piece it lands on). It does not fan out + * again after the cable: the wires drawn there suggested each core went its own way + * towards the target, which said nothing about where that target is. + * The shield is linked to a dashed ellipse drawn around the cable. * @param painter : the pen and the font must be set * @param cells : one cell per terminal, in the drawing order * @param pattern @@ -350,7 +354,6 @@ void drawCables(QPainter *painter, const QVector &cells, const Termin const qreal cable_x{(min_x + max_x) / 2}; const qreal bar_1_y{max_bottom + wire_length}; const qreal bar_2_y{bar_1_y + cable_length}; - const qreal end_y{bar_2_y + end_length}; //Wires between the terminals and the cable for (const auto &wire : std::as_const(wires)) @@ -363,12 +366,51 @@ void drawCables(QPainter *painter, const QVector &cells, const Termin //The cable painter->drawLine(QPointF{cable_x, bar_1_y}, QPointF{cable_x, bar_2_y}); - //Wires after the cable + //The bundle stays together at the end: the cross bar, a short + //stub, and a box naming where the cable goes -- the Installation, + //Location and Label (BMK) of the piece it lands on, the same + //three fields the cable list shows. The lines shrink to the end + //zone the settings leave, so the box never grows past the strip. painter->drawLine(QPointF{min_x, bar_2_y}, QPointF{max_x, bar_2_y}); - for (const auto &wire : std::as_const(wires)) + const qreal stub{qMin(6, end_length)}; + painter->drawLine(QPointF{cable_x, bar_2_y}, QPointF{cable_x, bar_2_y + stub}); + + const auto target_lines{wires.first().terminal ? wires.first().terminal->cableTarget() + : QVector()}; + if (!target_lines.isEmpty() && end_length - stub > 4) { - painter->drawLine(QPointF{wire.x, bar_2_y}, QPointF{wire.x, end_y}); - draw_wire_mark(wire.x, qMin(bar_2_y + 10, end_y), wire.wire, false); + QFont box_font{painter->font()}; + if (box_font.pixelSize() <= 0 && box_font.pointSizeF() > 0) { + box_font.setPixelSize(qMax(4, qRound(box_font.pointSizeF() * 96.0 / 72.0))); + } + const qreal room{end_length - stub}; + const auto fits_the_end_zone = [&target_lines, room](const QFont &font_) { + return QFontMetricsF{font_}.height() * target_lines.size() + 4 <= room; + }; + while (!fits_the_end_zone(box_font) && box_font.pixelSize() > 4) { + box_font.setPixelSize(box_font.pixelSize() - 1); + } + + const QFontMetricsF box_metrics{box_font}; + qreal box_width{6}; + for (const QString &line_ : target_lines) { + box_width = qMax(box_width, box_metrics.horizontalAdvance(line_) + 6); + } + const qreal box_height{box_metrics.height() * target_lines.size() + 4}; + const QRectF box{cable_x - box_width / 2, bar_2_y + stub, box_width, box_height}; + + painter->save(); + painter->setFont(box_font); + painter->drawRect(box); + for (auto i = 0 ; i < target_lines.size() ; ++i) + { + painter->drawText(QRectF{box.left() + 1, + box.top() + 2 + i * box_metrics.height(), + box.width() - 2, box_metrics.height()}, + Qt::AlignCenter, + target_lines.at(i)); + } + painter->restore(); } //The shield, linked to an ellipse around the cable @@ -1037,6 +1079,7 @@ void TerminalStripDrawer::paint(QPainter *painter) cable_cell.cable = real_terminal_vector[i]->cable(); cable_cell.wire = real_terminal_vector[i]->cableWire(); cable_cell.shield = real_terminal_vector[i]->isShield(); + cable_cell.terminal = real_terminal_vector[i]; } const bool have_cable{m_pattern->m_cable_wire_length > 0 && !cable_cell.cable.isEmpty()}; cable_cells.append(cable_cell); diff --git a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.h b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.h index 5588092fd..db080d234 100644 --- a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.h +++ b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.h @@ -78,6 +78,12 @@ namespace TerminalStripDrawer virtual QString cable() const { return QString(); } virtual QString cableWire() const { return QString(); } virtual bool isShield() const { return false; } + //Installation, Location and Label (BMK) of the piece the cable + //goes to at its far end -- the same three fields the cable list + //shows for a target -- drawn in a box under the cable. Empty + //when the terminal has no cable, or when the cable lands on + //nothing past the strip's own terminals. + virtual QVector cableTarget() const { return QVector(); } //Color and width (mm) of the terminal, only used to draw the terminal. //An invalid color means "no override" : the default color of the type is used. //A width <= 0 means "unknown" : the default width of the layout is used. diff --git a/sources/TerminalStrip/GraphicsItem/trueterminalstrip.cpp b/sources/TerminalStrip/GraphicsItem/trueterminalstrip.cpp index d5248fc2e..8a0199ab3 100644 --- a/sources/TerminalStrip/GraphicsItem/trueterminalstrip.cpp +++ b/sources/TerminalStrip/GraphicsItem/trueterminalstrip.cpp @@ -153,6 +153,11 @@ namespace TerminalStripDrawer return m_real ? m_real->isShield() : false; } + QVector TrueRealTerminal::cableTarget() const + { + return m_real ? m_real->cableTarget() : QVector(); + } + QColor TrueRealTerminal::color() const { return m_real ? m_real->color() : QColor(); diff --git a/sources/TerminalStrip/GraphicsItem/trueterminalstrip.h b/sources/TerminalStrip/GraphicsItem/trueterminalstrip.h index abde6dbd2..ac20d2bc8 100644 --- a/sources/TerminalStrip/GraphicsItem/trueterminalstrip.h +++ b/sources/TerminalStrip/GraphicsItem/trueterminalstrip.h @@ -64,6 +64,7 @@ namespace TerminalStripDrawer QString cable() const override; QString cableWire() const override; bool isShield() const override; + QVector cableTarget() const override; QColor color() const override; QColor stripeColor() const override; qreal widthMm() const override; diff --git a/sources/TerminalStrip/realterminal.cpp b/sources/TerminalStrip/realterminal.cpp index 3ec7ad462..787735cd7 100644 --- a/sources/TerminalStrip/realterminal.cpp +++ b/sources/TerminalStrip/realterminal.cpp @@ -22,7 +22,15 @@ #include "terminalcolor.h" #include "terminalcatalog.h" #include "../qetgraphicsitem/conductor.h" +#include "../qetgraphicsitem/element.h" +#include "../qetgraphicsitem/terminal.h" #include "../qetinformation.h" +#include "../qetproject.h" +#include "../cable/cable.h" +#include "../diagram.h" + +#include +#include /** * @brief RealTerminal @@ -322,6 +330,144 @@ bool RealTerminal::isShield() const { return false; } +/** + * @brief RealTerminal::cableTarget + * Where the cable of this terminal goes at its far end: the + * Installation, Location and Label (BMK) of the first piece the + * wires of the cable land on, past the strip's own terminals, in + * plan order -- the same three fields the cable list shows for a + * target, in the same order. An empty field is left out; a terminal + * with no cable, or a cable which lands on nothing else, gives no + * lines at all. + * + * The cable is the one the wires drawn to this terminal carry; a + * cable name typed by hand, with no wire behind it, is looked up by + * its name among the cables of the project. + * @return the lines to write in the box under the cable + */ +QVector RealTerminal::cableTarget() const +{ + if (m_spare || m_element.isNull()) { + return QVector(); + } + TerminalStrip *strip_ = parentStrip(); + if (!strip_ || !strip_->project()) { + return QVector(); + } + QETProject *project_{strip_->project()}; + + //The cable the wires drawn to this terminal are part of + QUuid cable_uuid_; + for (Terminal *terminal_ : m_element->terminals()) + { + if (!terminal_) { + continue; + } + for (Conductor *conductor_ : terminal_->conductors()) + { + if (conductor_ && !conductor_->properties().m_cable_uuid.isNull()) { + cable_uuid_ = conductor_->properties().m_cable_uuid; + break; + } + } + if (!cable_uuid_.isNull()) { + break; + } + } + if (cable_uuid_.isNull()) + { + //A cable name typed by hand, no wire behind it: the first + //cable of the project drawn under that name + const auto name_{cable()}; + if (name_.isEmpty()) { + return QVector(); + } + for (Cable *cable_ : project_->cables()) + { + if (cable_ && cable_->designation() == name_) { + cable_uuid_ = cable_->uuid(); + break; + } + } + if (cable_uuid_.isNull()) { + return QVector(); + } + } + + //The own side: this strip's terminals never are the target + QSet own_; + for (const auto &physical_ : strip_->physicalTerminal()) + { + if (!physical_) { + continue; + } + for (const auto &real_ : physical_->realTerminals()) { + if (real_ && real_->element()) { + own_.insert(real_->element()->uuid()); + } + } + } + + //Where the wires of the cable land: one piece whatever the + //number of its cores, never a report (the cable continues on + //the sheet the report points to) and never the strip's own + //terminals. Same rule as the cable list (cablelistrows.cpp). + QHash landed_; + for (Diagram *diagram_ : project_->diagrams()) + { + for (Conductor *conductor_ : diagram_->conductors()) + { + if (!conductor_ || conductor_->properties().m_cable_uuid != cable_uuid_) { + continue; + } + Terminal *ends_[2] = {conductor_->terminal1, conductor_->terminal2}; + for (Terminal *terminal_ : ends_) + { + if (!terminal_) { + continue; + } + Element *element_ = terminal_->parentElement(); + if (!element_ || (element_->linkType() & Element::AllReport)) { + continue; + } + if (own_.contains(element_->uuid())) { + continue; + } + landed_.insert(element_->uuid(), element_); + } + } + } + if (landed_.isEmpty()) { + return QVector(); + } + + //The first piece in plan order is the target, as the cable + //list chooses it + Element *target_{nullptr}; + for (Element *element_ : std::as_const(landed_)) { + if (!target_ || comparPos(element_, target_)) { + target_ = element_; + } + } + + const DiagramContext informations_{target_->elementInformations()}; + const auto installation_{informations_.value(QETInformation::ELMT_PLANT).toString()}; + const auto location_{informations_.value(QETInformation::ELMT_LOCATION).toString()}; + const auto label_{informations_.value(QETInformation::ELMT_LABEL).toString()}; + + QVector lines_; + if (!installation_.isEmpty()) { + lines_ << installation_; + } + if (!location_.isEmpty()) { + lines_ << location_; + } + if (!label_.isEmpty()) { + lines_ << label_; + } + return lines_; +} + /** * @brief RealTerminal::color * @return The color set by the user for this terminal, drawn in the diff --git a/sources/TerminalStrip/realterminal.h b/sources/TerminalStrip/realterminal.h index bf8d39bbd..d969871d7 100644 --- a/sources/TerminalStrip/realterminal.h +++ b/sources/TerminalStrip/realterminal.h @@ -74,6 +74,7 @@ class RealTerminal QString cable() const; QString cableWire() const; bool isShield() const; + QVector cableTarget() const; QColor color() const; QColor stripeColor() const; qreal section() const; diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index 644cf4725..a2e6496aa 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -332,6 +332,11 @@ target_link_libraries(tst_textresizehandles PRIVATE Qt::Test Qt::Widgets) # rectangle's does; a pivot placed by hand stays. qet_add_app_test(tst_linepivotresize tst_linepivotresize.cpp) +# The strip graphic names where a cable goes at its far end -- Installation, +# Location and Label of the first piece its wires land on past the strip, in +# plan order, the same three fields the cable list computes for a target. +qet_add_app_test(tst_cabletarget tst_cabletarget.cpp) + # legacyelementtypes.h is header-only: the rewrite of element type names in # nomenclature queries saved before June 2022. add_executable(tst_legacyelementtypes tst_legacyelementtypes.cpp) diff --git a/tests/qttest/tst_cabletarget.cpp b/tests/qttest/tst_cabletarget.cpp new file mode 100644 index 000000000..b87385aca --- /dev/null +++ b/tests/qttest/tst_cabletarget.cpp @@ -0,0 +1,307 @@ +/* + 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 . +*/ +#include "qetapptest.h" + +#include "TerminalStrip/GraphicsItem/trueterminalstrip.h" +#include "TerminalStrip/physicalterminal.h" +#include "TerminalStrip/realterminal.h" +#include "TerminalStrip/terminalstrip.h" +#include "cable/cable.h" +#include "conductorproperties.h" +#include "diagram.h" +#include "properties/elementdata.h" +#include "qetgraphicsitem/conductor.h" +#include "qetgraphicsitem/element.h" +#include "qetinformation.h" +#include "qetproject.h" + +#include +#include +#include + +/** + The terminal strip graphic names where a cable goes: Installation, + Location and Label (BMK) of the first piece the wires of the cable + land on, past the strip's own terminals, in plan order -- the same + three fields the cable list computes for a target + (cablelistrows.cpp), so the strip and the list can never disagree. + The graphic draws them in a small box under the cable, the bundle + staying together (RealTerminal::cableTarget()). + + fixtures/every_feature.qet has the strip X1 (elements 891b70f2 and + 4861e268) and the contact fc9cc920 wired to 891b70f2 by the + conductor 8bd16d7a; the test gives the contact the Installation, + Location and Label it reads back. The conductor 584b452e joins the + two strip terminals to each other. No conductor of the fixture + belongs to a cable yet. +*/ +class tst_cabletarget : public QObject +{ + Q_OBJECT + + QETProject *m_project = nullptr; + + Element *element(const QString &uuid) const + { + for (Diagram *diagram_ : m_project->diagrams()) { + for (Element *element_ : diagram_->elements()) { + if (element_->uuid().toString() == uuid) { + return element_; + } + } + } + return nullptr; + } + + QSharedPointer terminalInStrip(const QString &element_uuid) const + { + const auto strips_ = m_project->terminalStrip(); + if (strips_.isEmpty()) { + return {}; + } + for (const auto &real_ : strips_.first()->realTerminals()) { + if (real_ && real_->element() + && real_->element()->uuid().toString() == element_uuid) { + return real_; + } + } + return {}; + } + + Conductor *conductor(const QString &uuid) const + { + for (Diagram *diagram_ : m_project->diagrams()) { + for (Conductor *conductor_ : diagram_->conductors()) { + if (conductor_ && conductor_->uuid().toString() == uuid) { + return conductor_; + } + } + } + return nullptr; + } + + /// Let the drawn wires of @a conductor_ belong to the cable @a cable_uuid. + void claimCable(Conductor *conductor_, const QUuid &cable_uuid) const + { + auto properties_ = conductor_->properties(); + properties_.m_cable_uuid = cable_uuid; + conductor_->setProperties(properties_); + } + + /** + Same setup for the value and the drawing test: the contact the + strip's wire lands on carries the three fields the test reads + back (the fixture's own block has none), the wire belongs to a + new cable, and the terminal carries that cable's name the way + the strip editor stores it -- without a name the strip draws no + cable at all. @return false when the fixture is not the one this + test was written for. + */ + bool wireCableToTheContact() + { + Element *contact_ = element( + QStringLiteral("{fc9cc920-5822-421a-916e-a17100acdcf6}")); + if (!contact_) { + return false; + } + ElementData contact_data_{contact_->elementData()}; + contact_data_.m_informations.addValue(QETInformation::ELMT_PLANT, + QStringLiteral("Plant 7")); + contact_data_.m_informations.addValue(QETInformation::ELMT_LOCATION, + QStringLiteral("Cabinet 7")); + contact_data_.m_informations.addValue(QETInformation::ELMT_LABEL, + QStringLiteral("-S7")); + contact_->setElementData(contact_data_); + + Cable *cable_ = m_project->newCable(); + Conductor *wired_ = conductor( + QStringLiteral("{8bd16d7a-18c4-4fa0-9495-8dab65fd6408}")); + if (!cable_ || !wired_) { + return false; + } + claimCable(wired_, cable_->uuid()); + + //The name of the cable on the terminal, the way the strip + //editor stores it: without it the strip draws no cable at all + Element *terminal_ = element( + QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + if (!terminal_) { + return false; + } + ElementData terminal_data_{terminal_->elementData()}; + terminal_data_.m_informations.addValue(RealTerminal::cableInfoKey(), + QStringLiteral("X99")); + terminal_->setElementData(terminal_data_); + return true; + } + +private slots: + void init(); + void cleanup(); + + void noCableNoTarget(); + void targetIsTheOtherPiece(); + void ownStripIsNoTarget(); + void spareHasNoTarget(); + void paintsWithTargetBox(); +}; + +void tst_cabletarget::init() +{ + const QString fixture_ = QFINDTESTDATA("fixtures/every_feature.qet"); + QVERIFY(!fixture_.isEmpty()); + m_project = new QETProject(fixture_); + QCOMPARE(m_project->state(), QETProject::Ok); +} + +void tst_cabletarget::cleanup() +{ + delete m_project; + m_project = nullptr; +} + +/** + Nothing to name when no wire of the terminal belongs to a cable and + no cable of the project is drawn under a name the terminal carries. +*/ +void tst_cabletarget::noCableNoTarget() +{ + const auto a_ = terminalInStrip(QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + const auto b_ = terminalInStrip(QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}")); + QVERIFY(a_); + QVERIFY(b_); + QVERIFY(a_->cable().isEmpty()); + QVERIFY(a_->cableTarget().isEmpty()); + QVERIFY(b_->cableTarget().isEmpty()); +} + +/** + The cable of the strip lands on the contact past it: the three + fields of that piece, in the order the cable list shows them + (Installation, Location, Label), and nothing of the strip itself. +*/ +void tst_cabletarget::targetIsTheOtherPiece() +{ + QVERIFY(wireCableToTheContact()); + + const auto a_ = terminalInStrip(QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + QVERIFY(a_); + QCOMPARE(a_->cableTarget(), + QVector({ + QStringLiteral("Plant 7"), + QStringLiteral("Cabinet 7"), + QStringLiteral("-S7"), + })); + + //The other strip terminal carries no claimed wire of its own + //(584b452e joins it to a_ and is not claimed) and no cable name, + //so it has no target to name + const auto b_ = terminalInStrip(QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}")); + QVERIFY(b_); + QVERIFY(b_->cableTarget().isEmpty()); +} + +/** + A cable whose wires only ever join the terminals of the strip + itself lands on nothing past the strip: no target, no box. +*/ +void tst_cabletarget::ownStripIsNoTarget() +{ + Cable *cable_ = m_project->newCable(); + QVERIFY(cable_); + Conductor *inside_ = conductor( + QStringLiteral("{584b452e-a885-491a-98f1-fd348610b5e0}")); + QVERIFY(inside_); + claimCable(inside_, cable_->uuid()); + + const auto a_ = terminalInStrip(QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + const auto b_ = terminalInStrip(QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}")); + QVERIFY(a_); + QVERIFY(b_); + QVERIFY(a_->cableTarget().isEmpty()); + QVERIFY(b_->cableTarget().isEmpty()); +} + +/** + A spare terminal stands on no folio and is wired to nothing: it has + no target to name either. +*/ +void tst_cabletarget::spareHasNoTarget() +{ + ElementData data_; + data_.m_informations.addValue(QStringLiteral("label"), + QStringLiteral("AAA")); + const auto spare_ = RealTerminal::createSpare(data_); + QVERIFY(spare_); + QVERIFY(spare_->isSpare()); + QVERIFY(spare_->cableTarget().isEmpty()); +} + +/** + The strip graphic draws the box: the same strip painted with + something to name at the end of the cable and painted with nothing + to name come out as two different pictures -- the box is the + difference. Painting it also runs the whole drawCables() path. +*/ +void tst_cabletarget::paintsWithTargetBox() +{ + QVERIFY(wireCableToTheContact()); + + const auto strips_ = m_project->terminalStrip(); + QVERIFY(!strips_.isEmpty()); + TerminalStripDrawer::TerminalStripDrawer drawer{ + QSharedPointer{ + new TerminalStripDrawer::TrueTerminalStrip{strips_.first()}}, + QSharedPointer{new TerminalStripLayoutPattern}}; + + const QRectF bounds{drawer.boundingRect()}; + QVERIFY(!bounds.isEmpty()); + + const auto render = [&drawer, &bounds]() { + QImage image{bounds.size().toSize() + QSize{4, 4}, + QImage::Format_ARGB32_Premultiplied}; + image.fill(Qt::transparent); + QPainter painter{&image}; + painter.translate(-bounds.topLeft() + QPointF{2, 2}); + drawer.paint(&painter); + return image; + }; + + const QImage with_box{render()}; + + //Nothing left to name at the far end of the cable + Element *contact_ = element( + QStringLiteral("{fc9cc920-5822-421a-916e-a17100acdcf6}")); + QVERIFY(contact_); + ElementData contact_data_{contact_->elementData()}; + contact_data_.m_informations.addValue(QETInformation::ELMT_PLANT, QString()); + contact_data_.m_informations.addValue(QETInformation::ELMT_LOCATION, QString()); + contact_data_.m_informations.addValue(QETInformation::ELMT_LABEL, QString()); + contact_->setElementData(contact_data_); + + const QImage without_box{render()}; + QVERIFY(with_box != without_box); +} + +int main(int argc, char **argv) +{ + QStandardPaths::setTestModeEnabled(true); + return QetAppTest::run(argc, argv); +} + +#include "tst_cabletarget.moc" From b6312f1f78ec0ffaf640d8adc3831cdacd349285 Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Mon, 19 Oct 2026 22:22:40 +0200 Subject: [PATCH 06/12] Keep the wire numbers out of the terminal drawing The number of a wire was written from its mark towards the terminal without any regard for the room left: a colour code like 'gnge' ran into the drawing of the terminal. The length of the wires setting moves the mark, but every project has to be tuned to its longest number by hand, and a short length leaves no room at all. Shrink the font while the number is longer than the room between the mark and the terminal, and clip away what still does not fit even at the smallest size, so a number never crosses into the terminal -- the wire length and the font stay the settings to make the drawing pleasant, they are just no longer able to break it. tst_cabletarget paints the strip with a long number and without any and checks the two pictures are the same above the bottom of the terminal: the number is drawn below it or not at all. --- .../GraphicsItem/terminalstripdrawer.cpp | 38 ++++++++-- tests/qttest/tst_cabletarget.cpp | 73 +++++++++++++++++++ 2 files changed, 105 insertions(+), 6 deletions(-) diff --git a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp index 282729d30..fc343ac7c 100644 --- a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp +++ b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp @@ -299,19 +299,45 @@ void drawCables(QPainter *painter, const QVector &cells, const Termin const qreal text_height{font_metrics.height()}; //Draw a mark (small oblique line) on a wire and the number of the wire next to it. - //The text is written from the bottom to the top, at the left of the wire. - const auto draw_wire_mark = [&](qreal x, qreal mark_y, const QString &text, bool text_above) + //The text is written from the bottom to the top, at the left of the wire, starting + //above the mark and running towards the terminal: the font shrinks while the text + //is longer than the room left between the mark and the terminal, and what still + //does not fit is clipped away, so a long number never runs into the drawing of + //the terminal. The room left is @a terminal_bottom the bottom of that terminal. + const auto draw_wire_mark = [&](qreal x, qreal mark_y, qreal terminal_bottom, const QString &text) { painter->drawLine(QPointF{x - 3, mark_y + 3}, QPointF{x + 3, mark_y - 3}); if (text.isEmpty()) { return; } - const qreal text_width{font_metrics.horizontalAdvance(text)}; + //Room left between the mark and the terminal + const qreal room{mark_y - 4 - (terminal_bottom + 2)}; + if (room <= 0) { + return; + } + + QFont text_font{painter->font()}; + if (text_font.pixelSize() <= 0 && text_font.pointSizeF() > 0) { + text_font.setPixelSize(qMax(4, qRound(text_font.pointSizeF() * 96.0 / 72.0))); + } + while (QFontMetricsF{text_font}.horizontalAdvance(text) > room && + text_font.pixelSize() > 4) { + text_font.setPixelSize(text_font.pixelSize() - 1); + } + + const QFontMetricsF fitted_metrics{text_font}; + const qreal fitted_width{fitted_metrics.horizontalAdvance(text)}; + const qreal fitted_height{fitted_metrics.height()}; painter->save(); - painter->translate(x - 1, text_above ? mark_y - 4 : mark_y + 4 + text_width); + painter->setFont(text_font); + //The clip keeps the text under the terminal even when a number is + //so long that the smallest font does not fit it any more + painter->setClipRect(QRectF{x - 1 - fitted_height, terminal_bottom + 2, + fitted_height + 1, room}); + painter->translate(x - 1, mark_y - 4); painter->rotate(270); - painter->drawText(QRectF{0, -text_height, text_width + 2, text_height}, + painter->drawText(QRectF{0, -fitted_height, fitted_width + 2, fitted_height}, Qt::AlignLeft | Qt::AlignVCenter, text); painter->restore(); @@ -359,7 +385,7 @@ void drawCables(QPainter *painter, const QVector &cells, const Termin for (const auto &wire : std::as_const(wires)) { painter->drawLine(QPointF{wire.x, wire.bottom}, QPointF{wire.x, bar_1_y}); - draw_wire_mark(wire.x, qMax(wire.bottom + 4, bar_1_y - 10), wire.wire, true); + draw_wire_mark(wire.x, qMax(wire.bottom + 4, bar_1_y - 10), wire.bottom, wire.wire); } painter->drawLine(QPointF{min_x, bar_1_y}, QPointF{max_x, bar_1_y}); diff --git a/tests/qttest/tst_cabletarget.cpp b/tests/qttest/tst_cabletarget.cpp index b87385aca..1ca83a347 100644 --- a/tests/qttest/tst_cabletarget.cpp +++ b/tests/qttest/tst_cabletarget.cpp @@ -43,6 +43,10 @@ The graphic draws them in a small box under the cable, the bundle staying together (RealTerminal::cableTarget()). + Also here: the number of a wire must stay in the room left between + its mark and the terminal -- it shrinks, and what does not fit is + clipped, so it never runs into the drawing of the terminal. + fixtures/every_feature.qet has the strip X1 (elements 891b70f2 and 4861e268) and the contact fc9cc920 wired to 891b70f2 by the conductor 8bd16d7a; the test gives the contact the Installation, @@ -159,6 +163,7 @@ private slots: void ownStripIsNoTarget(); void spareHasNoTarget(); void paintsWithTargetBox(); + void wireLabelStaysOutOfTheTerminal(); }; void tst_cabletarget::init() @@ -298,6 +303,74 @@ void tst_cabletarget::paintsWithTargetBox() QVERIFY(with_box != without_box); } +/** + The number of a wire is written next to its mark, in the room left + between the mark and the terminal: it shrinks to fit there, and what + still does not fit is clipped away, so a long number never runs into + the drawing of the terminal. The two pictures -- one with a number + long enough to reach into the terminal, one without any -- must be + the same above the bottom of the terminal, and differ below it (the + number is drawn somewhere). +*/ +void tst_cabletarget::wireLabelStaysOutOfTheTerminal() +{ + QVERIFY(wireCableToTheContact()); + + auto pattern_ = QSharedPointer{new TerminalStripLayoutPattern}; + const auto strips_ = m_project->terminalStrip(); + QVERIFY(!strips_.isEmpty()); + TerminalStripDrawer::TerminalStripDrawer drawer{ + QSharedPointer{ + new TerminalStripDrawer::TrueTerminalStrip{strips_.first()}}, + pattern_}; + + const QRectF bounds{drawer.boundingRect()}; + QVERIFY(!bounds.isEmpty()); + + const auto render = [&drawer, &bounds]() { + QImage image{bounds.size().toSize() + QSize{4, 4}, + QImage::Format_ARGB32_Premultiplied}; + image.fill(Qt::transparent); + QPainter painter{&image}; + painter.translate(-bounds.topLeft() + QPointF{2, 2}); + drawer.paint(&painter); + return image; + }; + + //A number much longer than the room left over the mark + Element *terminal_ = element( + QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + QVERIFY(terminal_); + ElementData terminal_data_{terminal_->elementData()}; + terminal_data_.m_informations.addValue( + RealTerminal::cableWireInfoKey(), + QStringLiteral("gnge-gnge-gnge-gnge-gnge")); + terminal_->setElementData(terminal_data_); + const QImage with_label{render()}; + + //The same strip, the wire carries no number at all + terminal_data_.m_informations.addValue(RealTerminal::cableWireInfoKey(), + QString()); + terminal_->setElementData(terminal_data_); + const QImage without_label{render()}; + + //The number is drawn: the two pictures differ + QVERIFY(with_label != without_label); + + //The terminals of this strip all stand on the last level of the + //pattern, so their bottom is the same rectangle + const qreal terminal_bottom{pattern_->m_terminal_rect.at(3).bottom()}; + const auto rows_to_check{qMin(qRound(terminal_bottom - bounds.topLeft().y() + 2), + with_label.height())}; + for (auto y = 0 ; y < rows_to_check ; ++y) + { + for (auto x = 0 ; x < with_label.width() ; ++x) + { + QCOMPARE(with_label.pixel(x, y), without_label.pixel(x, y)); + } + } +} + int main(int argc, char **argv) { QStandardPaths::setTestModeEnabled(true); From 79ab711d93314cf9ee07a564cd842b1e9c20c3fe Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Tue, 20 Oct 2026 09:00:58 +0200 Subject: [PATCH 07/12] Size the cable target box to the cross bar The box hugged its own text, so it stayed small whatever room the end zone gave, and its font shrank until the three lines squeezed into it. It now spans the cross bar the cable bundle ends on -- the width the strip already draws there -- and keeps the font of the drawing: when its lines (Installation, Location and Label) are longer than that cross bar, it is the box which grows, the text is never cut down to fit a box. The end zone of the default layout leaves the room those three lines need at that font, so a target is not squeezed by the drawing itself. The setting still rules it: a smaller zone shrinks the text again. tst_cabletarget checks the box is drawn three lines tall at the font of the drawing, and that the default zone leaves that room. --- .../properties/terminalstriplayoutpattern.h | 7 +- .../GraphicsItem/terminalstripdrawer.cpp | 24 ++++-- tests/qttest/tst_cabletarget.cpp | 74 +++++++++++++++++++ 3 files changed, 95 insertions(+), 10 deletions(-) diff --git a/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutpattern.h b/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutpattern.h index 4dc941601..9a17f1df3 100644 --- a/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutpattern.h +++ b/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutpattern.h @@ -127,8 +127,11 @@ class TerminalStripLayoutPattern qreal m_cable_wire_length{40}; //Length of the cable qreal m_cable_length{60}; - //Length of the wires after the cable - qreal m_cable_end_length{40}; + //Length of the wires after the cable. It leaves the room the box + //naming where the cable goes needs: three lines (Installation, + //Location, Label) at the default font of 15 px, so the box does + //not have to shrink its text down. + qreal m_cable_end_length{80}; qreal m_bridge_point_d{5}; QVector m_bridge_point_y_offset{50,70,90,110}; diff --git a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp index fc343ac7c..70c5fe7da 100644 --- a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp +++ b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp @@ -395,8 +395,9 @@ void drawCables(QPainter *painter, const QVector &cells, const Termin //The bundle stays together at the end: the cross bar, a short //stub, and a box naming where the cable goes -- the Installation, //Location and Label (BMK) of the piece it lands on, the same - //three fields the cable list shows. The lines shrink to the end - //zone the settings leave, so the box never grows past the strip. + //three fields the cable list shows. The box spans the cross bar + //and never shrinks its text down to fit: when its lines are + //longer than the cross bar, the box is the one which grows. painter->drawLine(QPointF{min_x, bar_2_y}, QPointF{max_x, bar_2_y}); const qreal stub{qMin(6, end_length)}; painter->drawLine(QPointF{cable_x, bar_2_y}, QPointF{cable_x, bar_2_y + stub}); @@ -405,23 +406,30 @@ void drawCables(QPainter *painter, const QVector &cells, const Termin : QVector()}; if (!target_lines.isEmpty() && end_length - stub > 4) { + //The font of the drawing, shrunk only while the lines do not + //fit the height the settings leave for the box + const qreal room{end_length - stub}; QFont box_font{painter->font()}; if (box_font.pixelSize() <= 0 && box_font.pointSizeF() > 0) { box_font.setPixelSize(qMax(4, qRound(box_font.pointSizeF() * 96.0 / 72.0))); } - const qreal room{end_length - stub}; - const auto fits_the_end_zone = [&target_lines, room](const QFont &font_) { + const auto fits_the_height = [&target_lines, room](const QFont &font_) { return QFontMetricsF{font_}.height() * target_lines.size() + 4 <= room; }; - while (!fits_the_end_zone(box_font) && box_font.pixelSize() > 4) { + while (!fits_the_height(box_font) && box_font.pixelSize() > 4) { box_font.setPixelSize(box_font.pixelSize() - 1); } + //As wide as the cross bar the cable ends on -- the strip of + //one single terminal has no width to take, so a small floor + //keeps the box drawn at all -- and wider still when the lines + //of the target are longer than that const QFontMetricsF box_metrics{box_font}; - qreal box_width{6}; + qreal box_width{qMax(max_x - min_x, 16)}; for (const QString &line_ : target_lines) { box_width = qMax(box_width, box_metrics.horizontalAdvance(line_) + 6); } + const qreal box_height{box_metrics.height() * target_lines.size() + 4}; const QRectF box{cable_x - box_width / 2, bar_2_y + stub, box_width, box_height}; @@ -430,9 +438,9 @@ void drawCables(QPainter *painter, const QVector &cells, const Termin painter->drawRect(box); for (auto i = 0 ; i < target_lines.size() ; ++i) { - painter->drawText(QRectF{box.left() + 1, + painter->drawText(QRectF{box.left() + 3, box.top() + 2 + i * box_metrics.height(), - box.width() - 2, box_metrics.height()}, + box.width() - 6, box_metrics.height()}, Qt::AlignCenter, target_lines.at(i)); } diff --git a/tests/qttest/tst_cabletarget.cpp b/tests/qttest/tst_cabletarget.cpp index 1ca83a347..ee1ef2dbf 100644 --- a/tests/qttest/tst_cabletarget.cpp +++ b/tests/qttest/tst_cabletarget.cpp @@ -47,6 +47,10 @@ its mark and the terminal -- it shrinks, and what does not fit is clipped, so it never runs into the drawing of the terminal. + Also here: the box at the end of a cable is written at the font of + the drawing -- the end zone the settings leave is what gives it the + room, so three lines never end up squeezed into a tiny text. + fixtures/every_feature.qet has the strip X1 (elements 891b70f2 and 4861e268) and the contact fc9cc920 wired to 891b70f2 by the conductor 8bd16d7a; the test gives the contact the Installation, @@ -164,6 +168,7 @@ private slots: void spareHasNoTarget(); void paintsWithTargetBox(); void wireLabelStaysOutOfTheTerminal(); + void targetBoxKeepsTheDrawingFont(); }; void tst_cabletarget::init() @@ -371,6 +376,75 @@ void tst_cabletarget::wireLabelStaysOutOfTheTerminal() } } +/** + The box at the end of a cable keeps the font of the drawing: with + its three lines (Installation, Location and Label) and the end zone + the settings leave, the text is still written at the font of the + strip instead of being squeezed into a tiny one. The box is the + lowest thing the strip draws, so its bottom line is the lowest + painted row, and it must land where three lines of that font need + it to. +*/ +void tst_cabletarget::targetBoxKeepsTheDrawingFont() +{ + QVERIFY(wireCableToTheContact()); + + auto pattern_ = QSharedPointer{new TerminalStripLayoutPattern}; + const auto strips_ = m_project->terminalStrip(); + QVERIFY(!strips_.isEmpty()); + TerminalStripDrawer::TerminalStripDrawer drawer{ + QSharedPointer{ + new TerminalStripDrawer::TrueTerminalStrip{strips_.first()}}, + pattern_}; + + const QRectF bounds{drawer.boundingRect()}; + QVERIFY(!bounds.isEmpty()); + QImage image{bounds.size().toSize() + QSize{4, 4}, + QImage::Format_ARGB32_Premultiplied}; + image.fill(Qt::transparent); + QPainter painter{&image}; + painter.translate(-bounds.topLeft() + QPointF{2, 2}); + drawer.paint(&painter); + painter.end(); + + qreal lowest{-1}; + for (auto y = 0 ; y < image.height() ; ++y) + { + for (auto x = 0 ; x < image.width() ; ++x) + { + if (image.pixel(x, y) != qRgba(0, 0, 0, 0)) { + lowest = y; + break; + } + } + } + QVERIFY(lowest >= 0); + + //The end zone the default layout leaves is what gives the box its + //room: three lines of the default font must fit in it, or the text + //of a target is squeezed down again by the drawing itself + const QFontMetricsF metrics{pattern_->font()}; + const qreal room{pattern_->m_cable_end_length + - qMin(6, pattern_->m_cable_end_length)}; + QVERIFY2(metrics.height() * 3 + 4 <= room, + "the default end zone leaves no room for three lines of the " + "default font, a target would be squeezed again"); + + //Where the bottom line of the box lands when it holds the three + //lines of its text at the font of the drawing + const qreal terminals_bottom{pattern_->m_terminal_rect.at(3).bottom()}; + const qreal bar_2{terminals_bottom + pattern_->m_cable_wire_length + + pattern_->m_cable_length}; + const qreal box_bottom{bar_2 + qMin(6, pattern_->m_cable_end_length) + + metrics.height() * 3 + 4}; + + //The strip draws nothing under the box, so the lowest painted row + //is its bottom line -- within a pixel, the outline of a rectangle + //is painted on its edge + const qreal row{box_bottom - bounds.top() + 2}; + QVERIFY(lowest + 2 >= row); +} + int main(int argc, char **argv) { QStandardPaths::setTestModeEnabled(true); From 6e83012109c62ef845d270f887fc427acec2e014 Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Tue, 20 Oct 2026 09:01:08 +0200 Subject: [PATCH 08/12] Draw the cables in the realistic style too The strip seen from above drew no cable at all: it had no zone for one and no code to draw it, so a strip drawn realistically told nothing about where its cables go while the same strip drawn schematically did. The drawing itself was already shared between nothing -- it lived in one style only, so it is now Helpers::drawCables() with its cells, and both styles hand it the terminals they show. The realistic style collects one wire exit per level spread over the width of the terminal body, all of them leaving the strip at its bottom edge, and reserves the zone under the strip for them as soon as one terminal carries a cable, exactly like the schematic style does. tst_cabletarget checks the realistic style paints under the body of the strip when a terminal carries a cable, and drops that zone when it does not. --- .../realisticterminalstripdrawer.cpp | 66 +++++++++++++++- .../GraphicsItem/terminalstripdrawer.cpp | 25 +++---- .../GraphicsItem/terminalstripdrawerhelpers.h | 16 ++++ tests/qttest/tst_cabletarget.cpp | 75 +++++++++++++++++++ 4 files changed, 166 insertions(+), 16 deletions(-) diff --git a/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.cpp b/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.cpp index 8b145aef2..b1ee952e6 100644 --- a/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.cpp +++ b/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.cpp @@ -522,6 +522,26 @@ struct BridgePoint qreal width{0}; }; + //True when at least one terminal of the strip carries a cable : the strip + //then needs the zone drawn under it to let the cables out, like the + //schematic style does. +bool haveCable(const AbstractTerminalStripInterface *strip) +{ + if (!strip) { + return false; + } + for (const auto &physical_t : strip->physicalTerminal()) + { + for (const auto &real_t : physical_t->realTerminals()) + { + if (real_t && !real_t->cable().isEmpty()) { + return true; + } + } + } + return false; +} + } //End anonymous namespace QRectF boundingRect(const AbstractTerminalStripInterface *strip, @@ -535,7 +555,20 @@ QRectF boundingRect(const AbstractTerminalStripInterface *strip, } } const qreal s_{pattern.m_realistic_px_per_mm}; - return QRectF{0, 0, width_mm_ * s_, pattern.m_realistic_length_mm * s_}; + const qreal length_{pattern.m_realistic_length_mm * s_}; + + //The cables are drawn under the strip, in the zone the schematic + //style reserves for them too + qreal extra_bottom_{0}; + if (pattern.m_cable_wire_length > 0 && haveCable(strip)) + { + //4 is the diameter of the circle at the end of the wires + extra_bottom_ = pattern.m_cable_wire_length + + std::max(0, pattern.m_cable_length) + + std::max(0, pattern.m_cable_end_length) + + 4; + } + return QRectF{0, 0, width_mm_ * s_, length_ + extra_bottom_}; } void paint(QPainter *painter, @@ -595,6 +628,7 @@ void paint(QPainter *painter, } QHash> bridges_; + QVector cable_cells_; for (const auto &g_ : geometry(&strip)) { @@ -603,6 +637,24 @@ void paint(QPainter *painter, const auto first_{g_.reals.first()}; const int levels_{static_cast(g_.reals.size())}; + //Cables (hoses) of this terminal : one wire exit by level, spread over + //the width of the body, all of them leaving the strip at its bottom edge + for (int k = 0 ; k < levels_ ; ++k) + { + const auto &real_t{g_.reals.at(k)}; + if (!real_t || real_t->cable().isEmpty()) { + continue; + } + Helpers::CableCell cell_; + cell_.x = x_ + w_ * (k + 0.5) / levels_; + cell_.bottom = length_; + cell_.cable = real_t->cable(); + cell_.wire = real_t->cableWire(); + cell_.shield = real_t->isShield(); + cell_.terminal = real_t; + cable_cells_.append(cell_); + } + //A terminal chosen in the catalog is drawn with its own clamp, so that a strip //mixing screw and spring terminals is drawn as it is. The spring-cage clamps //(opened with a screwdriver) are drawn like the push-in ones. @@ -793,6 +845,18 @@ void paint(QPainter *painter, draw_end_stop(); + //The cables under the strip : the wires of a cable join it, and the + //cable ends together in the box naming where it goes -- drawn exactly + //like the schematic style draws them, so both styles tell the same thing + if (!cable_cells_.isEmpty()) + { + painter->save(); + painter->setFont(pattern.font()); + painter->setPen(QPen(Qt::black, 0.5)); + Helpers::drawCables(painter, cable_cells_, pattern); + painter->restore(); + } + painter->restore(); } diff --git a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp index 70c5fe7da..52cd984e0 100644 --- a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp +++ b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp @@ -29,6 +29,10 @@ namespace TerminalStripDrawer { + //The cells of a cable drawing live with the helpers, so both drawing + //styles build the same thing and pass it to Helpers::drawCables() +using Helpers::CableCell; + /** * @brief TerminalStripDrawer::TerminalStripDrawer * @param strip @@ -259,20 +263,6 @@ void drawConnections(QPainter *painter, const QRectF &terminal_rect, qreal lengt } } -/** - * @brief The CableCell struct - * What is needed to draw the cable under a terminal. - */ -struct CableCell -{ - qreal x{0}; ///< x of the middle of the terminal, in the strip coordinates - qreal bottom{0}; ///< y of the bottom of the terminal - QString cable; ///< name of the cable (hose) - QString wire; ///< color / number of the wire - bool shield{false}; ///< true if the terminal is the shield of the cable - QSharedPointer terminal; ///< to ask where the cable goes -}; - /** * @brief drawCables * Draw the cables under the terminals. Consecutive terminals who have the same cable name @@ -941,6 +931,11 @@ void Helpers::drawTypeSymbol(QPainter *painter, const QRectF &box, ::TerminalStripDrawer::drawTypeSymbol(painter, box, type, led); } +void Helpers::drawCables(QPainter *painter, const QVector &cells, + const TerminalStripLayoutPattern &pattern) { + ::TerminalStripDrawer::drawCables(painter, cells, pattern); +} + /** * @brief TerminalStripDrawer::paint * @param painter @@ -1368,7 +1363,7 @@ void TerminalStripDrawer::paint(QPainter *painter) painter->setPen(cable_pen); painter->setFont(m_pattern->font()); painter->setBrush(Qt::NoBrush); - drawCables(painter, cable_cells, *m_pattern); + Helpers::drawCables(painter, cable_cells, *m_pattern); painter->restore(); } } diff --git a/sources/TerminalStrip/GraphicsItem/terminalstripdrawerhelpers.h b/sources/TerminalStrip/GraphicsItem/terminalstripdrawerhelpers.h index 7cbafd5ea..494262358 100644 --- a/sources/TerminalStrip/GraphicsItem/terminalstripdrawerhelpers.h +++ b/sources/TerminalStrip/GraphicsItem/terminalstripdrawerhelpers.h @@ -42,6 +42,22 @@ namespace TerminalStripDrawer //Symbol of the type of the terminal (fuse, diode, led...) scaled to fit in box void drawTypeSymbol(QPainter *painter, const QRectF &box, ElementData::TerminalType type, bool led); + //What is needed to draw the cable under a terminal + struct CableCell + { + qreal x{0}; ///< x of the middle of the terminal, in the strip coordinates + qreal bottom{0}; ///< y of the bottom of the terminal + QString cable; ///< name of the cable (hose) + QString wire; ///< color / number of the wire + bool shield{false}; ///< true if the terminal is the shield of the cable + QSharedPointer terminal; ///< to ask where the cable goes + }; + //Draw the cables under the terminals, whichever style draws them : + //consecutive terminals with the same cable share one cable, which ends + //together in a short stub and a box naming where it goes (see drawCables() + //in terminalstripdrawer.cpp for the whole drawing) + void drawCables(QPainter *painter, const QVector &cells, + const TerminalStripLayoutPattern &pattern); } } diff --git a/tests/qttest/tst_cabletarget.cpp b/tests/qttest/tst_cabletarget.cpp index ee1ef2dbf..f34dd92f8 100644 --- a/tests/qttest/tst_cabletarget.cpp +++ b/tests/qttest/tst_cabletarget.cpp @@ -17,6 +17,7 @@ */ #include "qetapptest.h" +#include "TerminalStrip/GraphicsItem/realisticterminalstripdrawer.h" #include "TerminalStrip/GraphicsItem/trueterminalstrip.h" #include "TerminalStrip/physicalterminal.h" #include "TerminalStrip/realterminal.h" @@ -51,6 +52,10 @@ the drawing -- the end zone the settings leave is what gives it the room, so three lines never end up squeezed into a tiny text. + Both drawing styles are covered: the schematic one and the + realistic one (the strip seen from above), which paint the same + cable through Helpers::drawCables(). + fixtures/every_feature.qet has the strip X1 (elements 891b70f2 and 4861e268) and the contact fc9cc920 wired to 891b70f2 by the conductor 8bd16d7a; the test gives the contact the Installation, @@ -169,6 +174,7 @@ private slots: void paintsWithTargetBox(); void wireLabelStaysOutOfTheTerminal(); void targetBoxKeepsTheDrawingFont(); + void realisticStyleDrawsTheCable(); }; void tst_cabletarget::init() @@ -445,6 +451,75 @@ void tst_cabletarget::targetBoxKeepsTheDrawingFont() QVERIFY(lowest + 2 >= row); } +/** + The realistic style draws the cable too. The strip seen from above + reserves the zone under it when one of its terminals carries a + cable, and the wires, the cable and the box naming where it goes + are painted in it -- exactly what the schematic style draws, since + both styles now share the same Helpers::drawCables(). +*/ +void tst_cabletarget::realisticStyleDrawsTheCable() +{ + QVERIFY(wireCableToTheContact()); + + const auto strips_ = m_project->terminalStrip(); + QVERIFY(!strips_.isEmpty()); + const auto strip_ = QSharedPointer{ + new TerminalStripDrawer::TrueTerminalStrip{strips_.first()}}; + TerminalStripLayoutPattern pattern_; + + const auto bounds_of = [&strip_, &pattern_]() { + return TerminalStripDrawer::Realistic::boundingRect(strip_.data(), + pattern_); + }; + const auto render = [&strip_, &pattern_](const QRectF &bounds) { + QImage image{bounds.size().toSize() + QSize{4, 4}, + QImage::Format_ARGB32_Premultiplied}; + image.fill(Qt::transparent); + QPainter painter{&image}; + painter.translate(-bounds.topLeft() + QPointF{2, 2}); + TerminalStripDrawer::Realistic::paint( + &painter, *strip_, pattern_, + TerminalStripLayoutPattern::DrawStyle::RealisticPushIn); + return image; + }; + + const QRectF with_bounds{bounds_of()}; + const QImage with_cable{render(with_bounds)}; + + //The body of the strip ends there : everything painted under it + //belongs to the cable + const qreal body_bottom{ + pattern_.m_realistic_length_mm * pattern_.m_realistic_px_per_mm}; + QVERIFY(with_bounds.height() > body_bottom); + + int painted_{0}; + for (auto y = qRound(body_bottom) + 4 ; y + 4 < with_cable.height() ; ++y) + { + for (auto x = 0 ; x + 4 < with_cable.width() ; ++x) + { + if (with_cable.pixel(x, y) != qRgba(0, 0, 0, 0)) { + ++painted_; + } + } + } + //The wires and the cable are drawn in that zone + QVERIFY(painted_ > 0); + + //The same strip, its terminal carries no cable name any more + Element *terminal_ = element( + QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + QVERIFY(terminal_); + ElementData terminal_data_{terminal_->elementData()}; + terminal_data_.m_informations.addValue(RealTerminal::cableInfoKey(), + QString()); + terminal_->setElementData(terminal_data_); + + //No cable to let out : the strip does not reserve the zone + const QRectF without_bounds{bounds_of()}; + QVERIFY(without_bounds.height() < with_bounds.height()); +} + int main(int argc, char **argv) { QStandardPaths::setTestModeEnabled(true); From dc92a57873542ebd0c7fcfd87cff9b997de1d60c Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Tue, 20 Oct 2026 10:49:24 +0200 Subject: [PATCH 09/12] Add a Target column to the terminal strip editor Reading where a cable lands meant going back to the drawing: the strip editor said which cable a terminal carries and which core, but never what the cable goes to. A single core (1x16) could not be followed from the terminal to its piece at all. The new Target column names it: Installation, Location and Label (BMK) of the piece the wires land on, past this strip, joined in one line -- the very three fields the strip drawing writes in the box under its cable, in the same order (RealTerminal::cableTarget()). It is worked out of the project like the Cable cells and so follows their rule: a target the drawing names is read only, for only the drawing decides it, and a cell nothing is worked out for takes an edit, so a target the drawing cannot find -- a cable landing on a piece with no Installation, Location nor Label set, a cable going to another strip -- is still worth naming by hand. The text typed in is stored in the element (RealTerminal::cableTargetInfoKey()) and kept until a cable works a target out again, and back once that cable goes, exactly like a cable designation typed by hand: the automatic value is never written, so no stale text can be left behind. The column takes the width of its own text, so a long Installation is never cut off. A text typed in is written the way the fields are shown in the column, "Installation / Location / Label", and gives one line per field: the box under the cable draws three lines without any slash in them, just like a target worked out from the project, instead of one long line carrying its separators as text. tst_terminalcableauto checks the column is empty -- and editable -- without a cable, reads "Plant 7 / Cabinet 7 / -S7" with one, goes empty -- and editable again -- when the core is removed, takes no edit while the drawing names the target, and is headed "Target"; a second case checks a target typed in by hand is taken by the cell, stored by Apply, shown again by a fresh model, set aside for the read only target of the drawing once a cable lands somewhere worth naming, and back once that cable goes; a third one the line per field. tst_cable target paints a strip whose cable ends on one field and the same strip ending on three, the second box two lines deeper than the first. --- sources/TerminalStrip/realterminal.cpp | 53 +++- sources/TerminalStrip/realterminal.h | 10 + sources/TerminalStrip/ui/modelTerminalData.h | 26 ++ .../TerminalStrip/ui/terminalstripeditor.cpp | 14 +- .../TerminalStrip/ui/terminalstripmodel.cpp | 64 +++-- sources/TerminalStrip/ui/terminalstripmodel.h | 17 +- tests/qttest/tst_cabletarget.cpp | 94 ++++++- tests/qttest/tst_terminalcableauto.cpp | 262 ++++++++++++++++++ 8 files changed, 512 insertions(+), 28 deletions(-) diff --git a/sources/TerminalStrip/realterminal.cpp b/sources/TerminalStrip/realterminal.cpp index f4354bc75..8b86dd31f 100644 --- a/sources/TerminalStrip/realterminal.cpp +++ b/sources/TerminalStrip/realterminal.cpp @@ -407,6 +407,57 @@ bool RealTerminal::isShield() const { /** * @brief RealTerminal::cableTarget + * Where the cable of this terminal goes at its far end: the lines to + * write in the box under the cable. What the project itself says when + * a cable lands on a piece (see automaticCableTarget()), and the text + * typed in by hand when none does -- the Target cell of the terminal + * strip editor is read only in the first case, so a target the drawing + * cannot work out is still worth naming by hand. A text typed in is + * written the way the fields are shown in the column, + * "Installation / Location / Label", and gives one line per field. + * @return the lines to write in the box under the cable + */ +QVector RealTerminal::cableTarget() const +{ + const auto automatic_{automaticCableTarget()}; + if (!automatic_.isEmpty()) { + return automatic_; + } + + if (hasData()) + { + const auto typed_{terminalData().m_informations + .value(cableTargetInfoKey()).toString()}; + if (!typed_.isEmpty()) + { + //Written the way the fields are shown in the column, + //"Installation / Location / Label": the box draws one line + //per field, without the separators, like it does for a + //target worked out from the project + QVector lines_; + for (const QString &field_ + : typed_.split(QStringLiteral(" / "), Qt::SkipEmptyParts)) { + lines_ << field_; + } + return lines_; + } + } + return QVector(); +} + +/** + * @brief RealTerminal::cableTargetIsAutomatic + * @return true when the target of this terminal is worked out from the + * project -- one of its cables lands on a piece worth naming -- and + * false when only a text typed in by hand is there to show. + */ +bool RealTerminal::cableTargetIsAutomatic() const +{ + return !automaticCableTarget().isEmpty(); +} + +/** + * @brief RealTerminal::automaticCableTarget * Where the cable of this terminal goes at its far end: the * Installation, Location and Label (BMK) of the first piece the * wires of the cable land on, past the strip's own terminals, in @@ -420,7 +471,7 @@ bool RealTerminal::isShield() const { * its name among the cables of the project. * @return the lines to write in the box under the cable */ -QVector RealTerminal::cableTarget() const +QVector RealTerminal::automaticCableTarget() const { if (m_spare || m_element.isNull()) { return QVector(); diff --git a/sources/TerminalStrip/realterminal.h b/sources/TerminalStrip/realterminal.h index d0c4dcb76..61f44295a 100644 --- a/sources/TerminalStrip/realterminal.h +++ b/sources/TerminalStrip/realterminal.h @@ -55,6 +55,11 @@ class RealTerminal void setPhysicalTerminal(const QSharedPointer &phy_t); + ///Where the wires of a cable land, worked out from the project: + ///empty when no cable lands on a piece worth naming (cableTarget() + ///then falls back on the text typed in by hand). + QVector automaticCableTarget() const; + public: ~RealTerminal(); static QSharedPointer createSpare(const ElementData &data, const QUuid &uuid = QUuid()); @@ -76,6 +81,9 @@ class RealTerminal bool cableIsAutomatic() const; bool isShield() const; QVector cableTarget() const; + ///True when cableTarget() is worked out from the project (read only), + ///false when it is the text typed in by hand (which may be edited) + bool cableTargetIsAutomatic() const; QColor color() const; QColor stripeColor() const; qreal section() const; @@ -86,6 +94,8 @@ class RealTerminal //Keys used to store the cable data in the element informations static QString cableInfoKey() { return QStringLiteral("terminal_cable"); } static QString cableWireInfoKey() { return QStringLiteral("terminal_cable_wire"); } + //Target typed in by hand, only read when the cables give none + static QString cableTargetInfoKey() { return QStringLiteral("terminal_cable_target"); } static QString shieldInfoKey() { return QStringLiteral("terminal_shield"); } //Keys used to store the color and the section (mm2) of the terminal static QString colorInfoKey() { return QStringLiteral("terminal_color"); } diff --git a/sources/TerminalStrip/ui/modelTerminalData.h b/sources/TerminalStrip/ui/modelTerminalData.h index e947d4dc5..0e8778832 100644 --- a/sources/TerminalStrip/ui/modelTerminalData.h +++ b/sources/TerminalStrip/ui/modelTerminalData.h @@ -20,11 +20,30 @@ #include #include +#include #include "../../qetgraphicsitem/element.h" #include "../realterminal.h" struct modelRealTerminalData { + ///The Installation, Location and Label (BMK) of the piece the cable + ///lands on, joined in one line : what the strip draws in the box + ///under its cable (RealTerminal::cableTarget), in the same order. + ///A target typed in by hand is one single line, and nothing is + ///shown when the cable lands on nothing worth naming and no text + ///was ever written in. + static QString targetText(const QVector &target_) + { + QStringList fields_; + for (const QString &field_ : target_) + { + if (!field_.isEmpty()) { + fields_ << field_; + } + } + return fields_.join(QStringLiteral(" / ")); + } + static modelRealTerminalData data(const QSharedPointer &real_t) { modelRealTerminalData mrtd; @@ -35,6 +54,8 @@ struct modelRealTerminalData mrtd.Xref_ = real_t->Xref(); mrtd.cable_ = real_t->cable(); mrtd.cable_wire = real_t->cableWire(); + mrtd.cable_target_ = targetText(real_t->cableTarget()); + mrtd.cable_target_auto_ = real_t->cableTargetIsAutomatic(); mrtd.cable_auto_ = real_t->cableIsAutomatic(); mrtd.conductor_ = real_t->conductor(); mrtd.led_ = real_t->isLed(); @@ -61,6 +82,11 @@ struct modelRealTerminalData QString Xref_; QString cable_; QString cable_wire; + //Where the cable lands, past this strip : Installation / Location / Label + QString cable_target_; + //True when cable_target_ comes from the project (read only), + //false when it is empty or was typed in by hand (editable) + bool cable_target_auto_ = false; bool cable_auto_ = false; //True when cable_ and cable_wire come from the cables of the project QString conductor_; bool led_ = false; diff --git a/sources/TerminalStrip/ui/terminalstripeditor.cpp b/sources/TerminalStrip/ui/terminalstripeditor.cpp index c5ea3f760..9d17884e9 100644 --- a/sources/TerminalStrip/ui/terminalstripeditor.cpp +++ b/sources/TerminalStrip/ui/terminalstripeditor.cpp @@ -249,6 +249,10 @@ void TerminalStripEditor::setCurrentStrip(TerminalStrip *strip_) { m_model = new TerminalStripModel{strip_, this}; ui->m_table_widget->setModel(m_model); + //The Target column is worked out from the drawing: it takes + //the width of its own text, never a cut off Installation + ui->m_table_widget->horizontalHeader()->setSectionResizeMode( + TerminalStripModel::CableTarget, QHeaderView::ResizeToContents); m_model->buildBridgePixmap(setUpBridgeCellWidth()); connect(ui->m_table_widget->selectionModel(), &QItemSelectionModel::selectionChanged, this, &TerminalStripEditor::selectionChanged); } @@ -335,6 +339,12 @@ void TerminalStripEditor::apply() if (!data_.cable_auto_) { set_info(RealTerminal::cableInfoKey(), data_.cable_); set_info(RealTerminal::cableWireInfoKey(), data_.cable_wire); + } + //The same for the target: a target worked out from the + //project is never written in the element, only the one + //typed in by hand is stored + if (!data_.cable_target_auto_) { + set_info(RealTerminal::cableTargetInfoKey(), data_.cable_target_); } set_info(RealTerminal::shieldInfoKey(), data_.shield_ ? QStringLiteral("true") : QString()); //The color and the section are stored with the dot as decimal separator, whatever the locale @@ -790,7 +800,8 @@ void TerminalStripEditor::on_m_led_cb_activated(int index) namespace { /** * @return true if the cell at @a index contain a free text - * (label, cable, wire of the cable, color, section or width), so a text can be pasted in it. + * (label, cable, wire of the cable, target of the cable, color, section + * or width), so a text can be pasted in it. */ bool cellAcceptText(const QModelIndex &index) { @@ -798,6 +809,7 @@ bool cellAcceptText(const QModelIndex &index) return column_ == TerminalStripModel::Label || column_ == TerminalStripModel::Cable || column_ == TerminalStripModel::CableWire + || column_ == TerminalStripModel::CableTarget || column_ == TerminalStripModel::Color || column_ == TerminalStripModel::Section || column_ == TerminalStripModel::Width; diff --git a/sources/TerminalStrip/ui/terminalstripmodel.cpp b/sources/TerminalStrip/ui/terminalstripmodel.cpp index 6c4274bae..b15be27a9 100644 --- a/sources/TerminalStrip/ui/terminalstripmodel.cpp +++ b/sources/TerminalStrip/ui/terminalstripmodel.cpp @@ -48,16 +48,17 @@ const int CONDUCTOR_CELL = 7; const int XREF_CELL = 8; const int CABLE_CELL = 9; const int CABLE_WIRE_CELL = 10; -const int TYPE_CELL = 11; -const int FUNCTION_CELL = 12; -const int LED_CELL = 13; -const int SHIELD_CELL = 14; -const int COLOR_CELL = 15; -const int SECTION_CELL = 16; -const int WIDTH_CELL = 17; -const int SEPARATOR_CELL = 18; +const int CABLE_TARGET_CELL = 11; +const int TYPE_CELL = 12; +const int FUNCTION_CELL = 13; +const int LED_CELL = 14; +const int SHIELD_CELL = 15; +const int COLOR_CELL = 16; +const int SECTION_CELL = 17; +const int WIDTH_CELL = 18; +const int SEPARATOR_CELL = 19; -const int COLUMN_COUNT = 19; +const int COLUMN_COUNT = 20; static QVector UNMODIFIED_CELL_VECTOR(COLUMN_COUNT, false); @@ -132,14 +133,15 @@ TerminalStripModel::Column TerminalStripModel::columnTypeForIndex(const QModelIn case 8 : return XRef; case 9 : return Cable; case 10 : return CableWire; - case 11 : return Type; - case 12 : return Function; - case 13 : return Led; - case 14 : return Shield; - case 15 : return Color; - case 16 : return Section; - case 17 : return Width; - case 18 : return Separator; + case 11 : return CableTarget; + case 12 : return Type; + case 13 : return Function; + case 14 : return Led; + case 15 : return Shield; + case 16 : return Color; + case 17 : return Section; + case 18 : return Width; + case 19 : return Separator; default : return Invalid; } } @@ -215,6 +217,7 @@ QVariant TerminalStripModel::data(const QModelIndex &index, int role) const case XREF_CELL : return mrtd.spare_ ? tr("Spare") : mrtd.Xref_; case CABLE_CELL : return mrtd.cable_; case CABLE_WIRE_CELL : return mrtd.cable_wire; + case CABLE_TARGET_CELL : return mrtd.cable_target_; case TYPE_CELL : return ElementData::translatedTerminalType(mrtd.type_); case FUNCTION_CELL : return ElementData::translatedTerminalFunction(mrtd.function_); case COLOR_CELL : return TerminalColor::displayName(mrtd.color_, mrtd.stripe_color_); @@ -235,6 +238,7 @@ QVariant TerminalStripModel::data(const QModelIndex &index, int role) const case LABEL_CELL : return mrtd.label_; case CABLE_CELL : return mrtd.cable_; case CABLE_WIRE_CELL : return mrtd.cable_wire; + case CABLE_TARGET_CELL : return mrtd.cable_target_; case COLOR_CELL : return TerminalColor::toSpec(mrtd.color_, mrtd.stripe_color_); case SECTION_CELL : return mrtd.section_ > 0 ? QLocale().toString(mrtd.section_, 'g', 6) : QString(); case WIDTH_CELL : return mrtd.width_mm_ > 0 ? QLocale().toString(mrtd.width_mm_, 'g', 6) : QString(); @@ -309,6 +313,10 @@ bool TerminalStripModel::setData(const QModelIndex &index, const QVariant &value && mrtd.cable_auto_) { return false; } + if (column_ == CABLE_TARGET_CELL + && mrtd.cable_target_auto_) { + return false; + } if (column_ == LEVEL_CELL && role == Qt::EditRole) @@ -361,6 +369,14 @@ bool TerminalStripModel::setData(const QModelIndex &index, const QVariant &value modified_ = true; modified_cell = CABLE_WIRE_CELL; } + else if (column_ == CABLE_TARGET_CELL && + role == Qt::EditRole && + mrtd.cable_target_ != value.toString()) + { + mrtd.cable_target_ = value.toString(); + modified_ = true; + modified_cell = CABLE_TARGET_CELL; + } else if (column_ == SHIELD_CELL) { mrtd.shield_ = value.toBool(); @@ -484,6 +500,7 @@ QVariant TerminalStripModel::headerData(int section, Qt::Orientation orientation case XREF_CELL: return tr("Cross-reference"); case CABLE_CELL: return tr("Cable"); case CABLE_WIRE_CELL: return tr("Color / wire number of the cable"); + case CABLE_TARGET_CELL: return tr("Target"); case TYPE_CELL: return tr("Type"); case FUNCTION_CELL : return tr("Function"); case LED_CELL: return tr("led"); @@ -496,6 +513,13 @@ QVariant TerminalStripModel::headerData(int section, Qt::Orientation orientation } } } + else if (role == Qt::ToolTipRole && + orientation == Qt::Horizontal && + section == CABLE_TARGET_CELL) + { + //Same three fields as the box drawn under the cable + return tr("Installation, Location and Label (BMK) of the piece the cable lands on, past this strip"); + } return QVariant(); } @@ -510,6 +534,12 @@ Qt::ItemFlags TerminalStripModel::flags(const QModelIndex &index) const if ((c == CABLE_CELL || c == CABLE_WIRE_CELL) && !dataAtRow(index.row()).cable_auto_) flags = flags | Qt::ItemIsEditable; + //The target the project works out is read only: only the drawing + //decides it. No target to show, or one typed in by hand, may be + //written -- the same rule as the cable cells. + if (c == CABLE_TARGET_CELL + && !dataAtRow(index.row()).cable_target_auto_) + flags = flags | Qt::ItemIsEditable; if (c == LABEL_CELL || c == TYPE_CELL || c == FUNCTION_CELL || c == COLOR_CELL || c == SECTION_CELL || c == WIDTH_CELL) flags = flags | Qt::ItemIsEditable; diff --git a/sources/TerminalStrip/ui/terminalstripmodel.h b/sources/TerminalStrip/ui/terminalstripmodel.h index 7a95f227a..ac0fc9e7f 100644 --- a/sources/TerminalStrip/ui/terminalstripmodel.h +++ b/sources/TerminalStrip/ui/terminalstripmodel.h @@ -58,14 +58,15 @@ class TerminalStripModel : public QAbstractTableModel XRef = 8, Cable = 9, CableWire = 10, - Type = 11, - Function = 12, - Led = 13, - Shield = 14, - Color = 15, - Section = 16, - Width = 17, - Separator = 18, + CableTarget = 11, + Type = 12, + Function = 13, + Led = 14, + Shield = 15, + Color = 16, + Section = 17, + Width = 18, + Separator = 19, Invalid = 99 }; diff --git a/tests/qttest/tst_cabletarget.cpp b/tests/qttest/tst_cabletarget.cpp index f34dd92f8..1504b4761 100644 --- a/tests/qttest/tst_cabletarget.cpp +++ b/tests/qttest/tst_cabletarget.cpp @@ -50,7 +50,10 @@ Also here: the box at the end of a cable is written at the font of the drawing -- the end zone the settings leave is what gives it the - room, so three lines never end up squeezed into a tiny text. + room, so three lines never end up squeezed into a tiny text. A + target typed in by hand is written the way the fields are shown in + the column, "Installation / Location / Label", and the box draws one + line per field, with no slash in it. Both drawing styles are covered: the schematic one and the realistic one (the strip seen from above), which paint the same @@ -174,6 +177,7 @@ private slots: void paintsWithTargetBox(); void wireLabelStaysOutOfTheTerminal(); void targetBoxKeepsTheDrawingFont(); + void typedTargetIsDrawnInItsOwnLines(); void realisticStyleDrawsTheCable(); }; @@ -520,6 +524,94 @@ void tst_cabletarget::realisticStyleDrawsTheCable() QVERIFY(without_bounds.height() < with_bounds.height()); } +/** + A target typed in by hand is written the way the fields are shown in + the column, "Installation / Location / Label": the box under the + cable draws one line per field, with no slash in it. The picture of + a strip whose cable ends on three fields is therefore that much + deeper than the picture of the same strip with one single field -- + two lines of the font of the drawing. +*/ +void tst_cabletarget::typedTargetIsDrawnInItsOwnLines() +{ + //The cable lands on a piece which names nothing, so there is no + //target of the drawing and the box takes what is typed in + QVERIFY(wireCableToTheContact()); + Element *contact_ = element( + QStringLiteral("{fc9cc920-5822-421a-916e-a17100acdcf6}")); + QVERIFY(contact_); + ElementData contact_data_{contact_->elementData()}; + contact_data_.m_informations.addValue(QETInformation::ELMT_PLANT, QString()); + contact_data_.m_informations.addValue(QETInformation::ELMT_LOCATION, QString()); + contact_data_.m_informations.addValue(QETInformation::ELMT_LABEL, QString()); + contact_->setElementData(contact_data_); + + const auto a_ = terminalInStrip( + QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + QVERIFY(a_); + QVERIFY(a_->cableTarget().isEmpty()); + Element *terminal_ = a_->element(); + QVERIFY(terminal_); + + auto pattern_ = QSharedPointer{new TerminalStripLayoutPattern}; + const auto strips_ = m_project->terminalStrip(); + QVERIFY(!strips_.isEmpty()); + TerminalStripDrawer::TerminalStripDrawer drawer{ + QSharedPointer{ + new TerminalStripDrawer::TrueTerminalStrip{strips_.first()}}, + pattern_}; + + const QRectF bounds{drawer.boundingRect()}; + QVERIFY(!bounds.isEmpty()); + + const auto render = [&drawer, &bounds]() { + QImage image{bounds.size().toSize() + QSize{4, 4}, + QImage::Format_ARGB32_Premultiplied}; + image.fill(Qt::transparent); + QPainter painter{&image}; + painter.translate(-bounds.topLeft() + QPointF{2, 2}); + drawer.paint(&painter); + painter.end(); + return image; + }; + const auto lowest_row = [](const QImage &image_) { + qreal lowest{-1}; + for (auto y = 0 ; y < image_.height() ; ++y) + { + for (auto x = 0 ; x < image_.width() ; ++x) + { + if (image_.pixel(x, y) != qRgba(0, 0, 0, 0)) { + lowest = y; + break; + } + } + } + return lowest; + }; + const auto type_in = [terminal_](const QString &text_) { + ElementData data_{terminal_->elementData()}; + data_.m_informations.addValue(RealTerminal::cableTargetInfoKey(), text_); + terminal_->setElementData(data_); + }; + + //One field: the box holds one single line + type_in(QStringLiteral("Boiler room")); + const qreal one_line{lowest_row(render())}; + QVERIFY(one_line >= 0); + + //Three fields: the box is two lines deeper, at the font of the + //drawing -- the slashes are only there to separate the fields in + //the column, the box never draws them + type_in(QStringLiteral("Boiler room / Cabinet 3 / -K3")); + QCOMPARE(a_->cableTarget(), (QVector{QStringLiteral("Boiler room"), + QStringLiteral("Cabinet 3"), + QStringLiteral("-K3")})); + const qreal three_lines{lowest_row(render())}; + const QFontMetricsF metrics{pattern_->font()}; + QVERIFY2(three_lines - one_line > 1.5 * metrics.height(), + "three fields must give a box two lines deeper than one field"); +} + int main(int argc, char **argv) { QStandardPaths::setTestModeEnabled(true); diff --git a/tests/qttest/tst_terminalcableauto.cpp b/tests/qttest/tst_terminalcableauto.cpp index 802068b28..56c764590 100644 --- a/tests/qttest/tst_terminalcableauto.cpp +++ b/tests/qttest/tst_terminalcableauto.cpp @@ -17,6 +17,7 @@ along with QElectroTech. If not, see . */ #include "qetapptest.h" +#include "TerminalStrip/physicalterminal.h" #include "TerminalStrip/realterminal.h" #include "TerminalStrip/terminalstrip.h" #include "TerminalStrip/ui/terminalstripeditor.h" @@ -25,6 +26,7 @@ #include "qetgraphicsitem/conductor.h" #include "qetgraphicsitem/element.h" #include "qetgraphicsitem/terminal.h" +#include "qetinformation.h" #include "qetproject.h" #include @@ -37,6 +39,17 @@ cannot leave a stale value behind. A terminal no cable is wired to keeps the value typed in by hand, cell and all. + The Target column follows the same rule: it names where the cable + lands past this strip (Installation, Location and Label), the very + three fields the strip drawing writes in its box. Worked out from a + cable it is read only -- only the drawing decides it -- and with no + cable landing anywhere it is there to be typed in, just like the + Cable cell, the text then stored in the element and kept until a + cable works a target out again. A text typed in is written the way + the fields are shown in the column, "Installation / Location / + Label", so the box draws one line per field and not one long line + needing the slashes. + fixtures/every_feature.qet has the strip X1 whose two terminals are wired to conductors on the folio, and no cable at all. */ @@ -82,6 +95,37 @@ class tst_terminalcableauto : public QObject return conductors.isEmpty() ? nullptr : conductors.first(); } + // The wire which leaves the strip: one of its ends belongs to a piece + // outside the strip, while the wire joining two terminals of this very + // strip never leaves it -- and never has a target to name. + static Conductor *outgoingConductor(const QSharedPointer &real_t) + { + TerminalStrip *strip = real_t ? real_t->parentStrip() : nullptr; + if (!strip) return nullptr; + + QSet own; + for (const QSharedPointer &physical + : strip->physicalTerminal()) + { + if (!physical) continue; + for (const QSharedPointer &real : physical->realTerminals()) + if (real && real->element()) + own.insert(real->element()->uuid()); + } + + for (Conductor *conductor : conductorsOf(real_t)) + { + if (!conductor) continue; + Element *ends[2] = { + conductor->terminal1 ? conductor->terminal1->parentElement() : nullptr, + conductor->terminal2 ? conductor->terminal2->parentElement() : nullptr }; + for (Element *end : ends) + if (end && !own.contains(end->uuid())) + return conductor; + } + return nullptr; + } + // The cable the create dialog hands over: owned by the project, four // cores, one core wired to the terminal's conductor (or to @p on). static Cable *wireCable(QETProject &project, @@ -264,6 +308,224 @@ private slots: QCOMPARE(real_t->cableWire(), QStringLiteral("bl, bl")); QVERIFY(real_t->cableIsAutomatic()); } + + // The Target column names where the cable lands, past this strip: the + // three fields the strip drawing writes in the box under the cable, + // joined in one line. Worked out from a cable it is read only, there + // is nothing to name without one, and the cell empties again once the + // cable goes. + void targetColumnNamesWhereTheCableLands() + { + const QString fixture = QFINDTESTDATA("fixtures/every_feature.qet"); + QETProject project(fixture); + QCOMPARE(project.state(), QETProject::Ok); + + const auto real_t = wiredTerminal(project); + QVERIFY(!real_t.isNull()); + Conductor *outgoing = outgoingConductor(real_t); + QVERIFY2(outgoing, "the fixture terminal has a wire leaving the strip"); + + Element *target = (outgoing->terminal1 + && outgoing->terminal1->parentElement() == real_t->element()) + ? (outgoing->terminal2 ? outgoing->terminal2->parentElement() : nullptr) + : (outgoing->terminal1 ? outgoing->terminal1->parentElement() : nullptr); + QVERIFY(target); + + //The piece the cable lands on carries an Installation, a Location + //and a Label, the way an element of a real drawing does + ElementData named = target->elementData(); + named.m_informations.addValue(QETInformation::ELMT_PLANT, + QStringLiteral("Plant 7")); + named.m_informations.addValue(QETInformation::ELMT_LOCATION, + QStringLiteral("Cabinet 7")); + named.m_informations.addValue(QETInformation::ELMT_LABEL, + QStringLiteral("-S7")); + target->setElementData(named); + + //No cable yet : nothing to name, so the cell is there to be + //filled in by hand -- the very rule of the Cable cell + TerminalStripModel model_before(project.terminalStrip().first()); + const QModelIndex before_cell = model_before.index( + 0, TerminalStripModel::CableTarget); + QVERIFY(model_before.data(before_cell).toString().isEmpty()); + QVERIFY(model_before.flags(before_cell) & Qt::ItemIsEditable); + + Cable *cable = wireCable(project, real_t, 1, QStringLiteral("15W1"), + outgoing); + + //Row zero is the terminal the cable is wired to + TerminalStripModel model(project.terminalStrip().first()); + QCOMPARE(model.data(model.index(0, TerminalStripModel::Cable)) + .toString(), QStringLiteral("15W1")); + + //The three fields, in the order the box draws them + const QModelIndex target_cell = model.index(0, + TerminalStripModel::CableTarget); + QCOMPARE(model.data(target_cell).toString(), + QStringLiteral("Plant 7 / Cabinet 7 / -S7")); + QVERIFY(!(model.flags(target_cell) & Qt::ItemIsEditable)); + QCOMPARE(model.headerData(TerminalStripModel::CableTarget, + Qt::Horizontal).toString(), + QStringLiteral("Target")); + + //The cable taken away : the cell is empty again, nothing stale + //behind, and it takes an edit once more + QVERIFY(cable); + cable->removeCore(1); + TerminalStripModel reloaded(project.terminalStrip().first()); + const QModelIndex reloaded_cell = reloaded.index( + 0, TerminalStripModel::CableTarget); + QVERIFY(reloaded.data(reloaded_cell).toString().isEmpty()); + QVERIFY(reloaded.flags(reloaded_cell) & Qt::ItemIsEditable); + } + + // A target the drawing cannot work out is typed in by hand, exactly + // like a cable designation: the cell takes the edit, Apply stores the + // text in the element, a fresh model shows it again. A cable landing + // on a named piece then gives the read only target of the drawing and + // the text typed in by hand waits, untouched, until that cable goes. + void targetTypedByHandIsStoredAndKept() + { + const QString fixture = QFINDTESTDATA("fixtures/every_feature.qet"); + QETProject project(fixture); + QCOMPARE(project.state(), QETProject::Ok); + + const auto real_t = wiredTerminal(project); + QVERIFY(!real_t.isNull()); + Element *element = real_t->element(); + QVERIFY(element); + Conductor *outgoing = outgoingConductor(real_t); + QVERIFY2(outgoing, "the fixture terminal has a wire leaving the strip"); + + //The editor takes a target typed in by hand : no cable is wired, + //so there is nothing the drawing could name and the cell is open + TerminalStripEditor editor(&project); + editor.setCurrentStrip(project.terminalStrip().first()); + QAbstractItemView *table = editor.findChild( + QStringLiteral("m_table_widget")); + QVERIFY2(table && table->model(), "the editor has its table"); + QAbstractItemModel *model = table->model(); + const QModelIndex cell = model->index(0, TerminalStripModel::CableTarget); + QVERIFY(model->data(cell).toString().isEmpty()); + QVERIFY(model->flags(cell) & Qt::ItemIsEditable); + + const QString typed{QStringLiteral("Boiler room / Cabinet 3 / -K3")}; + QVERIFY(model->setData(cell, typed, Qt::EditRole)); + QCOMPARE(model->data(cell, Qt::EditRole).toString(), typed); + QCOMPARE(model->data(cell).toString(), typed); + + //Apply writes it in the element, and nowhere else + editor.apply(); + QCOMPARE(element->elementData().m_informations + .value(RealTerminal::cableTargetInfoKey()).toString(), typed); + //Three fields, written the way the column shows them: the box + //draws three lines, without the slashes + QCOMPARE(real_t->cableTarget(), + (QVector{QStringLiteral("Boiler room"), + QStringLiteral("Cabinet 3"), + QStringLiteral("-K3")})); + QVERIFY(!real_t->cableTargetIsAutomatic()); + + //A fresh model shows the text and still lets it be edited + TerminalStripModel model_after(project.terminalStrip().first()); + const QModelIndex after_cell = model_after.index( + 0, TerminalStripModel::CableTarget); + QCOMPARE(model_after.data(after_cell).toString(), typed); + QVERIFY(model_after.flags(after_cell) & Qt::ItemIsEditable); + + //The piece the cable lands on is named now : once the cable is + //wired the drawing gives the target of its own, and that one is + //read only while the text typed in by hand stays in the element + Element *target = (outgoing->terminal1 + && outgoing->terminal1->parentElement() == real_t->element()) + ? (outgoing->terminal2 ? outgoing->terminal2->parentElement() : nullptr) + : (outgoing->terminal1 ? outgoing->terminal1->parentElement() : nullptr); + QVERIFY(target); + ElementData named = target->elementData(); + named.m_informations.addValue(QETInformation::ELMT_PLANT, + QStringLiteral("Plant 9")); + named.m_informations.addValue(QETInformation::ELMT_LOCATION, + QStringLiteral("Cabinet 9")); + named.m_informations.addValue(QETInformation::ELMT_LABEL, + QStringLiteral("-S9")); + target->setElementData(named); + + Cable *cable = wireCable(project, real_t, 1, QStringLiteral("15W1"), + outgoing); + QVERIFY(cable); + QVERIFY(real_t->cableTargetIsAutomatic()); + QCOMPARE(real_t->cableTarget(), + (QVector{QStringLiteral("Plant 9"), + QStringLiteral("Cabinet 9"), + QStringLiteral("-S9")})); + QCOMPARE(element->elementData().m_informations + .value(RealTerminal::cableTargetInfoKey()).toString(), typed); + + TerminalStripModel model_cable(project.terminalStrip().first()); + const QModelIndex cable_cell = model_cable.index( + 0, TerminalStripModel::CableTarget); + QCOMPARE(model_cable.data(cable_cell).toString(), + QStringLiteral("Plant 9 / Cabinet 9 / -S9")); + QVERIFY(!(model_cable.flags(cable_cell) & Qt::ItemIsEditable)); + //And the automatic target was never written in the element + QCOMPARE(element->elementData().m_informations + .value(RealTerminal::cableTargetInfoKey()).toString(), typed); + + //The cable taken away : the text typed in by hand is back, cell + //and all, no stale Plant 9 anywhere + cable->removeCore(1); + QVERIFY(!real_t->cableTargetIsAutomatic()); + QCOMPARE(real_t->cableTarget(), + (QVector{QStringLiteral("Boiler room"), + QStringLiteral("Cabinet 3"), + QStringLiteral("-K3")})); + TerminalStripModel model_back(project.terminalStrip().first()); + const QModelIndex back_cell = model_back.index( + 0, TerminalStripModel::CableTarget); + QCOMPARE(model_back.data(back_cell).toString(), typed); + QVERIFY(model_back.flags(back_cell) & Qt::ItemIsEditable); + } + + // A text typed in by hand is written the way the fields are shown in + // the column, "Installation / Location / Label": the box under the + // cable draws one line per field, without the slashes, exactly like a + // target worked out from the project. + void typedTargetGivesOneLinePerField() + { + const QString fixture = QFINDTESTDATA("fixtures/every_feature.qet"); + QETProject project(fixture); + QCOMPARE(project.state(), QETProject::Ok); + + const auto real_t = wiredTerminal(project); + QVERIFY(!real_t.isNull()); + Element *element = real_t->element(); + QVERIFY(element); + + //One field only: one single line in the box + ElementData typed = element->elementData(); + typed.m_informations.addValue(RealTerminal::cableTargetInfoKey(), + QStringLiteral("Boiler room")); + element->setElementData(typed); + QCOMPARE(real_t->cableTarget(), + QVector{QStringLiteral("Boiler room")}); + + //Three fields: three lines, and no slash anywhere in them + typed = element->elementData(); + typed.m_informations.addValue(RealTerminal::cableTargetInfoKey(), + QStringLiteral("Plant 9 / Cabinet 9 / -S9")); + element->setElementData(typed); + QCOMPARE(real_t->cableTarget(), + (QVector{QStringLiteral("Plant 9"), + QStringLiteral("Cabinet 9"), + QStringLiteral("-S9")})); + //A typed text is never a target of the drawing + QVERIFY(!real_t->cableTargetIsAutomatic()); + + //The column shows the text back the way it was written + TerminalStripModel model(project.terminalStrip().first()); + QCOMPARE(model.data(model.index(0, TerminalStripModel::CableTarget)) + .toString(), QStringLiteral("Plant 9 / Cabinet 9 / -S9")); + } }; QET_APP_TEST_MAIN(tst_terminalcableauto) From 5baf5bec603ecff510160414e8356651bcc2f3dd Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Tue, 20 Oct 2026 16:22:15 +0200 Subject: [PATCH 10/12] Set a target box lower when another one is in its way Two cables of one strip end close to each other, and their boxes are wider than the room between the two cables: a long Installation is enough for that, and the two boxes ended up written the one over the other -- a picture nobody can read, and no way to tell which box belongs to which cable. The boxes of a strip are now all measured before any of them is drawn, and placed from the left to the right: a box which would reach into one already placed is set just low enough to stand clear of it, its stub drawn longer. Nothing is cut down to fit -- the text keeps its font and the box keeps its width, it is the box which moves. The strip reserves the room for the lower box too, with the same rule and the very cells the drawing works on (Helpers::cableBoxesExtraDepth (), both styles now build one list of cells for the painting and for the reserved depth), so a lower box is never cut off by the drawing area. tst_cabletarget paints a strip with one cable ending on a target and the same strip with a second cable ending on one: the second picture draws a whole box lower, and the strip is deeper by exactly the room that lower box needs. --- .../realisticterminalstripdrawer.cpp | 84 +++-- .../GraphicsItem/terminalstripdrawer.cpp | 329 +++++++++++++++--- .../GraphicsItem/terminalstripdrawerhelpers.h | 7 + tests/qttest/tst_cabletarget.cpp | 105 +++++- 4 files changed, 453 insertions(+), 72 deletions(-) diff --git a/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.cpp b/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.cpp index b1ee952e6..26e68688c 100644 --- a/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.cpp +++ b/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.cpp @@ -542,6 +542,57 @@ bool haveCable(const AbstractTerminalStripInterface *strip) return false; } + /** + * @brief cableCells + * The cells the cables are drawn from : one per terminal of the strip + * carrying a cable, spread over the width of its body, all of them + * leaving the strip at its bottom edge. The paint and the depth the + * strip reserves under it both work on this list, so a box set lower + * than the others is never cut off by the drawing area. + */ + QVector cableCells(const AbstractTerminalStripInterface &strip, + const TerminalStripLayoutPattern &pattern) + { + const qreal s_{pattern.m_realistic_px_per_mm}; + const qreal length_{pattern.m_realistic_length_mm * s_}; + + //Left end stop and strip name : the terminals start after them + qreal x_{endStopWidthMm(pattern) * s_}; + if (pattern.m_realistic_group_marker) { + x_ += group_marker_width_mm * s_; + } + + QVector cells; + for (const auto &g_ : geometry(&strip)) + { + const qreal w_{g_.width_mm * s_}; + const int levels_{static_cast(g_.reals.size())}; + + //One wire exit by level, spread over the width of the body + for (int k = 0 ; k < levels_ ; ++k) + { + const auto &real_t{g_.reals.at(k)}; + if (!real_t || real_t->cable().isEmpty()) { + continue; + } + Helpers::CableCell cell_; + cell_.x = x_ + w_ * (k + 0.5) / levels_; + cell_.bottom = length_; + cell_.cable = real_t->cable(); + cell_.wire = real_t->cableWire(); + cell_.shield = real_t->isShield(); + cell_.terminal = real_t; + cells.append(cell_); + } + + x_ += w_; + if (g_.separator_after) { + x_ += separator_width_mm * s_; + } + } + return cells; + } + } //End anonymous namespace QRectF boundingRect(const AbstractTerminalStripInterface *strip, @@ -562,11 +613,16 @@ QRectF boundingRect(const AbstractTerminalStripInterface *strip, qreal extra_bottom_{0}; if (pattern.m_cable_wire_length > 0 && haveCable(strip)) { - //4 is the diameter of the circle at the end of the wires + //4 is the diameter of the circle at the end of the wires. + //A box standing in the way of another one is drawn lower than + //the end zone, and the room for it is reserved too (same rule, + //same cells as the drawing) or it is cut off extra_bottom_ = pattern.m_cable_wire_length + std::max(0, pattern.m_cable_length) + std::max(0, pattern.m_cable_end_length) - + 4; + + 4 + + Helpers::cableBoxesExtraDepth(cableCells(*strip, pattern), + pattern); } return QRectF{0, 0, width_mm_ * s_, length_ + extra_bottom_}; } @@ -628,7 +684,11 @@ void paint(QPainter *painter, } QHash> bridges_; - QVector cable_cells_; + //The cells the cables are drawn from, worked out once for the whole + //strip: the depth the strip reserves under it is built on the same + //list (boundingRect()), so a box set lower than another one is + //never cut off + QVector cable_cells_{cableCells(strip, pattern)}; for (const auto &g_ : geometry(&strip)) { @@ -637,24 +697,6 @@ void paint(QPainter *painter, const auto first_{g_.reals.first()}; const int levels_{static_cast(g_.reals.size())}; - //Cables (hoses) of this terminal : one wire exit by level, spread over - //the width of the body, all of them leaving the strip at its bottom edge - for (int k = 0 ; k < levels_ ; ++k) - { - const auto &real_t{g_.reals.at(k)}; - if (!real_t || real_t->cable().isEmpty()) { - continue; - } - Helpers::CableCell cell_; - cell_.x = x_ + w_ * (k + 0.5) / levels_; - cell_.bottom = length_; - cell_.cable = real_t->cable(); - cell_.wire = real_t->cableWire(); - cell_.shield = real_t->isShield(); - cell_.terminal = real_t; - cable_cells_.append(cell_); - } - //A terminal chosen in the catalog is drawn with its own clamp, so that a strip //mixing screw and spring terminals is drawn as it is. The spring-cage clamps //(opened with a screwdriver) are drawn like the push-in ones. diff --git a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp index 52cd984e0..06877d9bb 100644 --- a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp +++ b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp @@ -263,6 +263,168 @@ void drawConnections(QPainter *painter, const QRectF &terminal_rect, qreal lengt } } +/** + One box at the end of a cable : which terminals the cable has and + where the box goes. The boxes of a strip are all measured before any + of them is drawn, so a box reaching into another one can be set + lower than it instead of being written over it. +*/ +struct TargetBox +{ + int first{-1}; /// lines; /// other.cable_x - other.width / 2 + && top < other.bottom() && bottom() > other.top; + } +}; + +/** + * @brief boxFont + * The font of a box naming where a cable goes : the font of the drawing, + * shrunk only while the lines of the target do not fit the height left + * for the box. The width never shrinks anything -- a line too long for + * the cross bar gives a wider box, it never gives a smaller text. + * @param room : the height left for the box + * @return the font the lines are written with + */ +QFont boxFont(const QFont &font, const QVector &lines, qreal room) +{ + QFont box_font{font}; + if (box_font.pixelSize() <= 0 && box_font.pointSizeF() > 0) { + box_font.setPixelSize(qMax(4, qRound(box_font.pointSizeF() * 96.0 / 72.0))); + } + const auto fits_the_height = [&lines, room](const QFont &font_) { + return QFontMetricsF{font_}.height() * lines.size() + 4 <= room; + }; + while (!fits_the_height(box_font) && box_font.pixelSize() > 4) { + box_font.setPixelSize(box_font.pixelSize() - 1); + } + return box_font; +} + +/** + * @brief targetBoxes + * Where the boxes naming where the cables go stand, and how big they + * are. Every box is measured first -- the lines of the target at the + * font of the drawing, a width of the widest line or of the cross bar + * the cable ends on, whichever is wider -- and they are then placed + * from the left to the right : a box which would reach into one + * already placed is set just low enough to stand clear of it, its stub + * drawn longer. Two cables of one strip therefore never write their + * boxes the one over the other, and no text is ever cut down to fit. + * @return one entry per cable which has something to name at its end + */ +QVector targetBoxes(const QVector &cells, + const TerminalStripLayoutPattern &pattern, + const QFont &font) +{ + QVector boxes; + const qreal wire_length{pattern.m_cable_wire_length}; + if (wire_length <= 0) { + return boxes; + } + const qreal cable_length{qMax(0, pattern.m_cable_length)}; + const qreal end_length{qMax(0, pattern.m_cable_end_length)}; + const qreal stub{qMin(6, end_length)}; + if (end_length - stub <= 4) { + return boxes; + } + constexpr qreal box_gap{2}; + + auto first{0}; + while (first < cells.size()) + { + if (cells.at(first).cable.isEmpty()) { + ++first; + continue; + } + + //Find the last terminal of the cable, the same groups the + //drawing makes + auto last{first}; + while (last + 1 < cells.size() && + cells.at(last + 1).cable == cells.at(first).cable) { + ++last; + } + + QVector wires, shields; + qreal max_bottom{cells.at(first).bottom}; + for (auto i = first ; i <= last ; ++i) + { + max_bottom = qMax(max_bottom, cells.at(i).bottom); + if (cells.at(i).shield) { + shields << i; + } else { + wires << i; + } + } + //A cable with only a shield is drawn like a cable with only one wire + if (wires.isEmpty()) { + wires = shields; + } + + const auto &first_wire{cells.at(wires.first())}; + TargetBox box; + box.first = first; + box.bar_2_y = max_bottom + wire_length + cable_length; + box.min_x = first_wire.x; + box.max_x = cells.at(wires.last()).x; + box.cable_x = (box.min_x + box.max_x) / 2; + box.lines = first_wire.terminal ? first_wire.terminal->cableTarget() + : QVector(); + + if (!box.lines.isEmpty()) + { + const QFontMetricsF metrics{boxFont(font, box.lines, end_length - stub)}; + box.width = qMax(box.max_x - box.min_x, 16); + for (const QString &line_ : std::as_const(box.lines)) { + box.width = qMax(box.width, metrics.horizontalAdvance(line_) + 6); + } + box.height = metrics.height() * box.lines.size() + 4; + box.top = box.bar_2_y + stub; + boxes << box; + } + + first = last + 1; + } + + //Place them : a box reaching into one already placed goes just + //under it, and the boxes set aside settle down again with it + for (auto i = 0 ; i < boxes.size() ; ++i) + { + bool settled{false}; + for (auto guard = 0 ; !settled && guard < boxes.size() + 2 ; ++guard) + { + settled = true; + for (auto j = 0 ; j < i ; ++j) + { + if (boxes.at(i).reachesInto(boxes.at(j)) + && boxes.at(i).top < boxes.at(j).bottom() + box_gap) + { + boxes[i].top = boxes.at(j).bottom() + box_gap; + settled = false; + } + } + } + } + return boxes; +} + /** * @brief drawCables * Draw the cables under the terminals. Consecutive terminals who have the same cable name @@ -333,6 +495,15 @@ void drawCables(QPainter *painter, const QVector &cells, const Termin painter->restore(); }; + //Where every box stands, worked out for the whole strip before + //any of them is drawn: a box reaching into another one is set + //lower than it, never written over it + QHash box_of; + const auto boxes{targetBoxes(cells, pattern, painter->font())}; + for (const TargetBox &box_ : boxes) { + box_of.insert(box_.first, box_); + } + auto first{0}; while (first < cells.size()) { @@ -387,52 +558,42 @@ void drawCables(QPainter *painter, const QVector &cells, const Termin //Location and Label (BMK) of the piece it lands on, the same //three fields the cable list shows. The box spans the cross bar //and never shrinks its text down to fit: when its lines are - //longer than the cross bar, the box is the one which grows. + //longer than the cross bar, the box is the one which grows. A + //box standing in the way of another one is set lower instead, + //its stub drawn longer, so no box is ever written over another. painter->drawLine(QPointF{min_x, bar_2_y}, QPointF{max_x, bar_2_y}); const qreal stub{qMin(6, end_length)}; - painter->drawLine(QPointF{cable_x, bar_2_y}, QPointF{cable_x, bar_2_y + stub}); - - const auto target_lines{wires.first().terminal ? wires.first().terminal->cableTarget() - : QVector()}; - if (!target_lines.isEmpty() && end_length - stub > 4) + const TargetBox box{box_of.value(first)}; + if (box.lines.isEmpty()) { + painter->drawLine(QPointF{cable_x, bar_2_y}, + QPointF{cable_x, bar_2_y + stub}); + } + else + { + //The stub goes down to the box, which stands lower than the + //end zone when another box of this strip was in its way + painter->drawLine(QPointF{cable_x, bar_2_y}, + QPointF{cable_x, box.top}); + //The font of the drawing, shrunk only while the lines do not //fit the height the settings leave for the box - const qreal room{end_length - stub}; - QFont box_font{painter->font()}; - if (box_font.pixelSize() <= 0 && box_font.pointSizeF() > 0) { - box_font.setPixelSize(qMax(4, qRound(box_font.pointSizeF() * 96.0 / 72.0))); - } - const auto fits_the_height = [&target_lines, room](const QFont &font_) { - return QFontMetricsF{font_}.height() * target_lines.size() + 4 <= room; - }; - while (!fits_the_height(box_font) && box_font.pixelSize() > 4) { - box_font.setPixelSize(box_font.pixelSize() - 1); - } - - //As wide as the cross bar the cable ends on -- the strip of - //one single terminal has no width to take, so a small floor - //keeps the box drawn at all -- and wider still when the lines - //of the target are longer than that + const QFont box_font{boxFont(painter->font(), box.lines, + end_length - stub)}; const QFontMetricsF box_metrics{box_font}; - qreal box_width{qMax(max_x - min_x, 16)}; - for (const QString &line_ : target_lines) { - box_width = qMax(box_width, box_metrics.horizontalAdvance(line_) + 6); - } - - const qreal box_height{box_metrics.height() * target_lines.size() + 4}; - const QRectF box{cable_x - box_width / 2, bar_2_y + stub, box_width, box_height}; + const QRectF box_rect{cable_x - box.width / 2, box.top, + box.width, box.height}; painter->save(); painter->setFont(box_font); - painter->drawRect(box); - for (auto i = 0 ; i < target_lines.size() ; ++i) + painter->drawRect(box_rect); + for (auto i = 0 ; i < box.lines.size() ; ++i) { - painter->drawText(QRectF{box.left() + 3, - box.top() + 2 + i * box_metrics.height(), - box.width() - 6, box_metrics.height()}, + painter->drawText(QRectF{box_rect.left() + 3, + box_rect.top() + 2 + i * box_metrics.height(), + box_rect.width() - 6, box_metrics.height()}, Qt::AlignCenter, - target_lines.at(i)); + box.lines.at(i)); } painter->restore(); } @@ -505,6 +666,53 @@ QRectF terminalRect(const TerminalStripLayoutPattern &pattern, return rect_; } +/** + * @brief cableCells + * The cells the cables are drawn from : one per real terminal of the + * strip, in the order they stand. The paint loop and the depth the + * strip reserves under its terminals both work on this list, so a box + * set lower than the others is never cut off by the drawing area. + * @param strip + * @param pattern + * @return the cells to hand over to Helpers::drawCables() + */ +QVector cableCells(const AbstractTerminalStripInterface &strip, + const TerminalStripLayoutPattern &pattern) +{ + QVector cells; + qreal x_offset{pattern.m_header_rect.width() + pattern.m_spacer_rect.width()}; + for (const auto &physical_t : strip.physicalTerminal()) + { + const QVector> real_terminal_vector{physical_t->realTerminals()}; + const auto real_t_count{real_terminal_vector.size()}; + const auto offset_{4 - real_t_count}; + + for (auto i=0 ; i= 4) { + break; + } + + const auto terminal_rect{terminalRect(pattern, index_, + real_terminal_vector[i])}; + CableCell cell; + cell.x = x_offset + terminal_rect.width()/2; + cell.bottom = terminal_rect.y() + terminal_rect.height(); + if (real_terminal_vector[i]) + { + cell.cable = real_terminal_vector[i]->cable(); + cell.wire = real_terminal_vector[i]->cableWire(); + cell.shield = real_terminal_vector[i]->isShield(); + cell.terminal = real_terminal_vector[i]; + } + cells.append(cell); + x_offset += terminal_rect.width(); + } + } + return cells; +} + /** * @brief The TerminalColors struct * Colors used to fill the rectangle of a terminal. @@ -936,6 +1144,22 @@ void Helpers::drawCables(QPainter *painter, const QVector &cells, ::TerminalStripDrawer::drawCables(painter, cells, pattern); } +qreal Helpers::cableBoxesExtraDepth(const QVector &cells, + const TerminalStripLayoutPattern &pattern) +{ + const qreal end_length{qMax(0, pattern.m_cable_end_length)}; + qreal extra{0}; + for (const auto &box_ : ::TerminalStripDrawer::targetBoxes(cells, pattern, + pattern.font())) + { + //How much deeper than the end zone the strip reserves this box + //stands -- the zone is the room of the box, a box set lower + //than the others needs room the strip must reserve too + extra = qMax(extra, box_.bottom() - (box_.bar_2_y + end_length)); + } + return qMax(0, extra); +} + /** * @brief TerminalStripDrawer::paint * @param painter @@ -1041,7 +1265,12 @@ void TerminalStripDrawer::paint(QPainter *painter) QRectF xref_rect; QHash> bridges_anchor_points; - QVector cable_cells; + //The cells the cables are drawn from, worked out once for the + //whole strip: the depth the strip reserves under its terminals + //is built on the same list (boundingRect()), so a box set lower + //than the others is never cut off + QVector cable_cells{cableCells(*m_strip, *m_pattern)}; + auto cell_index{0}; m_hovered_xref = hoverTerminal{}; @@ -1099,19 +1328,13 @@ void TerminalStripDrawer::paint(QPainter *painter) terminal_rect = terminalRect(*m_pattern, index_, real_terminal_vector[i]); - //Cable (hose) of this terminal - CableCell cable_cell; - if (real_terminal_vector[i]) - { - cable_cell.x = x_offset + terminal_rect.width()/2; - cable_cell.bottom = terminal_rect.y() + terminal_rect.height(); - cable_cell.cable = real_terminal_vector[i]->cable(); - cable_cell.wire = real_terminal_vector[i]->cableWire(); - cable_cell.shield = real_terminal_vector[i]->isShield(); - cable_cell.terminal = real_terminal_vector[i]; - } + //Cable (hose) of this terminal, and where this + //terminal stands: the very cell the depth of the strip + //was worked out from, so the drawing and the room it + //is given can never disagree + const CableCell &cable_cell{cable_cells.at(cell_index++)}; + x_offset = cable_cell.x - terminal_rect.width()/2; const bool have_cable{m_pattern->m_cable_wire_length > 0 && !cable_cell.cable.isEmpty()}; - cable_cells.append(cable_cell); //Draw terminal rect, filled with the color of the terminal (if any) const auto colors_{terminalColors(real_terminal_vector[i])}; const auto fill_color_{colors_.fill}; @@ -1412,12 +1635,18 @@ QRectF TerminalStripDrawer::boundingRect() const if (have_cable) { - //4 is the diameter of the circle at the end of the wires + //4 is the diameter of the circle at the end of the wires. + //A box standing in the way of another one is drawn lower + //than the end zone, and the room for it is reserved too + //(same rule, same cells as the drawing) or it is cut off extra_bottom = std::max(extra_bottom, m_pattern->m_cable_wire_length + std::max(0, m_pattern->m_cable_length) + std::max(0, m_pattern->m_cable_end_length) - + 4); + + 4 + + Helpers::cableBoxesExtraDepth( + cableCells(*m_strip, *m_pattern), + *m_pattern)); } } diff --git a/sources/TerminalStrip/GraphicsItem/terminalstripdrawerhelpers.h b/sources/TerminalStrip/GraphicsItem/terminalstripdrawerhelpers.h index 494262358..d39bc76b6 100644 --- a/sources/TerminalStrip/GraphicsItem/terminalstripdrawerhelpers.h +++ b/sources/TerminalStrip/GraphicsItem/terminalstripdrawerhelpers.h @@ -58,6 +58,13 @@ namespace TerminalStripDrawer //in terminalstripdrawer.cpp for the whole drawing) void drawCables(QPainter *painter, const QVector &cells, const TerminalStripLayoutPattern &pattern); + //How much deeper than the end zone (m_cable_end_length) the boxes of + //the cables need: a box reaching into another one is set lower than + //the others, and the strip must reserve the room for it or the lower + //box is cut off. The same rule the drawing uses, worked out of the + //same cells, so the two can never disagree. + qreal cableBoxesExtraDepth(const QVector &cells, + const TerminalStripLayoutPattern &pattern); } } diff --git a/tests/qttest/tst_cabletarget.cpp b/tests/qttest/tst_cabletarget.cpp index 1504b4761..36095fd31 100644 --- a/tests/qttest/tst_cabletarget.cpp +++ b/tests/qttest/tst_cabletarget.cpp @@ -53,7 +53,10 @@ room, so three lines never end up squeezed into a tiny text. A target typed in by hand is written the way the fields are shown in the column, "Installation / Location / Label", and the box draws one - line per field, with no slash in it. + line per field, with no slash in it. Two cables of one strip end + close to each other, and their boxes are wider than the room between + them: the box standing in the way of the other is set lower, its + stub drawn longer, and the strip reserves the room for it too. Both drawing styles are covered: the schematic one and the realistic one (the strip seen from above), which paint the same @@ -178,6 +181,7 @@ private slots: void wireLabelStaysOutOfTheTerminal(); void targetBoxKeepsTheDrawingFont(); void typedTargetIsDrawnInItsOwnLines(); + void boxesOfTwoCablesAreSetApart(); void realisticStyleDrawsTheCable(); }; @@ -612,6 +616,105 @@ void tst_cabletarget::typedTargetIsDrawnInItsOwnLines() "three fields must give a box two lines deeper than one field"); } +/** + Two cables of one strip end close to each other, and their boxes are + wider than the room between the two cables: the box standing in the + way of the other is set lower than it, its stub drawn longer -- the + two boxes are never written the one over the other, and the text is + never cut down to fit. The strip reserves the room for the lower box + too, or the drawing would be cut off there. +*/ +void tst_cabletarget::boxesOfTwoCablesAreSetApart() +{ + QVERIFY(wireCableToTheContact()); + + //The second cable of the strip : its wires only join the strip's + //own terminals, so it names nothing and takes what is typed in + Cable *cable_ = m_project->newCable(); + QVERIFY(cable_); + Conductor *inside_ = conductor( + QStringLiteral("{584b452e-a885-491a-98f1-fd348610b5e0}")); + QVERIFY(inside_); + claimCable(inside_, cable_->uuid()); + + const auto b_ = terminalInStrip( + QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}")); + QVERIFY(b_); + Element *second_ = b_->element(); + QVERIFY(second_); + + auto pattern_ = QSharedPointer{new TerminalStripLayoutPattern}; + const auto strips_ = m_project->terminalStrip(); + QVERIFY(!strips_.isEmpty()); + TerminalStripDrawer::TerminalStripDrawer drawer{ + QSharedPointer{ + new TerminalStripDrawer::TrueTerminalStrip{strips_.first()}}, + pattern_}; + + const auto render = [&drawer](const QRectF &bounds) { + QImage image{bounds.size().toSize() + QSize{4, 4}, + QImage::Format_ARGB32_Premultiplied}; + image.fill(Qt::transparent); + QPainter painter{&image}; + painter.translate(-bounds.topLeft() + QPointF{2, 2}); + drawer.paint(&painter); + painter.end(); + return image; + }; + const auto lowest_row = [](const QImage &image_) { + qreal lowest{-1}; + for (auto y = 0 ; y < image_.height() ; ++y) + { + for (auto x = 0 ; x < image_.width() ; ++x) + { + if (image_.pixel(x, y) != qRgba(0, 0, 0, 0)) { + lowest = y; + break; + } + } + } + return lowest; + }; + + //One cable with a target : one box, in the end zone + const QRectF one_bounds{drawer.boundingRect()}; + const qreal one_row{lowest_row(render(one_bounds))}; + QVERIFY(one_row >= 0); + + //The second cable gets a target of its own : its box is wider + //than the distance between the two cables and would be written + //over the first one + ElementData second_data_{second_->elementData()}; + second_data_.m_informations.addValue(RealTerminal::cableInfoKey(), + QStringLiteral("X98")); + second_data_.m_informations.addValue(RealTerminal::cableTargetInfoKey(), + QStringLiteral("Plant 9 / Cabinet 9 / -S9")); + second_->setElementData(second_data_); + QCOMPARE(b_->cableTarget(), + (QVector{QStringLiteral("Plant 9"), + QStringLiteral("Cabinet 9"), + QStringLiteral("-S9")})); + + const QRectF two_bounds{drawer.boundingRect()}; + const qreal two_row{lowest_row(render(two_bounds))}; + + //The box in the way is set lower : the strip draws a whole box + //deeper than it did with one single box -- the second box stands + //under the first one, one box height plus the gap between them + const QFontMetricsF metrics{pattern_->font()}; + const qreal box_height{metrics.height() * 3 + 4}; + QVERIFY2(two_row - one_row > box_height, + "a box reaching into another one must be set lower, not " + "written over it"); + + //And the strip is deep enough for it, or the lower box would be + //cut off by the drawing area + QVERIFY2(two_bounds.height() > one_bounds.height(), + "the strip reserves no room for the box it sets lower"); + QVERIFY2(two_bounds.bottom() - one_bounds.bottom() > 0.5 * box_height, + "the room reserved for the lower box is too small"); +} + int main(int argc, char **argv) { QStandardPaths::setTestModeEnabled(true); From 36b1cb08ff3e2829e73b3d5f3b23129030bdba9a Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Tue, 20 Oct 2026 19:08:16 +0200 Subject: [PATCH 11/12] Tell the fields of a target apart by line breaks only A target typed in by hand was split into its fields on " / ", which broke as soon as a field itself contained a slash: an installation named "ASP/01" gave the box two lines with half a name in each, and nothing to tell a separator apart from the name. Only a line break now breaks the text into fields (RealTerminal::cableTarget()), and the Target cell shows the fields the very same way, one line each (modelRealTerminalData::targetText()), so the cell and the box under the cable always read the same text. The Target cell is edited line by line to go with that: it takes a multi line editor where Return writes a new line, Ctrl+Return hands the cell back to the table and Escape throws the text away (TerminalStripModelDelegate), and the rows of the table are as tall as the lines their cell shows, so the fields below the first one are never cut off -- a cell without a target keeps the row it has always had. The text copied to the clipboard keeps one line per row, the fields of such a cell joined with " / " again, or a spreadsheet would take them for several rows. tst_terminalcableauto reads the three fields of a target of the drawing back one line each, checks a "/" typed inside a field stays part of it -- one line holding it, slashes and all -- checks the editor of the cell is the multi line one, hands its text back untouched, gives the cell back on Ctrl+Return while Return keeps writing a new line, and checks the rows grow with the lines the cell holds and shrink back afterwards. tst_cabletarget paints a strip whose cable ends on a field containing a slash: the box is no deeper than with one plain field. --- sources/TerminalStrip/realterminal.cpp | 18 +- sources/TerminalStrip/ui/modelTerminalData.h | 12 +- .../TerminalStrip/ui/terminalstripeditor.cpp | 19 +- .../TerminalStrip/ui/terminalstripmodel.cpp | 76 +++++- sources/TerminalStrip/ui/terminalstripmodel.h | 1 + tests/qttest/tst_cabletarget.cpp | 39 ++- tests/qttest/tst_terminalcableauto.cpp | 223 ++++++++++++++++-- 7 files changed, 342 insertions(+), 46 deletions(-) diff --git a/sources/TerminalStrip/realterminal.cpp b/sources/TerminalStrip/realterminal.cpp index 8b86dd31f..92b367fb3 100644 --- a/sources/TerminalStrip/realterminal.cpp +++ b/sources/TerminalStrip/realterminal.cpp @@ -413,8 +413,10 @@ bool RealTerminal::isShield() const { * typed in by hand when none does -- the Target cell of the terminal * strip editor is read only in the first case, so a target the drawing * cannot work out is still worth naming by hand. A text typed in is - * written the way the fields are shown in the column, - * "Installation / Location / Label", and gives one line per field. + * written one line per field, Installation, Location and Label (BMK), + * the way the cell shows them: only a line break breaks it into fields. + * A "/" written inside a field (a name like "ASP/01") is part of that + * field and never breaks anything. * @return the lines to write in the box under the cable */ QVector RealTerminal::cableTarget() const @@ -430,13 +432,15 @@ QVector RealTerminal::cableTarget() const .value(cableTargetInfoKey()).toString()}; if (!typed_.isEmpty()) { - //Written the way the fields are shown in the column, - //"Installation / Location / Label": the box draws one line - //per field, without the separators, like it does for a - //target worked out from the project + //One line by field, and only a line break breaks the text + //into fields : a "/" written inside a field stays where it is + QString text_{typed_}; + text_.replace(QStringLiteral("\r\n"), QStringLiteral("\n")); + text_.replace(QLatin1Char('\r'), QLatin1Char('\n')); + QVector lines_; for (const QString &field_ - : typed_.split(QStringLiteral(" / "), Qt::SkipEmptyParts)) { + : text_.split(QLatin1Char('\n'), Qt::SkipEmptyParts)) { lines_ << field_; } return lines_; diff --git a/sources/TerminalStrip/ui/modelTerminalData.h b/sources/TerminalStrip/ui/modelTerminalData.h index 0e8778832..ff14f21de 100644 --- a/sources/TerminalStrip/ui/modelTerminalData.h +++ b/sources/TerminalStrip/ui/modelTerminalData.h @@ -27,10 +27,12 @@ struct modelRealTerminalData { ///The Installation, Location and Label (BMK) of the piece the cable - ///lands on, joined in one line : what the strip draws in the box - ///under its cable (RealTerminal::cableTarget), in the same order. - ///A target typed in by hand is one single line, and nothing is - ///shown when the cable lands on nothing worth naming and no text + ///lands on, one line by field : what the strip draws in the box + ///under its cable (RealTerminal::cableTarget) and what the Target + ///cell shows, both read line by line. A "/" written inside a field + ///is part of that field, never a separator. + ///A target typed in by hand is kept as it was written, and nothing + ///is shown when the cable lands on nothing worth naming and no text ///was ever written in. static QString targetText(const QVector &target_) { @@ -41,7 +43,7 @@ struct modelRealTerminalData fields_ << field_; } } - return fields_.join(QStringLiteral(" / ")); + return fields_.join(QLatin1Char('\n')); } static modelRealTerminalData data(const QSharedPointer &real_t) diff --git a/sources/TerminalStrip/ui/terminalstripeditor.cpp b/sources/TerminalStrip/ui/terminalstripeditor.cpp index 9d17884e9..8b714fa0c 100644 --- a/sources/TerminalStrip/ui/terminalstripeditor.cpp +++ b/sources/TerminalStrip/ui/terminalstripeditor.cpp @@ -253,6 +253,14 @@ void TerminalStripEditor::setCurrentStrip(TerminalStrip *strip_) //the width of its own text, never a cut off Installation ui->m_table_widget->horizontalHeader()->setSectionResizeMode( TerminalStripModel::CableTarget, QHeaderView::ResizeToContents); + //That column holds one line by field: its rows must be tall + //enough for as many lines as a cell shows, while a cell of + //one line keeps the height the rows have always had + auto v_header_ = ui->m_table_widget->verticalHeader(); + v_header_->setMinimumSectionSize(qMax( + v_header_->defaultSectionSize(), + ui->m_table_widget->rowHeight(0))); + v_header_->setSectionResizeMode(QHeaderView::ResizeToContents); m_model->buildBridgePixmap(setUpBridgeCellWidth()); connect(ui->m_table_widget->selectionModel(), &QItemSelectionModel::selectionChanged, this, &TerminalStripEditor::selectionChanged); } @@ -880,8 +888,15 @@ void TerminalStripEditor::copySelectionToClipboard() for (auto row = top ; row <= bottom ; ++row) { QStringList cells; - for (auto column = left ; column <= right ; ++column) { - cells << texts.value((qint64(row) << 32) | qint64(column)); + for (auto column = left ; column <= right ; ++column) + { + //A cell may hold several lines (the Target cell holds one + //line by field): the text of the clipboard keeps one line by + //row, the fields of that cell are joined back the way a + //spreadsheet reads them + QString cell_{texts.value((qint64(row) << 32) | qint64(column))}; + cell_.replace(QLatin1Char('\n'), QStringLiteral(" / ")); + cells << cell_; } lines << cells.join(QLatin1Char('\t')); } diff --git a/sources/TerminalStrip/ui/terminalstripmodel.cpp b/sources/TerminalStrip/ui/terminalstripmodel.cpp index b15be27a9..9ac943241 100644 --- a/sources/TerminalStrip/ui/terminalstripmodel.cpp +++ b/sources/TerminalStrip/ui/terminalstripmodel.cpp @@ -33,6 +33,9 @@ #include #include #include +#include +#include +#include /** * Some const int who describe what a column contain @@ -517,8 +520,9 @@ QVariant TerminalStripModel::headerData(int section, Qt::Orientation orientation orientation == Qt::Horizontal && section == CABLE_TARGET_CELL) { - //Same three fields as the box drawn under the cable - return tr("Installation, Location and Label (BMK) of the piece the cable lands on, past this strip"); + //Same three fields as the box drawn under the cable, one line + //by field: only a line break separates them + return tr("Installation, Location and Label (BMK) of the piece the cable lands on, past this strip, one line per field"); } return QVariant(); @@ -1128,10 +1132,59 @@ QWidget *TerminalStripModelDelegate::createEditor(QWidget *parent, const QStyleO return qcb; } + if (index.column() == CABLE_TARGET_CELL) { + //The Target cell holds one line by field, so it takes a multi + //line editor : Return writes a new line (the cell gives its + //text back when the focus goes somewhere else), Ctrl+Return + //hands it over at once, Escape throws the text away + auto te = new QPlainTextEdit(parent); + te->setObjectName(QStringLiteral("terminal_cable_target")); + te->setFrameStyle(QFrame::NoFrame); + te->setTabChangesFocus(true); + te->setToolTip(tr("One line per field: Installation, Location, Label (BMK).\n" + "Ctrl+Return gives the cell back, Escape throws the text away.")); + te->installEventFilter(const_cast(this)); + + return te; + } return QStyledItemDelegate::createEditor(parent, option, index); } +/** + * @brief TerminalStripModelDelegate::eventFilter + * The filter taken on by the Target cell editor: only the key which is + * handled here is the one giving the text back to the table. The filter + * of the base class is left aside for that cell on purpose, it is written + * for one line editors and would take Return away from the text. + * @param object the editor of the cell + * @param event the event sent to it + * @return true when the event is taken by this delegate + */ +bool TerminalStripModelDelegate::eventFilter(QObject *object, QEvent *event) +{ + if (object && object->objectName() == QLatin1String("terminal_cable_target")) + { + if (event && event->type() == QEvent::KeyPress) + { + const auto key_event = static_cast(event); + if ((key_event->key() == Qt::Key_Return || + key_event->key() == Qt::Key_Enter) && + key_event->modifiers().testFlag(Qt::ControlModifier)) + { + auto editor_ = qobject_cast(object); + emit commitData(editor_); + emit closeEditor(editor_, QAbstractItemDelegate::NoHint); + + return true; + } + } + return false; + } + + return QStyledItemDelegate::eventFilter(object, event); +} + void TerminalStripModelDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { if (index.isValid() && @@ -1214,6 +1267,17 @@ void TerminalStripModelDelegate::setEditorData(QWidget *editor, const QModelInde return; } + if (index.isValid() && + editor->objectName() == QLatin1String("terminal_cable_target")) + { + if (auto te = dynamic_cast(editor)) { + //The cell shows one line by field: the editor gets them + //back exactly as they are, nothing joined, nothing split + te->setPlainText(index.data(Qt::EditRole).toString()); + } + return; + } + QStyledItemDelegate::setEditorData(editor, index); } @@ -1254,6 +1318,14 @@ void TerminalStripModelDelegate::setModelData(QWidget *editor, QAbstractItemMode model->setData(index, qcb->currentData(), Qt::EditRole); } } + else if (editor->objectName() == QLatin1String("terminal_cable_target")) + { + if (auto te = dynamic_cast(editor)) { + //The lines are handed over as they were written, so a + //" / " inside a field is part of it and never a separator + model->setData(index, te->toPlainText(), Qt::EditRole); + } + } else { QStyledItemDelegate::setModelData(editor, model, index); } diff --git a/sources/TerminalStrip/ui/terminalstripmodel.h b/sources/TerminalStrip/ui/terminalstripmodel.h index ac0fc9e7f..366638f97 100644 --- a/sources/TerminalStrip/ui/terminalstripmodel.h +++ b/sources/TerminalStrip/ui/terminalstripmodel.h @@ -141,6 +141,7 @@ class TerminalStripModelDelegate : public QStyledItemDelegate QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override; + bool eventFilter(QObject *object, QEvent *event) override; void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override; diff --git a/tests/qttest/tst_cabletarget.cpp b/tests/qttest/tst_cabletarget.cpp index 36095fd31..d12490dab 100644 --- a/tests/qttest/tst_cabletarget.cpp +++ b/tests/qttest/tst_cabletarget.cpp @@ -51,9 +51,10 @@ Also here: the box at the end of a cable is written at the font of the drawing -- the end zone the settings leave is what gives it the room, so three lines never end up squeezed into a tiny text. A - target typed in by hand is written the way the fields are shown in - the column, "Installation / Location / Label", and the box draws one - line per field, with no slash in it. Two cables of one strip end + target typed in by hand is written one line per field, Installation, + Location, Label, the way the column shows them, and only a line + break breaks it into fields: a "/" written inside a field is part of + it, the box draws one line holding it. Two cables of one strip end close to each other, and their boxes are wider than the room between them: the box standing in the way of the other is set lower, its stub drawn longer, and the strip reserves the room for it too. @@ -529,12 +530,14 @@ void tst_cabletarget::realisticStyleDrawsTheCable() } /** - A target typed in by hand is written the way the fields are shown in - the column, "Installation / Location / Label": the box under the - cable draws one line per field, with no slash in it. The picture of - a strip whose cable ends on three fields is therefore that much - deeper than the picture of the same strip with one single field -- - two lines of the font of the drawing. + A target typed in by hand is written one line per field, and only a + line break breaks it into fields: the box under the cable draws one + line per field, with no separator in it. The picture of a strip + whose cable ends on three fields is therefore that much deeper than + the picture of the same strip with one single field -- two lines of + the font of the drawing. A "/" written inside a field, an + installation named "Betrieb 1/2", keeps its line: the box is no + deeper than with one plain field. */ void tst_cabletarget::typedTargetIsDrawnInItsOwnLines() { @@ -604,9 +607,9 @@ void tst_cabletarget::typedTargetIsDrawnInItsOwnLines() QVERIFY(one_line >= 0); //Three fields: the box is two lines deeper, at the font of the - //drawing -- the slashes are only there to separate the fields in - //the column, the box never draws them - type_in(QStringLiteral("Boiler room / Cabinet 3 / -K3")); + //drawing -- the fields are written one under the other, the cell + //holding them only tells them apart by their line breaks + type_in(QStringLiteral("Boiler room\nCabinet 3\n-K3")); QCOMPARE(a_->cableTarget(), (QVector{QStringLiteral("Boiler room"), QStringLiteral("Cabinet 3"), QStringLiteral("-K3")})); @@ -614,6 +617,16 @@ void tst_cabletarget::typedTargetIsDrawnInItsOwnLines() const QFontMetricsF metrics{pattern_->font()}; QVERIFY2(three_lines - one_line > 1.5 * metrics.height(), "three fields must give a box two lines deeper than one field"); + + //A slash written inside a field is part of it: the text gives + //one single line, the box is back to the depth of one field and + //holds the slash where it was typed + type_in(QStringLiteral("Betrieb 1/2 / Keller")); + QCOMPARE(a_->cableTarget(), + QVector{QStringLiteral("Betrieb 1/2 / Keller")}); + const qreal one_slashed_field{lowest_row(render())}; + QVERIFY2(qAbs(one_slashed_field - one_line) < metrics.height() / 2, + "a slash inside a field must not split it into lines"); } /** @@ -688,7 +701,7 @@ void tst_cabletarget::boxesOfTwoCablesAreSetApart() second_data_.m_informations.addValue(RealTerminal::cableInfoKey(), QStringLiteral("X98")); second_data_.m_informations.addValue(RealTerminal::cableTargetInfoKey(), - QStringLiteral("Plant 9 / Cabinet 9 / -S9")); + QStringLiteral("Plant 9\nCabinet 9\n-S9")); second_->setElementData(second_data_); QCOMPARE(b_->cableTarget(), (QVector{QStringLiteral("Plant 9"), diff --git a/tests/qttest/tst_terminalcableauto.cpp b/tests/qttest/tst_terminalcableauto.cpp index 56c764590..e0dfd7dae 100644 --- a/tests/qttest/tst_terminalcableauto.cpp +++ b/tests/qttest/tst_terminalcableauto.cpp @@ -30,6 +30,12 @@ #include "qetproject.h" #include +#include +#include +#include +#include +#include +#include /** The Cable and Color / wire number columns of the terminal strip editor @@ -45,10 +51,11 @@ cable it is read only -- only the drawing decides it -- and with no cable landing anywhere it is there to be typed in, just like the Cable cell, the text then stored in the element and kept until a - cable works a target out again. A text typed in is written the way - the fields are shown in the column, "Installation / Location / - Label", so the box draws one line per field and not one long line - needing the slashes. + cable works a target out again. A text typed in is written one line + per field, Installation, Location, Label, the way the column shows + them: only a line break breaks it into fields, so a "/" written + inside a field (a name like "ASP/01") is part of that field and the + box draws one line containing it. fixtures/every_feature.qet has the strip X1 whose two terminals are wired to conductors on the folio, and no cable at all. @@ -358,11 +365,11 @@ private slots: QCOMPARE(model.data(model.index(0, TerminalStripModel::Cable)) .toString(), QStringLiteral("15W1")); - //The three fields, in the order the box draws them + //The three fields, one on its own line as the box draws them const QModelIndex target_cell = model.index(0, TerminalStripModel::CableTarget); QCOMPARE(model.data(target_cell).toString(), - QStringLiteral("Plant 7 / Cabinet 7 / -S7")); + QStringLiteral("Plant 7\nCabinet 7\n-S7")); QVERIFY(!(model.flags(target_cell) & Qt::ItemIsEditable)); QCOMPARE(model.headerData(TerminalStripModel::CableTarget, Qt::Horizontal).toString(), @@ -409,7 +416,7 @@ private slots: QVERIFY(model->data(cell).toString().isEmpty()); QVERIFY(model->flags(cell) & Qt::ItemIsEditable); - const QString typed{QStringLiteral("Boiler room / Cabinet 3 / -K3")}; + const QString typed{QStringLiteral("Boiler room\nCabinet 3\n-K3")}; QVERIFY(model->setData(cell, typed, Qt::EditRole)); QCOMPARE(model->data(cell, Qt::EditRole).toString(), typed); QCOMPARE(model->data(cell).toString(), typed); @@ -418,8 +425,8 @@ private slots: editor.apply(); QCOMPARE(element->elementData().m_informations .value(RealTerminal::cableTargetInfoKey()).toString(), typed); - //Three fields, written the way the column shows them: the box - //draws three lines, without the slashes + //Three lines, each field on its own: the box draws them one under + //the other, without any separator QCOMPARE(real_t->cableTarget(), (QVector{QStringLiteral("Boiler room"), QStringLiteral("Cabinet 3"), @@ -465,7 +472,7 @@ private slots: const QModelIndex cable_cell = model_cable.index( 0, TerminalStripModel::CableTarget); QCOMPARE(model_cable.data(cable_cell).toString(), - QStringLiteral("Plant 9 / Cabinet 9 / -S9")); + QStringLiteral("Plant 9\nCabinet 9\n-S9")); QVERIFY(!(model_cable.flags(cable_cell) & Qt::ItemIsEditable)); //And the automatic target was never written in the element QCOMPARE(element->elementData().m_informations @@ -486,10 +493,11 @@ private slots: QVERIFY(model_back.flags(back_cell) & Qt::ItemIsEditable); } - // A text typed in by hand is written the way the fields are shown in - // the column, "Installation / Location / Label": the box under the - // cable draws one line per field, without the slashes, exactly like a - // target worked out from the project. + // A text typed in by hand is written one line per field, and only a + // line break breaks it into fields: the box under the cable draws + // one line per field, exactly like a target worked out from the + // project, while a "/" written inside a field is part of it and + // gives one single line holding it. void typedTargetGivesOneLinePerField() { const QString fixture = QFINDTESTDATA("fixtures/every_feature.qet"); @@ -509,10 +517,11 @@ private slots: QCOMPARE(real_t->cableTarget(), QVector{QStringLiteral("Boiler room")}); - //Three fields: three lines, and no slash anywhere in them + //Three fields, written one per line: three lines, and no + //separator anywhere in them typed = element->elementData(); typed.m_informations.addValue(RealTerminal::cableTargetInfoKey(), - QStringLiteral("Plant 9 / Cabinet 9 / -S9")); + QStringLiteral("Plant 9\nCabinet 9\n-S9")); element->setElementData(typed); QCOMPARE(real_t->cableTarget(), (QVector{QStringLiteral("Plant 9"), @@ -524,7 +533,187 @@ private slots: //The column shows the text back the way it was written TerminalStripModel model(project.terminalStrip().first()); QCOMPARE(model.data(model.index(0, TerminalStripModel::CableTarget)) - .toString(), QStringLiteral("Plant 9 / Cabinet 9 / -S9")); + .toString(), QStringLiteral("Plant 9\nCabinet 9\n-S9")); + + //A slash inside a field, and one typed between two words of a + //field kept in one line: both stay part of the field, the cell + //shows them untouched and the box holds one single line each + typed = element->elementData(); + typed.m_informations.addValue(RealTerminal::cableTargetInfoKey(), + QStringLiteral("Betrieb 1/2")); + element->setElementData(typed); + QCOMPARE(real_t->cableTarget(), + QVector{QStringLiteral("Betrieb 1/2")}); + + typed = element->elementData(); + typed.m_informations.addValue(RealTerminal::cableTargetInfoKey(), + QStringLiteral("Betrieb 1/2 / Keller")); + element->setElementData(typed); + QCOMPARE(real_t->cableTarget(), + QVector{QStringLiteral("Betrieb 1/2 / Keller")}); + TerminalStripModel slashed(project.terminalStrip().first()); + QCOMPARE(slashed.data(slashed.index(0, TerminalStripModel::CableTarget)) + .toString(), QStringLiteral("Betrieb 1/2 / Keller")); + + //The line breaks written by hand are the fields again, whatever + //the fields themselves contain + typed = element->elementData(); + typed.m_informations.addValue(RealTerminal::cableTargetInfoKey(), + QStringLiteral("Betrieb 1/2\nKeller\n-K4")); + element->setElementData(typed); + QCOMPARE(real_t->cableTarget(), + (QVector{QStringLiteral("Betrieb 1/2"), + QStringLiteral("Keller"), + QStringLiteral("-K4")})); + } + + // The Target cell is edited line by line: its editor holds one line + // per field and hands the text back exactly as the cell holds it. + // Return writes a new line (the cell is multi line), Ctrl+Return + // gives the cell back without waiting for the focus to go somewhere + // else, and the row of the table is as tall as the lines the cell + // shows, so none of them is cut off. + void targetCellIsEditedLineByLine() + { + const QString fixture = QFINDTESTDATA("fixtures/every_feature.qet"); + QETProject project(fixture); + QCOMPARE(project.state(), QETProject::Ok); + + TerminalStripEditor editor(&project); + editor.setCurrentStrip(project.terminalStrip().first()); + QTableView *table = editor.findChild( + QStringLiteral("m_table_widget")); + QVERIFY2(table && table->model(), "the editor has its table"); + QAbstractItemModel *model = table->model(); + QVERIFY(model->rowCount() > 1); + const QModelIndex cell = model->index(0, TerminalStripModel::CableTarget); + QVERIFY(model->flags(cell) & Qt::ItemIsEditable); + + const QString typed{QStringLiteral("Betrieb 1/2\nKeller\n-K4")}; + QVERIFY(model->setData(cell, typed, Qt::EditRole)); + QCOMPARE(model->data(cell).toString(), typed); + + //The editor of the table is the one taking a cell of several + //lines, and one of its own does the very same thing for the + //keys, without a view in between + QVERIFY(qobject_cast( + table->itemDelegate())); + TerminalStripModelDelegate delegate; + + QStyleOptionViewItem option; + option.rect = QRect(0, 0, qMax( + 50, table->columnWidth(TerminalStripModel::CableTarget)), 400); + option.font = table->font(); + option.fontMetrics = QFontMetrics(option.font); + + QWidget *cell_editor = delegate.createEditor(table, option, cell); + QVERIFY(cell_editor); + auto *text_edit = qobject_cast(cell_editor); + QVERIFY2(text_edit, "the Target cell takes a multi line editor"); + QCOMPARE(text_edit->objectName(), + QStringLiteral("terminal_cable_target")); + + //The editor gives the lines back exactly as the cell holds + //them: nothing joined, nothing split again + delegate.setEditorData(cell_editor, cell); + QCOMPARE(text_edit->toPlainText(), typed); + delegate.setModelData(cell_editor, model, cell); + QCOMPARE(model->data(cell).toString(), typed); + + //Return writes a new line in the cell, and hands nothing over + //yet: Ctrl+Return is the key giving the cell back + QSignalSpy committed(&delegate, &TerminalStripModelDelegate::commitData); + QSignalSpy closed(&delegate, &TerminalStripModelDelegate::closeEditor); + text_edit->moveCursor(QTextCursor::End); + + QKeyEvent return_key{QEvent::KeyPress, Qt::Key_Return, + Qt::NoModifier}; + QCoreApplication::sendEvent(cell_editor, &return_key); + QCOMPARE(text_edit->toPlainText(), typed + QLatin1Char('\n')); + QCOMPARE(committed.count(), 0); + QCOMPARE(closed.count(), 0); + + QKeyEvent ctrl_return_key{QEvent::KeyPress, Qt::Key_Return, + Qt::ControlModifier}; + QCoreApplication::sendEvent(cell_editor, &ctrl_return_key); + QCOMPARE(committed.count(), 1); + QCOMPARE(closed.count(), 1); + delete cell_editor; + } + + // The row of the table follows the lines its Target cell holds: the + // cell is never cut off, however many lines it takes, the row grows + // to hold them all and shrinks back afterwards, while the rows + // around keep the height they have always had. + void targetRowGrowsWithTheLinesOfItsCell() + { + const QString fixture = QFINDTESTDATA("fixtures/every_feature.qet"); + QETProject project(fixture); + QCOMPARE(project.state(), QETProject::Ok); + + TerminalStripEditor editor(&project); + editor.setCurrentStrip(project.terminalStrip().first()); + QTableView *table = editor.findChild( + QStringLiteral("m_table_widget")); + QVERIFY2(table && table->model(), "the editor has its table"); + QAbstractItemModel *model = table->model(); + QVERIFY(model->rowCount() > 1); + const QModelIndex cell = model->index(0, TerminalStripModel::CableTarget); + QVERIFY(model->flags(cell) & Qt::ItemIsEditable); + QVERIFY2(model->data(cell).toString().isEmpty(), + "no target to start with, the row is the usual one"); + + const QModelIndex other_cell = model->index( + 1, TerminalStripModel::CableTarget); + const int base_row = table->rowHeight(cell.row()); + const int base_other_row = table->rowHeight(other_cell.row()); + QVERIFY(base_row > 0); + + //Three lines in the cell: the row holds them all, at the + //font of the table -- the row was already taller than that + //for what else the terminal draws, so it may stay as it is + const QString typed{QStringLiteral("Betrieb 1/2\nKeller\n-K4")}; + QVERIFY(model->setData(cell, typed, Qt::EditRole)); + QCOMPARE(model->data(cell).toString(), typed); + + const auto heightOfLines = [table](int lines_) { + return lines_ * table->fontMetrics().height(); + }; + const int three_line_row = table->rowHeight(cell.row()); + QVERIFY2(three_line_row >= qMax(base_row, heightOfLines(3)), + "the row must be as tall as both the lines the cell " + "shows and what it held before, or something is cut off"); + QVERIFY2(table->rowHeight(other_cell.row()) >= base_other_row, + "the rows around are not pushed down"); + + //From here on the rows have been worked out once: what the + //other rows hold is the height they keep + const int other_row = table->rowHeight(other_cell.row()); + + //Many more lines than anything else in the row: the row grows + //to hold them, and the rows around stay where they are + const QString many{QStringLiteral( + "Plant 6\nCabinet 6\n-K6\nZone 6\nLevel 6\nEnd 6")}; + QVERIFY(model->setData(cell, many, Qt::EditRole)); + const int six_line_row = table->rowHeight(cell.row()); + QVERIFY2(six_line_row >= heightOfLines(6), + "the row must be as tall as the lines the cell shows, " + "or the fields below the first one are cut off"); + QVERIFY2(six_line_row > qMax(base_row, three_line_row), + "a cell which took six lines must take a taller row " + "than the one it had before"); + QVERIFY2(table->rowHeight(other_cell.row()) == other_row, + "the rows around keep their own height"); + + //Back to one single line: the row follows again + QVERIFY(model->setData(cell, QStringLiteral("Keller"), Qt::EditRole)); + QVERIFY2(table->rowHeight(cell.row()) < six_line_row, + "the row follows the cell back to a single line"); + QVERIFY2(table->rowHeight(cell.row()) + <= base_row + table->fontMetrics().height(), + "the row is back to the height it had before"); + QVERIFY2(table->rowHeight(cell.row()) >= table->fontMetrics().height(), + "the single line left is still shown"); } }; From 650e318e021ee09e7f89d94d40e0a4403dd3d03a Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Wed, 21 Oct 2026 17:42:12 +0200 Subject: [PATCH 12/12] Answer the terminal strip editor without walking the whole strip The editor asks its model for every cell of every row, over and over (paint, flags, row height, resize to contents). Three answers used to cost far more than the little they tell: - a row was found by walking m_physical_data each time, which made a layout O(rows^2 * columns); - the bridge cells walked to the head of the strip for each neighbour of each step, and every step filled the full row data, which reads the cables of the whole project (the target column of the terminal strip editor); - without any bridge on the strip, that walk still ran to its end. Rows are now found through an index filled once with m_physical_data, the bridge walk steps from row to row and fills only the three fields it reads (level, bridge, identity), and it is skipped when the strip has no bridge at all. Nothing about what the editor shows, edits or saves changes: the model data, the bridge pixmaps and the strip order are the same, only the time to answer is not. Measured on a 302 row strip with one bridge (20 rounds, offscreen, off the UI): all flags 372 ms to 6.6 ms, all display cells 1195 ms to 31 ms, all decoration 7623 ms to 106 ms (a bridge cell 251 us to 2.5 us), about 65 times faster overall. The full test suite passes (121/121). --- sources/TerminalStrip/terminalstrip.h | 4 + .../TerminalStrip/ui/terminalstripmodel.cpp | 168 +++++++++--------- sources/TerminalStrip/ui/terminalstripmodel.h | 14 ++ 3 files changed, 98 insertions(+), 88 deletions(-) diff --git a/sources/TerminalStrip/terminalstrip.h b/sources/TerminalStrip/terminalstrip.h index 313f13ee0..a3b13cc9d 100644 --- a/sources/TerminalStrip/terminalstrip.h +++ b/sources/TerminalStrip/terminalstrip.h @@ -118,6 +118,10 @@ class TerminalStrip : public QObject void unBridge(const QVector> &real_terminals); bool canUnBridge(const QVector > &real_terminals) const; QSharedPointer isBridged(const QSharedPointer real_terminal) const; + ///True when at least one bridge joins terminals of this strip : + ///the bridge cells of the terminal strip editor are the only thing + ///drawn from them (see TerminalStripModel::bridgePixmapFor) + bool hasBridges() const { return !m_bridge.isEmpty(); } QSharedPointer bridgeFor (const QVector> &terminal_vector) const; QSharedPointer previousTerminalInLevel(const QSharedPointer &real_terminal) const; diff --git a/sources/TerminalStrip/ui/terminalstripmodel.cpp b/sources/TerminalStrip/ui/terminalstripmodel.cpp index 9ac943241..2fd22539a 100644 --- a/sources/TerminalStrip/ui/terminalstripmodel.cpp +++ b/sources/TerminalStrip/ui/terminalstripmodel.cpp @@ -187,12 +187,7 @@ int TerminalStripModel::rowCount(const QModelIndex &parent) const return 0; } - auto count = 0; - for (const auto &mptd : m_physical_data) { - count += mptd.real_data.size(); - } - - return count; + return m_row_count; } int TerminalStripModel::columnCount(const QModelIndex &parent) const @@ -274,7 +269,8 @@ QVariant TerminalStripModel::data(const QModelIndex &index, int role) const { return QBrush(Qt::gray); //"Spare" : not a reference to a folio } - else if (role == Qt::BackgroundRole && index.column() < COLUMN_COUNT ) + else if (role == Qt::BackgroundRole && index.column() < COLUMN_COUNT + && !m_modified_cell.isEmpty()) { const auto key_ = modifiedCellKey(mrtd); if (m_modified_cell.contains(key_) && @@ -635,7 +631,7 @@ QVector TerminalStripModel::modelRealTerminalDataForIndex for (auto i : set_) { const auto rtd_ = realDataAtIndex(i); if (rtd_.level_ > -1) { - vector_.append(realDataAtIndex(i)); + vector_.append(rtd_); } } @@ -743,6 +739,8 @@ void TerminalStripModel::reload() void TerminalStripModel::fillPhysicalTerminalData() { + m_row_index.clear(); + //Get all physical terminal if (m_terminal_strip) { @@ -752,41 +750,32 @@ void TerminalStripModel::fillPhysicalTerminalData() mptd.pos_ = phy_t->pos(); mptd.uuid_ = phy_t->uuid(); + const auto physical_index = static_cast(m_physical_data.size()); for (const auto &real_t : phy_t->realTerminals()) { if (!real_t.isNull()) { mptd.real_data.append(modelRealTerminalData::data(real_t)); + m_row_index.append({physical_index, + static_cast(mptd.real_data.size()) - 1}); } } m_physical_data.append(mptd); } } + + m_row_count = m_row_index.size(); } modelRealTerminalData TerminalStripModel::dataAtRow(int row) const { - if (row > rowCount(QModelIndex())) { + if (row < 0 || row >= m_row_count) { return modelRealTerminalData(); } - else - { - auto current_row = 0; - for (const auto &physical_data : std::as_const(m_physical_data)) - { - for (const auto &real_data : physical_data.real_data) - { - if (current_row == row) { - return real_data; - } else { - ++current_row; - } - } - } - } - return modelRealTerminalData(); + const auto &place_ = m_row_index.at(row); + return m_physical_data.at(place_.physical_).real_data.at(place_.real_); } /** @@ -797,32 +786,12 @@ modelRealTerminalData TerminalStripModel::dataAtRow(int row) const */ void TerminalStripModel::replaceDataAtRow(modelRealTerminalData data, int row) { - if (row > rowCount(QModelIndex())) { + if (row < 0 || row >= m_row_count) { return; } - else - { - auto current_row = 0; - auto current_physical = 0; - for (const auto &physical_data : std::as_const(m_physical_data)) - { - auto current_real = 0; - for (int i=0 ; i= index) { - match_ = true; - break; - } + //A row always belongs to the physical terminal it is a level of, + //m_row_index says which one (negative index: the first physical + //terminal, as the walk this replaces used to give) + if (index < 0) { + return m_physical_data.first(); } - - if (match_) { - return m_physical_data.at(current_phy); - } else { + if (index >= m_row_count) { return modelPhysicalTerminalData(); } + + return m_physical_data.at(m_row_index.at(index).physical_); } /** @@ -869,23 +829,34 @@ modelPhysicalTerminalData TerminalStripModel::physicalDataAtIndex(int index) con */ modelRealTerminalData TerminalStripModel::realDataAtIndex(int index) const { - if (m_physical_data.isEmpty()) { + if (index < 0 || index >= m_row_count) { return modelRealTerminalData(); } - int current_checked_index = -1; + const auto &place_ = m_row_index.at(index); + return m_physical_data.at(place_.physical_).real_data.at(place_.real_); +} - for (const auto & ptd_ : std::as_const(m_physical_data)) +/** + * @brief bridgeWalkStep + * The three fields the neighbour walk of bridgePixmapFor() reads: level, + * bridge and identity. The whole row data would answer as well, but filling + * it reads the cables of the project (RealTerminal::cableTarget() and + * friends) -- work done for every step of a walk which the view runs for + * every bridge cell it paints and measures. + * @param real_t : the terminal to look at, may be null (no previous one) + * @return the data the walk needs, everything else left as it defaults + */ +static modelRealTerminalData bridgeWalkStep(const QSharedPointer &real_t) +{ + modelRealTerminalData mrtd; + if (!real_t.isNull()) { - for (const auto & rtd_ : std::as_const(ptd_.real_data)) { - ++current_checked_index; - if (current_checked_index == index) { - return rtd_; - } - } + mrtd.level_ = real_t->level(); + mrtd.bridged_ = real_t->isBridged(); + mrtd.real_terminal = real_t.toWeakRef(); } - - return modelRealTerminalData(); + return mrtd; } QPixmap TerminalStripModel::bridgePixmapFor(const QModelIndex &index) const @@ -899,6 +870,15 @@ QPixmap TerminalStripModel::bridgePixmapFor(const QModelIndex &index) const return QPixmap(); } + //Without a bridge on this strip there is nothing to draw in the + //bridge cells: the check at the end of this function ends on an + //empty pixmap, and every step of the walk below to reach it used + //to fill the row data of a terminal (which reads the cables of the + //whole project). Skip the walk when there is no bridge at all. + if (!m_terminal_strip->hasBridges()) { + return QPixmap(); + } + auto mrtd = modelRealTerminalDataForIndex(index); //Terminal level correspond to the column level of index @@ -937,22 +917,30 @@ QPixmap TerminalStripModel::bridgePixmapFor(const QModelIndex &index) const //Terminal level ins't in the same column level of index //Check if we need to draw a none bridge pixmap - //Check previous - auto phy_t = mrtd.real_terminal.toStrongRef()->physicalTerminal(); - auto current_real_terminal = mrtd; - auto current_phy_uuid = phy_t->uuid(); + //Check previous. The rows of the table hold the terminals of the + //strip in strip order, so the neighbour of a row is the row next + //to it: asking the strip for its neighbour walked to the head of + //the strip every time, for every bridge cell the view paints and + //measures (and on every layout of the table). + auto current_row = index.row(); + auto current_t = mrtd.real_terminal.toStrongRef(); + auto current_phy_uuid = current_t->physicalTerminal()->uuid(); bool already_jumped_to_previous = false; modelRealTerminalData previous_data; do { - current_real_terminal = modelRealTerminalData::data(m_terminal_strip->previousRealTerminal(current_real_terminal.real_terminal)); + --current_row; + const auto previous_t = current_row >= 0 + ? dataAtRow(current_row).real_terminal.toStrongRef() + : QSharedPointer(); + const auto current_real_terminal = bridgeWalkStep(previous_t); if (current_real_terminal.level_ == -1) { break; } //We are in the same physical terminal as previous loop - if (current_phy_uuid == current_real_terminal.real_terminal.toStrongRef()->physicalTerminal()->uuid()) + if (current_phy_uuid == previous_t->physicalTerminal()->uuid()) { if (current_real_terminal.bridged_ && current_real_terminal.level_ == level_column) { @@ -964,7 +952,7 @@ QPixmap TerminalStripModel::bridgePixmapFor(const QModelIndex &index) const break; } else { already_jumped_to_previous = true; - current_phy_uuid = current_real_terminal.real_terminal.toStrongRef()->physicalTerminal()->uuid(); + current_phy_uuid = previous_t->physicalTerminal()->uuid(); if (current_real_terminal.bridged_ && current_real_terminal.level_ == level_column) { previous_data = current_real_terminal; @@ -974,20 +962,24 @@ QPixmap TerminalStripModel::bridgePixmapFor(const QModelIndex &index) const } while(true); //Check next - current_real_terminal = mrtd; - current_phy_uuid = phy_t->uuid(); + current_row = index.row(); + current_phy_uuid = current_t->physicalTerminal()->uuid(); bool already_jumped_to_next = false; modelRealTerminalData next_data; do { - current_real_terminal = modelRealTerminalData::data(m_terminal_strip->nextRealTerminal(current_real_terminal.real_terminal)); + ++current_row; + const auto next_t = current_row < m_row_count + ? dataAtRow(current_row).real_terminal.toStrongRef() + : QSharedPointer(); + const auto current_real_terminal = bridgeWalkStep(next_t); if (current_real_terminal.level_ == -1) { break; } //We are in the same physical terminal as previous loop - if (current_phy_uuid == current_real_terminal.real_terminal.toStrongRef()->physicalTerminal()->uuid()) + if (current_phy_uuid == next_t->physicalTerminal()->uuid()) { if (current_real_terminal.bridged_ && current_real_terminal.level_ == level_column) { @@ -999,7 +991,7 @@ QPixmap TerminalStripModel::bridgePixmapFor(const QModelIndex &index) const break; } else { already_jumped_to_next = true; - current_phy_uuid = current_real_terminal.real_terminal.toStrongRef()->physicalTerminal()->uuid(); + current_phy_uuid = next_t->physicalTerminal()->uuid(); if (current_real_terminal.bridged_ && current_real_terminal.level_ == level_column) { next_data = current_real_terminal; diff --git a/sources/TerminalStrip/ui/terminalstripmodel.h b/sources/TerminalStrip/ui/terminalstripmodel.h index 366638f97..a714a1631 100644 --- a/sources/TerminalStrip/ui/terminalstripmodel.h +++ b/sources/TerminalStrip/ui/terminalstripmodel.h @@ -107,10 +107,24 @@ class TerminalStripModel : public QAbstractTableModel QPixmap bridgePixmapFor(const QModelIndex &index) const; private: + //Where the data of one row of the table sit in m_physical_data. + //The view asks for every cell of every row, over and over (paint, + //flags, row height), so looking a row up must be a lookup and not + //a walk through the whole strip each time. + struct RowPlace + { + int physical_; //index in m_physical_data + int real_; //index in the real_data of that physical terminal + }; + QPointer m_terminal_strip; //Modified cells of each real terminal (spare terminals have no element) QHash> m_modified_cell; QVector m_physical_data; + //One RowPlace by row, filled with m_physical_data (see fillPhysicalTerminalData) + QVector m_row_index; + //Number of rows held in m_row_index, so rowCount() needs no walk + int m_row_count = 0; struct BridgePixmap { QPixmap top_,