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
@@ -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<qreal> m_bridge_point_y_offset{50,70,90,110};
@@ -522,6 +522,77 @@ 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;
}
/**
* @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<Helpers::CableCell> 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<Helpers::CableCell> cells;
for (const auto &g_ : geometry(&strip))
{
const qreal w_{g_.width_mm * s_};
const int levels_{static_cast<int>(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,
@@ -535,7 +606,25 @@ 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.
//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<qreal>(0, pattern.m_cable_length)
+ std::max<qreal>(0, pattern.m_cable_end_length)
+ 4
+ Helpers::cableBoxesExtraDepth(cableCells(*strip, pattern),
pattern);
}
return QRectF{0, 0, width_mm_ * s_, length_ + extra_bottom_};
}
void paint(QPainter *painter,
@@ -595,6 +684,11 @@ void paint(QPainter *painter,
}
QHash<QUuid, QVector<BridgePoint>> bridges_;
//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<Helpers::CableCell> cable_cells_{cableCells(strip, pattern)};
for (const auto &g_ : geometry(&strip))
{
@@ -793,6 +887,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();
}
@@ -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
@@ -260,24 +264,176 @@ 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
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
{
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
int first{-1}; ///<First terminal of the cable, in the cells
QVector<QString> lines; ///<One line per field, empty when there is no box
qreal bar_2_y{0}; ///<Cross bar the cable ends on
qreal cable_x{0}; ///<Middle of the cable the box hangs on
qreal min_x{0}; ///<Cross bar the box spans
qreal max_x{0};
qreal top{0}; ///<Top of the box, the end of the stub
qreal width{0};
qreal height{0};
qreal bottom() const { return top + height; }
///True when the two boxes would stand the one over the other, the
///way two texts written on the same line reach into each other
///when they are wider than the room between them
bool reachesInto(const TargetBox &other) const {
return cable_x - width / 2 < other.cable_x + other.width / 2
&& cable_x + width / 2 > 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<QString> &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<TargetBox> targetBoxes(const QVector<CableCell> &cells,
const TerminalStripLayoutPattern &pattern,
const QFont &font)
{
QVector<TargetBox> boxes;
const qreal wire_length{pattern.m_cable_wire_length};
if (wire_length <= 0) {
return boxes;
}
const qreal cable_length{qMax<qreal>(0, pattern.m_cable_length)};
const qreal end_length{qMax<qreal>(0, pattern.m_cable_end_length)};
const qreal stub{qMin<qreal>(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<int> 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<QString>();
if (!box.lines.isEmpty())
{
const QFontMetricsF metrics{boxFont(font, box.lines, end_length - stub)};
box.width = qMax<qreal>(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
* 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
@@ -295,24 +451,59 @@ void drawCables(QPainter *painter, const QVector<CableCell> &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();
};
//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<int, TargetBox> 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())
{
@@ -350,25 +541,61 @@ void drawCables(QPainter *painter, const QVector<CableCell> &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))
{
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});
//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 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. 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});
for (const auto &wire : std::as_const(wires))
const qreal stub{qMin<qreal>(6, end_length)};
const TargetBox box{box_of.value(first)};
if (box.lines.isEmpty())
{
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);
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 QFont box_font{boxFont(painter->font(), box.lines,
end_length - stub)};
const QFontMetricsF box_metrics{box_font};
const QRectF box_rect{cable_x - box.width / 2, box.top,
box.width, box.height};
painter->save();
painter->setFont(box_font);
painter->drawRect(box_rect);
for (auto i = 0 ; i < box.lines.size() ; ++i)
{
painter->drawText(QRectF{box_rect.left() + 3,
box_rect.top() + 2 + i * box_metrics.height(),
box_rect.width() - 6, box_metrics.height()},
Qt::AlignCenter,
box.lines.at(i));
}
painter->restore();
}
//The shield, linked to an ellipse around the cable
@@ -439,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<CableCell> cableCells(const AbstractTerminalStripInterface &strip,
const TerminalStripLayoutPattern &pattern)
{
QVector<CableCell> cells;
qreal x_offset{pattern.m_header_rect.width() + pattern.m_spacer_rect.width()};
for (const auto &physical_t : strip.physicalTerminal())
{
const QVector<QSharedPointer<AbstractRealTerminalInterface>> 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<real_t_count ; ++i)
{
const auto index_ = offset_ + i;
if (index_ >= 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.
@@ -865,6 +1139,27 @@ void Helpers::drawTypeSymbol(QPainter *painter, const QRectF &box,
::TerminalStripDrawer::drawTypeSymbol(painter, box, type, led);
}
void Helpers::drawCables(QPainter *painter, const QVector<CableCell> &cells,
const TerminalStripLayoutPattern &pattern) {
::TerminalStripDrawer::drawCables(painter, cells, pattern);
}
qreal Helpers::cableBoxesExtraDepth(const QVector<CableCell> &cells,
const TerminalStripLayoutPattern &pattern)
{
const qreal end_length{qMax<qreal>(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<qreal>(0, extra);
}
/**
* @brief TerminalStripDrawer::paint
* @param painter
@@ -970,7 +1265,12 @@ void TerminalStripDrawer::paint(QPainter *painter)
QRectF xref_rect;
QHash<QUuid, QVector<QPointF>> bridges_anchor_points;
QVector<CableCell> 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<CableCell> cable_cells{cableCells(*m_strip, *m_pattern)};
auto cell_index{0};
m_hovered_xref = hoverTerminal{};
@@ -1028,18 +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 (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};
@@ -1291,7 +1586,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();
}
}
@@ -1340,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<qreal>(0, m_pattern->m_cable_length)
+ std::max<qreal>(0, m_pattern->m_cable_end_length)
+ 4);
+ 4
+ Helpers::cableBoxesExtraDepth(
cableCells(*m_strip, *m_pattern),
*m_pattern));
}
}
@@ -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<QString> cableTarget() const { return QVector<QString>(); }
//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.
@@ -42,6 +42,29 @@ 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<AbstractRealTerminalInterface> 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<CableCell> &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<CableCell> &cells,
const TerminalStripLayoutPattern &pattern);
}
}
@@ -153,6 +153,11 @@ namespace TerminalStripDrawer
return m_real ? m_real->isShield() : false;
}
QVector<QString> TrueRealTerminal::cableTarget() const
{
return m_real ? m_real->cableTarget() : QVector<QString>();
}
QColor TrueRealTerminal::color() const
{
return m_real ? m_real->color() : QColor();
@@ -64,6 +64,7 @@ namespace TerminalStripDrawer
QString cable() const override;
QString cableWire() const override;
bool isShield() const override;
QVector<QString> cableTarget() const override;
QColor color() const override;
QColor stripeColor() const override;
qreal widthMm() const override;
@@ -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<PhysicalTerminal> &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<PhysicalTerminal> &t_) -> QSharedPointer<RealTerminal>
{
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<RealTerminal>();
};
std::stable_sort(m_new_order.begin(), m_new_order.end(),
[&label_of](const QSharedPointer<PhysicalTerminal> &arg1,
[&position_of](const QSharedPointer<PhysicalTerminal> &arg1,
const QSharedPointer<PhysicalTerminal> &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<RealTerminal> &a_,
const QSharedPointer<RealTerminal> &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());
}
@@ -20,10 +20,12 @@
#include <QUndoCommand>
#include <QPointer>
#include <QSharedPointer>
#include <QVector>
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<RealTerminal> &a_,
const QSharedPointer<RealTerminal> &b_);
private:
void sort();
+278 -2
View File
@@ -22,7 +22,56 @@
#include "terminalcolor.h"
#include "terminalcatalog.h"
#include "../qetgraphicsitem/conductor.h"
#include "../qetgraphicsitem/element.h"
#include "../qetgraphicsitem/terminal.h"
#include "../qetinformation.h"
#include "../diagram.h"
#include "../cable/cable.h"
#include "../cable/cablemanager.h"
#include "../qetproject.h"
#include <QHash>
#include <QSet>
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<QPair<QString, QString>> wiredCores(Element *element)
{
QList<QPair<QString, QString>> 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<QString, QString> core(cable->designation(), wire);
if (!cores.contains(core)) {
cores.append(core);
}
}
}
return cores;
}
}
/**
* @brief RealTerminal
@@ -291,18 +340,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();
}
@@ -322,6 +405,199 @@ bool RealTerminal::isShield() const {
return false;
}
/**
* @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 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<QString> 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())
{
//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<QString> lines_;
for (const QString &field_
: text_.split(QLatin1Char('\n'), Qt::SkipEmptyParts)) {
lines_ << field_;
}
return lines_;
}
}
return QVector<QString>();
}
/**
* @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
* 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<QString> RealTerminal::automaticCableTarget() const
{
if (m_spare || m_element.isNull()) {
return QVector<QString>();
}
TerminalStrip *strip_ = parentStrip();
if (!strip_ || !strip_->project()) {
return QVector<QString>();
}
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<QString>();
}
for (Cable *cable_ : project_->cables())
{
if (cable_ && cable_->designation() == name_) {
cable_uuid_ = cable_->uuid();
break;
}
}
if (cable_uuid_.isNull()) {
return QVector<QString>();
}
}
//The own side: this strip's terminals never are the target
QSet<QUuid> 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<QUuid, Element *> 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<QString>();
}
//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<QString> 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
+12
View File
@@ -55,6 +55,11 @@ class RealTerminal
void setPhysicalTerminal(const QSharedPointer<PhysicalTerminal> &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<QString> automaticCableTarget() const;
public:
~RealTerminal();
static QSharedPointer<RealTerminal> createSpare(const ElementData &data, const QUuid &uuid = QUuid());
@@ -73,7 +78,12 @@ class RealTerminal
QString Xref() const;
QString cable() const;
QString cableWire() const;
bool cableIsAutomatic() const;
bool isShield() const;
QVector<QString> 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;
@@ -84,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"); }
+4
View File
@@ -118,6 +118,10 @@ class TerminalStrip : public QObject
void unBridge(const QVector<QSharedPointer<RealTerminal>> &real_terminals);
bool canUnBridge(const QVector <QSharedPointer<RealTerminal>> &real_terminals) const;
QSharedPointer<TerminalStripBridge> isBridged(const QSharedPointer<RealTerminal> 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<TerminalStripBridge> bridgeFor (const QVector<QSharedPointer<RealTerminal>> &terminal_vector) const;
QSharedPointer<RealTerminal> previousTerminalInLevel(const QSharedPointer<RealTerminal> &real_terminal) const;
@@ -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<RealTerminal> &a_,
const QSharedPointer<RealTerminal> &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<RealTerminal> &a_,
const QSharedPointer<RealTerminal> &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);
});
}
@@ -20,11 +20,32 @@
#include <QColor>
#include <QString>
#include <QStringList>
#include "../../qetgraphicsitem/element.h"
#include "../realterminal.h"
struct modelRealTerminalData
{
///The Installation, Location and Label (BMK) of the piece the cable
///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<QString> &target_)
{
QStringList fields_;
for (const QString &field_ : target_)
{
if (!field_.isEmpty()) {
fields_ << field_;
}
}
return fields_.join(QLatin1Char('\n'));
}
static modelRealTerminalData data(const QSharedPointer<RealTerminal> &real_t)
{
modelRealTerminalData mrtd;
@@ -35,6 +56,9 @@ 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();
mrtd.shield_ = real_t->isShield();
@@ -60,6 +84,12 @@ 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;
bool shield_ = false;
@@ -35,6 +35,7 @@
#include "../terminalcatalog.h"
#include "../realterminal.h"
#include "../../qetinformation.h"
#include "../../cable/cable.h"
#include <QApplication>
#include <QCheckBox>
@@ -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_
@@ -165,6 +249,18 @@ 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);
//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);
}
@@ -245,8 +341,19 @@ 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);
}
//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
set_info(RealTerminal::colorInfoKey(), TerminalColor::toSpec(data_.color_, data_.stripe_color_));
@@ -701,7 +808,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)
{
@@ -709,6 +817,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;
@@ -779,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'));
}
@@ -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<TerminalStripModel::Column, QVector<modelRealTerminalData>> singleColumnData() const;
@@ -79,6 +83,9 @@ class TerminalStripEditor : public QWidget
private:
Ui::TerminalStripEditor *ui;
QPointer<QETProject> m_project;
//The project whose cables this editor follows, which is not
//always m_project yet when watchCableChanges() runs again
QPointer<QETProject> m_watched_project;
QPointer<TerminalStrip> m_current_strip;
TerminalStripModel *m_model {nullptr};
};
+212 -106
View File
@@ -33,6 +33,9 @@
#include <QSpinBox>
#include <QPainter>
#include <QApplication>
#include <QKeyEvent>
#include <QPlainTextEdit>
#include <QFrame>
/**
* Some const int who describe what a column contain
@@ -48,16 +51,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<bool> UNMODIFIED_CELL_VECTOR(COLUMN_COUNT, false);
@@ -132,14 +136,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;
}
}
@@ -182,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
@@ -215,6 +215,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 +236,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();
@@ -267,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_) &&
@@ -302,6 +305,18 @@ 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_ == CABLE_TARGET_CELL
&& mrtd.cable_target_auto_) {
return false;
}
if (column_ == LEVEL_CELL &&
role == Qt::EditRole)
{
@@ -353,6 +368,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();
@@ -476,6 +499,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");
@@ -488,6 +512,14 @@ 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, 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();
}
@@ -497,8 +529,18 @@ 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;
//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 == 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) {
@@ -589,7 +631,7 @@ QVector<modelRealTerminalData> TerminalStripModel::modelRealTerminalDataForIndex
for (auto i : set_) {
const auto rtd_ = realDataAtIndex(i);
if (rtd_.level_ > -1) {
vector_.append(realDataAtIndex(i));
vector_.append(rtd_);
}
}
@@ -697,6 +739,8 @@ void TerminalStripModel::reload()
void TerminalStripModel::fillPhysicalTerminalData()
{
m_row_index.clear();
//Get all physical terminal
if (m_terminal_strip)
{
@@ -706,41 +750,32 @@ void TerminalStripModel::fillPhysicalTerminalData()
mptd.pos_ = phy_t->pos();
mptd.uuid_ = phy_t->uuid();
const auto physical_index = static_cast<int>(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<int>(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_);
}
/**
@@ -751,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<physical_data.real_data.count() ; ++i)
{
if (current_row == row) {
auto physical_data = m_physical_data.at(current_physical);
physical_data.real_data.replace(current_real, data);
m_physical_data.replace(current_physical, physical_data);
return;
} else {
++current_real;
++current_row;
}
}
++current_physical;
}
}
const auto &place_ = m_row_index.at(row);
m_physical_data[place_.physical_].real_data.replace(place_.real_, data);
}
/**
@@ -794,26 +809,17 @@ modelPhysicalTerminalData TerminalStripModel::physicalDataAtIndex(int index) con
return modelPhysicalTerminalData();
}
int current_checked_index = -1;
int current_phy = -1;
bool match_ = false;
for (const auto &ptd_ : std::as_const(m_physical_data))
{
current_checked_index += ptd_.real_data.size();
++current_phy;
if (current_checked_index >= 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_);
}
/**
@@ -823,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<RealTerminal> &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
@@ -853,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
@@ -891,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<RealTerminal>();
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) {
@@ -918,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;
@@ -928,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<RealTerminal>();
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) {
@@ -953,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;
@@ -1086,10 +1124,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<TerminalStripModelDelegate *>(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<QKeyEvent *>(event);
if ((key_event->key() == Qt::Key_Return ||
key_event->key() == Qt::Key_Enter) &&
key_event->modifiers().testFlag(Qt::ControlModifier))
{
auto editor_ = qobject_cast<QWidget *>(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() &&
@@ -1172,6 +1259,17 @@ void TerminalStripModelDelegate::setEditorData(QWidget *editor, const QModelInde
return;
}
if (index.isValid() &&
editor->objectName() == QLatin1String("terminal_cable_target"))
{
if (auto te = dynamic_cast<QPlainTextEdit *>(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);
}
@@ -1212,6 +1310,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<QPlainTextEdit *>(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);
}
+24 -8
View File
@@ -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
};
@@ -106,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<TerminalStrip> m_terminal_strip;
//Modified cells of each real terminal (spare terminals have no element)
QHash<const RealTerminal *, QVector<bool>> m_modified_cell;
QVector<modelPhysicalTerminalData> m_physical_data;
//One RowPlace by row, filled with m_physical_data (see fillPhysicalTerminalData)
QVector<RowPlace> 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_,
@@ -140,6 +155,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;
+3 -2
View File
@@ -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)
{
+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"