Merge pull request #1577 from Kellermorph/integration/terminal-manager-upstream
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m12s

Terminal strip editor: automatic cable entry, plan order, target box, Target column and speed-ups
This commit is contained in:
Laurent Trinques
2026-10-22 06:01:59 +02:00
committed by GitHub
23 changed files with 3024 additions and 176 deletions
+16
View File
@@ -318,6 +318,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)
@@ -364,6 +369,17 @@ 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)
# 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)
+737
View File
@@ -0,0 +1,737 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "qetapptest.h"
#include "TerminalStrip/GraphicsItem/realisticterminalstripdrawer.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 <QImage>
#include <QPainter>
#include <QStandardPaths>
/**
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()).
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.
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 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.
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,
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<RealTerminal> 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 wireLabelStaysOutOfTheTerminal();
void targetBoxKeepsTheDrawingFont();
void typedTargetIsDrawnInItsOwnLines();
void boxesOfTwoCablesAreSetApart();
void realisticStyleDrawsTheCable();
};
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<QString>({
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<TerminalStripDrawer::TrueTerminalStrip>{
new TerminalStripDrawer::TrueTerminalStrip{strips_.first()}},
QSharedPointer<TerminalStripLayoutPattern>{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);
}
/**
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<TerminalStripLayoutPattern>{new TerminalStripLayoutPattern};
const auto strips_ = m_project->terminalStrip();
QVERIFY(!strips_.isEmpty());
TerminalStripDrawer::TerminalStripDrawer drawer{
QSharedPointer<TerminalStripDrawer::TrueTerminalStrip>{
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));
}
}
}
/**
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<TerminalStripLayoutPattern>{new TerminalStripLayoutPattern};
const auto strips_ = m_project->terminalStrip();
QVERIFY(!strips_.isEmpty());
TerminalStripDrawer::TerminalStripDrawer drawer{
QSharedPointer<TerminalStripDrawer::TrueTerminalStrip>{
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<qreal>(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<qreal>(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);
}
/**
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<TerminalStripDrawer::TrueTerminalStrip>{
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());
}
/**
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()
{
//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<TerminalStripLayoutPattern>{new TerminalStripLayoutPattern};
const auto strips_ = m_project->terminalStrip();
QVERIFY(!strips_.isEmpty());
TerminalStripDrawer::TerminalStripDrawer drawer{
QSharedPointer<TerminalStripDrawer::TrueTerminalStrip>{
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 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<QString>{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");
//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<QString>{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");
}
/**
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<TerminalStripLayoutPattern>{new TerminalStripLayoutPattern};
const auto strips_ = m_project->terminalStrip();
QVERIFY(!strips_.isEmpty());
TerminalStripDrawer::TerminalStripDrawer drawer{
QSharedPointer<TerminalStripDrawer::TrueTerminalStrip>{
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\nCabinet 9\n-S9"));
second_->setElementData(second_data_);
QCOMPARE(b_->cableTarget(),
(QVector<QString>{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);
return QetAppTest::run<tst_cabletarget>(argc, argv);
}
#include "tst_cabletarget.moc"
+722
View File
@@ -0,0 +1,722 @@
// 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 <http://www.gnu.org/licenses/>.
*/
#include "qetapptest.h"
#include "TerminalStrip/physicalterminal.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 "qetinformation.h"
#include "qetproject.h"
#include <QAbstractItemView>
#include <QCoreApplication>
#include <QKeyEvent>
#include <QPlainTextEdit>
#include <QSignalSpy>
#include <QTableView>
#include <QTextCursor>
/**
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.
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 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.
*/
class tst_terminalcableauto : public QObject
{
Q_OBJECT
// The wired terminal of strip X1: its element carries a conductor.
static QSharedPointer<RealTerminal> wiredTerminal(QETProject &project)
{
const QVector<TerminalStrip *> strips = project.terminalStrip();
if (strips.isEmpty()) return {};
for (const QSharedPointer<RealTerminal> &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<Conductor *> conductorsOf(const QSharedPointer<RealTerminal> &real_t)
{
QList<Conductor *> 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<RealTerminal> &real_t)
{
const QList<Conductor *> conductors = conductorsOf(real_t);
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<RealTerminal> &real_t)
{
TerminalStrip *strip = real_t ? real_t->parentStrip() : nullptr;
if (!strip) return nullptr;
QSet<QUuid> own;
for (const QSharedPointer<PhysicalTerminal> &physical
: strip->physicalTerminal())
{
if (!physical) continue;
for (const QSharedPointer<RealTerminal> &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,
const QSharedPointer<RealTerminal> &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<QAbstractItemView *>(
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<Conductor *> 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());
}
// 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, 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\nCabinet 7\n-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<QAbstractItemView *>(
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\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);
//Apply writes it in the element, and nowhere else
editor.apply();
QCOMPARE(element->elementData().m_informations
.value(RealTerminal::cableTargetInfoKey()).toString(), typed);
//Three lines, each field on its own: the box draws them one under
//the other, without any separator
QCOMPARE(real_t->cableTarget(),
(QVector<QString>{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<QString>{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\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
.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<QString>{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 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");
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<QString>{QStringLiteral("Boiler room")});
//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\nCabinet 9\n-S9"));
element->setElementData(typed);
QCOMPARE(real_t->cableTarget(),
(QVector<QString>{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\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<QString>{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<QString>{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<QString>{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<QTableView *>(
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<TerminalStripModelDelegate *>(
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<QPlainTextEdit *>(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<QTableView *>(
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");
}
};
QET_APP_TEST_MAIN(tst_terminalcableauto)
#include "tst_terminalcableauto.moc"
+272
View File
@@ -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 <http://www.gnu.org/licenses/>.
*/
#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 <QStandardPaths>
/**
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<RealTerminal> 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<Diagram *> 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<tst_terminalstriporder>(argc, argv);
}
#include "tst_terminalstriporder.moc"