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..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_ @@ -245,8 +329,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/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 4dfdb51e4..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) { @@ -497,8 +505,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) { 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..802068b28 --- /dev/null +++ b/tests/qttest/tst_terminalcableauto.cpp @@ -0,0 +1,271 @@ +// 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/terminalstripeditor.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" + +#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 + 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 QList conductorsOf(const QSharedPointer &real_t) + { + QList conductors; + Element *element = real_t ? real_t->element() : nullptr; + if (!element) return conductors; + for (Terminal *terminal : element->terminals()) { + if (!terminal) continue; + for (Conductor *conductor : terminal->conductors()) { + if (conductor && !conductors.contains(conductor)) + conductors.append(conductor); + } + } + 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, one core wired to the terminal's conductor (or to @p on). + static Cable *wireCable(QETProject &project, + const QSharedPointer &real_t, + int core = 1, + const QString &designation = QStringLiteral("15W1"), + Conductor *on = nullptr) + { + if (!on) on = conductorOf(real_t); + Cable *cable = project.newCable(); + cable->setDesignation(designation); + cable->setCoreCount(4); + cable->setCoreColors({QStringLiteral("bn"), QStringLiteral("bl"), + QStringLiteral("gn/ye"), QStringLiteral("gy")}); + CableCore assignment; + assignment.core = core; + assignment.conductor = on->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); + } + + // 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) + +#include "tst_terminalcableauto.moc"