diff --git a/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutpattern.h b/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutpattern.h index 4dc941601..9a17f1df3 100644 --- a/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutpattern.h +++ b/sources/TerminalStrip/GraphicsItem/properties/terminalstriplayoutpattern.h @@ -127,8 +127,11 @@ class TerminalStripLayoutPattern qreal m_cable_wire_length{40}; //Length of the cable qreal m_cable_length{60}; - //Length of the wires after the cable - qreal m_cable_end_length{40}; + //Length of the wires after the cable. It leaves the room the box + //naming where the cable goes needs: three lines (Installation, + //Location, Label) at the default font of 15 px, so the box does + //not have to shrink its text down. + qreal m_cable_end_length{80}; qreal m_bridge_point_d{5}; QVector m_bridge_point_y_offset{50,70,90,110}; diff --git a/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.cpp b/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.cpp index 8b145aef2..26e68688c 100644 --- a/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.cpp +++ b/sources/TerminalStrip/GraphicsItem/realisticterminalstripdrawer.cpp @@ -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 cableCells(const AbstractTerminalStripInterface &strip, + const TerminalStripLayoutPattern &pattern) + { + const qreal s_{pattern.m_realistic_px_per_mm}; + const qreal length_{pattern.m_realistic_length_mm * s_}; + + //Left end stop and strip name : the terminals start after them + qreal x_{endStopWidthMm(pattern) * s_}; + if (pattern.m_realistic_group_marker) { + x_ += group_marker_width_mm * s_; + } + + QVector cells; + for (const auto &g_ : geometry(&strip)) + { + const qreal w_{g_.width_mm * s_}; + const int levels_{static_cast(g_.reals.size())}; + + //One wire exit by level, spread over the width of the body + for (int k = 0 ; k < levels_ ; ++k) + { + const auto &real_t{g_.reals.at(k)}; + if (!real_t || real_t->cable().isEmpty()) { + continue; + } + Helpers::CableCell cell_; + cell_.x = x_ + w_ * (k + 0.5) / levels_; + cell_.bottom = length_; + cell_.cable = real_t->cable(); + cell_.wire = real_t->cableWire(); + cell_.shield = real_t->isShield(); + cell_.terminal = real_t; + cells.append(cell_); + } + + x_ += w_; + if (g_.separator_after) { + x_ += separator_width_mm * s_; + } + } + return cells; + } + } //End anonymous namespace QRectF boundingRect(const AbstractTerminalStripInterface *strip, @@ -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(0, pattern.m_cable_length) + + std::max(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> 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 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(); } diff --git a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp index f348bee7c..06877d9bb 100644 --- a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp +++ b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.cpp @@ -29,6 +29,10 @@ namespace TerminalStripDrawer { + //The cells of a cable drawing live with the helpers, so both drawing + //styles build the same thing and pass it to Helpers::drawCables() +using Helpers::CableCell; + /** * @brief TerminalStripDrawer::TerminalStripDrawer * @param strip @@ -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}; /// lines; /// other.cable_x - other.width / 2 + && top < other.bottom() && bottom() > other.top; + } }; +/** + * @brief boxFont + * The font of a box naming where a cable goes : the font of the drawing, + * shrunk only while the lines of the target do not fit the height left + * for the box. The width never shrinks anything -- a line too long for + * the cross bar gives a wider box, it never gives a smaller text. + * @param room : the height left for the box + * @return the font the lines are written with + */ +QFont boxFont(const QFont &font, const QVector &lines, qreal room) +{ + QFont box_font{font}; + if (box_font.pixelSize() <= 0 && box_font.pointSizeF() > 0) { + box_font.setPixelSize(qMax(4, qRound(box_font.pointSizeF() * 96.0 / 72.0))); + } + const auto fits_the_height = [&lines, room](const QFont &font_) { + return QFontMetricsF{font_}.height() * lines.size() + 4 <= room; + }; + while (!fits_the_height(box_font) && box_font.pixelSize() > 4) { + box_font.setPixelSize(box_font.pixelSize() - 1); + } + return box_font; +} + +/** + * @brief targetBoxes + * Where the boxes naming where the cables go stand, and how big they + * are. Every box is measured first -- the lines of the target at the + * font of the drawing, a width of the widest line or of the cross bar + * the cable ends on, whichever is wider -- and they are then placed + * from the left to the right : a box which would reach into one + * already placed is set just low enough to stand clear of it, its stub + * drawn longer. Two cables of one strip therefore never write their + * boxes the one over the other, and no text is ever cut down to fit. + * @return one entry per cable which has something to name at its end + */ +QVector targetBoxes(const QVector &cells, + const TerminalStripLayoutPattern &pattern, + const QFont &font) +{ + QVector boxes; + const qreal wire_length{pattern.m_cable_wire_length}; + if (wire_length <= 0) { + return boxes; + } + const qreal cable_length{qMax(0, pattern.m_cable_length)}; + const qreal end_length{qMax(0, pattern.m_cable_end_length)}; + const qreal stub{qMin(6, end_length)}; + if (end_length - stub <= 4) { + return boxes; + } + constexpr qreal box_gap{2}; + + auto first{0}; + while (first < cells.size()) + { + if (cells.at(first).cable.isEmpty()) { + ++first; + continue; + } + + //Find the last terminal of the cable, the same groups the + //drawing makes + auto last{first}; + while (last + 1 < cells.size() && + cells.at(last + 1).cable == cells.at(first).cable) { + ++last; + } + + QVector wires, shields; + qreal max_bottom{cells.at(first).bottom}; + for (auto i = first ; i <= last ; ++i) + { + max_bottom = qMax(max_bottom, cells.at(i).bottom); + if (cells.at(i).shield) { + shields << i; + } else { + wires << i; + } + } + //A cable with only a shield is drawn like a cable with only one wire + if (wires.isEmpty()) { + wires = shields; + } + + const auto &first_wire{cells.at(wires.first())}; + TargetBox box; + box.first = first; + box.bar_2_y = max_bottom + wire_length + cable_length; + box.min_x = first_wire.x; + box.max_x = cells.at(wires.last()).x; + box.cable_x = (box.min_x + box.max_x) / 2; + box.lines = first_wire.terminal ? first_wire.terminal->cableTarget() + : QVector(); + + if (!box.lines.isEmpty()) + { + const QFontMetricsF metrics{boxFont(font, box.lines, end_length - stub)}; + box.width = qMax(box.max_x - box.min_x, 16); + for (const QString &line_ : std::as_const(box.lines)) { + box.width = qMax(box.width, metrics.horizontalAdvance(line_) + 6); + } + box.height = metrics.height() * box.lines.size() + 4; + box.top = box.bar_2_y + stub; + boxes << box; + } + + first = last + 1; + } + + //Place them : a box reaching into one already placed goes just + //under it, and the boxes set aside settle down again with it + for (auto i = 0 ; i < boxes.size() ; ++i) + { + bool settled{false}; + for (auto guard = 0 ; !settled && guard < boxes.size() + 2 ; ++guard) + { + settled = true; + for (auto j = 0 ; j < i ; ++j) + { + if (boxes.at(i).reachesInto(boxes.at(j)) + && boxes.at(i).top < boxes.at(j).bottom() + box_gap) + { + boxes[i].top = boxes.at(j).bottom() + box_gap; + settled = false; + } + } + } + } + return boxes; +} + /** * @brief drawCables * Draw the cables under the terminals. Consecutive terminals who have the same cable name - * 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 &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 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 &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(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 cableCells(const AbstractTerminalStripInterface &strip, + const TerminalStripLayoutPattern &pattern) +{ + QVector cells; + qreal x_offset{pattern.m_header_rect.width() + pattern.m_spacer_rect.width()}; + for (const auto &physical_t : strip.physicalTerminal()) + { + const QVector> real_terminal_vector{physical_t->realTerminals()}; + const auto real_t_count{real_terminal_vector.size()}; + const auto offset_{4 - real_t_count}; + + for (auto i=0 ; i= 4) { + break; + } + + const auto terminal_rect{terminalRect(pattern, index_, + real_terminal_vector[i])}; + CableCell cell; + cell.x = x_offset + terminal_rect.width()/2; + cell.bottom = terminal_rect.y() + terminal_rect.height(); + if (real_terminal_vector[i]) + { + cell.cable = real_terminal_vector[i]->cable(); + cell.wire = real_terminal_vector[i]->cableWire(); + cell.shield = real_terminal_vector[i]->isShield(); + cell.terminal = real_terminal_vector[i]; + } + cells.append(cell); + x_offset += terminal_rect.width(); + } + } + return cells; +} + /** * @brief The TerminalColors struct * Colors used to fill the rectangle of a terminal. @@ -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 &cells, + const TerminalStripLayoutPattern &pattern) { + ::TerminalStripDrawer::drawCables(painter, cells, pattern); +} + +qreal Helpers::cableBoxesExtraDepth(const QVector &cells, + const TerminalStripLayoutPattern &pattern) +{ + const qreal end_length{qMax(0, pattern.m_cable_end_length)}; + qreal extra{0}; + for (const auto &box_ : ::TerminalStripDrawer::targetBoxes(cells, pattern, + pattern.font())) + { + //How much deeper than the end zone the strip reserves this box + //stands -- the zone is the room of the box, a box set lower + //than the others needs room the strip must reserve too + extra = qMax(extra, box_.bottom() - (box_.bar_2_y + end_length)); + } + return qMax(0, extra); +} + /** * @brief TerminalStripDrawer::paint * @param painter @@ -970,7 +1265,12 @@ void TerminalStripDrawer::paint(QPainter *painter) QRectF xref_rect; QHash> bridges_anchor_points; - QVector cable_cells; + //The cells the cables are drawn from, worked out once for the + //whole strip: the depth the strip reserves under its terminals + //is built on the same list (boundingRect()), so a box set lower + //than the others is never cut off + QVector cable_cells{cableCells(*m_strip, *m_pattern)}; + auto cell_index{0}; m_hovered_xref = hoverTerminal{}; @@ -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(0, m_pattern->m_cable_length) + std::max(0, m_pattern->m_cable_end_length) - + 4); + + 4 + + Helpers::cableBoxesExtraDepth( + cableCells(*m_strip, *m_pattern), + *m_pattern)); } } diff --git a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.h b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.h index 5588092fd..db080d234 100644 --- a/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.h +++ b/sources/TerminalStrip/GraphicsItem/terminalstripdrawer.h @@ -78,6 +78,12 @@ namespace TerminalStripDrawer virtual QString cable() const { return QString(); } virtual QString cableWire() const { return QString(); } virtual bool isShield() const { return false; } + //Installation, Location and Label (BMK) of the piece the cable + //goes to at its far end -- the same three fields the cable list + //shows for a target -- drawn in a box under the cable. Empty + //when the terminal has no cable, or when the cable lands on + //nothing past the strip's own terminals. + virtual QVector cableTarget() const { return QVector(); } //Color and width (mm) of the terminal, only used to draw the terminal. //An invalid color means "no override" : the default color of the type is used. //A width <= 0 means "unknown" : the default width of the layout is used. diff --git a/sources/TerminalStrip/GraphicsItem/terminalstripdrawerhelpers.h b/sources/TerminalStrip/GraphicsItem/terminalstripdrawerhelpers.h index 7cbafd5ea..d39bc76b6 100644 --- a/sources/TerminalStrip/GraphicsItem/terminalstripdrawerhelpers.h +++ b/sources/TerminalStrip/GraphicsItem/terminalstripdrawerhelpers.h @@ -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 terminal; ///< to ask where the cable goes + }; + //Draw the cables under the terminals, whichever style draws them : + //consecutive terminals with the same cable share one cable, which ends + //together in a short stub and a box naming where it goes (see drawCables() + //in terminalstripdrawer.cpp for the whole drawing) + void drawCables(QPainter *painter, const QVector &cells, + const TerminalStripLayoutPattern &pattern); + //How much deeper than the end zone (m_cable_end_length) the boxes of + //the cables need: a box reaching into another one is set lower than + //the others, and the strip must reserve the room for it or the lower + //box is cut off. The same rule the drawing uses, worked out of the + //same cells, so the two can never disagree. + qreal cableBoxesExtraDepth(const QVector &cells, + const TerminalStripLayoutPattern &pattern); } } diff --git a/sources/TerminalStrip/GraphicsItem/trueterminalstrip.cpp b/sources/TerminalStrip/GraphicsItem/trueterminalstrip.cpp index d5248fc2e..8a0199ab3 100644 --- a/sources/TerminalStrip/GraphicsItem/trueterminalstrip.cpp +++ b/sources/TerminalStrip/GraphicsItem/trueterminalstrip.cpp @@ -153,6 +153,11 @@ namespace TerminalStripDrawer return m_real ? m_real->isShield() : false; } + QVector TrueRealTerminal::cableTarget() const + { + return m_real ? m_real->cableTarget() : QVector(); + } + QColor TrueRealTerminal::color() const { return m_real ? m_real->color() : QColor(); diff --git a/sources/TerminalStrip/GraphicsItem/trueterminalstrip.h b/sources/TerminalStrip/GraphicsItem/trueterminalstrip.h index abde6dbd2..ac20d2bc8 100644 --- a/sources/TerminalStrip/GraphicsItem/trueterminalstrip.h +++ b/sources/TerminalStrip/GraphicsItem/trueterminalstrip.h @@ -64,6 +64,7 @@ namespace TerminalStripDrawer QString cable() const override; QString cableWire() const override; bool isShield() const override; + QVector cableTarget() const override; QColor color() const override; QColor stripeColor() const override; qreal widthMm() const override; diff --git a/sources/TerminalStrip/UndoCommand/sortterminalstripcommand.cpp b/sources/TerminalStrip/UndoCommand/sortterminalstripcommand.cpp index 898ec8973..ce54bfb94 100644 --- a/sources/TerminalStrip/UndoCommand/sortterminalstripcommand.cpp +++ b/sources/TerminalStrip/UndoCommand/sortterminalstripcommand.cpp @@ -19,6 +19,9 @@ #include "../terminalstrip.h" #include "../physicalterminal.h" #include "../realterminal.h" +#include "../../diagram.h" +#include "../../positionorder.h" +#include "../../qetgraphicsitem/element.h" #include "../../utils/qetutils.h" SortTerminalStripCommand::SortTerminalStripCommand(TerminalStrip *strip, QUndoCommand *parent) : @@ -47,16 +50,73 @@ void SortTerminalStripCommand::redo() void SortTerminalStripCommand::sort() { - auto label_of = [](const QSharedPointer &t) -> QString + //The plan position of a physical terminal is the position of the + //level that sits on a folio; a terminal whose levels are all + //spares has no position and is ordered by label only. + auto position_of = [](const QSharedPointer &t_) -> QSharedPointer { - return t->realTerminalCount() ? t->realTerminals().constLast()->label() - : QString(); + if (!t_) { + return {}; + } + for (const auto &real_ : t_->realTerminals()) { + if (real_ && real_->element()) { + return real_; + } + } + return t_->realTerminalCount() ? t_->realTerminals().constFirst() + : QSharedPointer(); }; std::stable_sort(m_new_order.begin(), m_new_order.end(), - [&label_of](const QSharedPointer &arg1, + [&position_of](const QSharedPointer &arg1, const QSharedPointer &arg2) { - return QETUtils::naturalLessThan(label_of(arg1), label_of(arg2)); + return planOrderLessThan(position_of(arg1), position_of(arg2)); }); } + +/** + @brief SortTerminalStripCommand::planOrderLessThan + Order two real terminals the way the plan is drawn : folio by folio, + then left to right. Terminals with no folio position (spares) come + last, and the label only breaks ties, so the result stays stable and + predictable whatever the labels look like ("X1:N" and "X1:PE" stand + where they are drawn, not at the end). + Both the auto import and sort() use this, so the two orders of the + same strip can never fight each other. + @a a_ terminal to compare + @a b_ other terminal to compare + @return true if @a a_ comes before @a b_ +*/ +bool SortTerminalStripCommand::planOrderLessThan( + const QSharedPointer &a_, + const QSharedPointer &b_) +{ + if (!a_ || !b_) { + return a_ && !b_; //a missing terminal goes last + } + + const Element *ea = a_->element(); + const Element *eb = b_->element(); + auto folio_of = [](const Element *e_) -> int + { + return (e_ && e_->diagram()) ? e_->diagram()->folioIndex() : -1; + }; + + const int fa = folio_of(ea); + const int fb = folio_of(eb); + if (fa != fb) { + //A terminal with no folio has no place in the plan order + if (fa < 0) { + return false; + } + if (fb < 0) { + return true; + } + return fa < fb; + } + if (fa >= 0 && ea->pos() != eb->pos()) { + return PositionOrder::roundedXThenY(ea->pos(), eb->pos()); + } + return QETUtils::naturalLessThan(a_->label(), b_->label()); +} diff --git a/sources/TerminalStrip/UndoCommand/sortterminalstripcommand.h b/sources/TerminalStrip/UndoCommand/sortterminalstripcommand.h index eae51e782..514fa90bc 100644 --- a/sources/TerminalStrip/UndoCommand/sortterminalstripcommand.h +++ b/sources/TerminalStrip/UndoCommand/sortterminalstripcommand.h @@ -20,10 +20,12 @@ #include #include +#include #include class TerminalStrip; class PhysicalTerminal; +class RealTerminal; /** * @brief The SortTerminalStripCommand class @@ -38,6 +40,25 @@ class SortTerminalStripCommand : public QUndoCommand void undo() override; void redo() override; + /** + * @brief planOrderLessThan + * Order two real terminals the way the plan is drawn : folio by + * folio, then left to right (top to bottom only breaks ties + * within a column). A spare has no folio and comes last, by + * label, and the label breaks every remaining tie (two + * terminals at the very same spot), which keeps the result + * stable. + * + * The auto import sorts with it too, so the order it works out + * cannot be rewritten by the SortTerminalStripCommand it then + * pushes for the strip it just created. + * @param a_ : first terminal + * @param b_ : second terminal + * @return true if @a a_ comes before @a b_ + */ + static bool planOrderLessThan(const QSharedPointer &a_, + const QSharedPointer &b_); + private: void sort(); diff --git a/sources/TerminalStrip/realterminal.cpp b/sources/TerminalStrip/realterminal.cpp index 3ec7ad462..92b367fb3 100644 --- a/sources/TerminalStrip/realterminal.cpp +++ b/sources/TerminalStrip/realterminal.cpp @@ -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 +#include + +namespace +{ + /** + * The cable cores wired to one terminal element, worked out from + * the cables of the project (PR #1438). A core knows its + * conductor, and CableManager::refreshLabels() writes the cable + * and the core into the properties of that conductor, so reading + * the conductors of the element is enough. + * Each pair is (cable designation, core colour or number). + */ + QList> wiredCores(Element *element) + { + QList> cores; + if (!element || !element->diagram()) { + return cores; + } + const auto project = element->diagram()->project(); + for (const auto terminal : element->terminals()) + { + for (const auto conductor : terminal->conductors()) + { + const auto properties = conductor->ownProperties(); + const auto cable = CableManager::cableByUuid(project, properties.m_cable_uuid); + if (!cable || cable->designation().isEmpty()) { + continue; + } + auto wire = cable->colorOfCore(properties.m_cable_slot); + if (wire.isEmpty() && properties.m_cable_slot >= 0) { + wire = QString::number(properties.m_cable_slot + 1); + } + const QPair core(cable->designation(), wire); + if (!cores.contains(core)) { + cores.append(core); + } + } + } + return cores; + } +} /** * @brief RealTerminal @@ -291,18 +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 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 lines_; + for (const QString &field_ + : text_.split(QLatin1Char('\n'), Qt::SkipEmptyParts)) { + lines_ << field_; + } + return lines_; + } + } + return QVector(); +} + +/** + * @brief RealTerminal::cableTargetIsAutomatic + * @return true when the target of this terminal is worked out from the + * project -- one of its cables lands on a piece worth naming -- and + * false when only a text typed in by hand is there to show. + */ +bool RealTerminal::cableTargetIsAutomatic() const +{ + return !automaticCableTarget().isEmpty(); +} + +/** + * @brief RealTerminal::automaticCableTarget + * Where the cable of this terminal goes at its far end: the + * Installation, Location and Label (BMK) of the first piece the + * wires of the cable land on, past the strip's own terminals, in + * plan order -- the same three fields the cable list shows for a + * target, in the same order. An empty field is left out; a terminal + * with no cable, or a cable which lands on nothing else, gives no + * lines at all. + * + * The cable is the one the wires drawn to this terminal carry; a + * cable name typed by hand, with no wire behind it, is looked up by + * its name among the cables of the project. + * @return the lines to write in the box under the cable + */ +QVector RealTerminal::automaticCableTarget() const +{ + if (m_spare || m_element.isNull()) { + return QVector(); + } + TerminalStrip *strip_ = parentStrip(); + if (!strip_ || !strip_->project()) { + return QVector(); + } + QETProject *project_{strip_->project()}; + + //The cable the wires drawn to this terminal are part of + QUuid cable_uuid_; + for (Terminal *terminal_ : m_element->terminals()) + { + if (!terminal_) { + continue; + } + for (Conductor *conductor_ : terminal_->conductors()) + { + if (conductor_ && !conductor_->properties().m_cable_uuid.isNull()) { + cable_uuid_ = conductor_->properties().m_cable_uuid; + break; + } + } + if (!cable_uuid_.isNull()) { + break; + } + } + if (cable_uuid_.isNull()) + { + //A cable name typed by hand, no wire behind it: the first + //cable of the project drawn under that name + const auto name_{cable()}; + if (name_.isEmpty()) { + return QVector(); + } + for (Cable *cable_ : project_->cables()) + { + if (cable_ && cable_->designation() == name_) { + cable_uuid_ = cable_->uuid(); + break; + } + } + if (cable_uuid_.isNull()) { + return QVector(); + } + } + + //The own side: this strip's terminals never are the target + QSet own_; + for (const auto &physical_ : strip_->physicalTerminal()) + { + if (!physical_) { + continue; + } + for (const auto &real_ : physical_->realTerminals()) { + if (real_ && real_->element()) { + own_.insert(real_->element()->uuid()); + } + } + } + + //Where the wires of the cable land: one piece whatever the + //number of its cores, never a report (the cable continues on + //the sheet the report points to) and never the strip's own + //terminals. Same rule as the cable list (cablelistrows.cpp). + QHash landed_; + for (Diagram *diagram_ : project_->diagrams()) + { + for (Conductor *conductor_ : diagram_->conductors()) + { + if (!conductor_ || conductor_->properties().m_cable_uuid != cable_uuid_) { + continue; + } + Terminal *ends_[2] = {conductor_->terminal1, conductor_->terminal2}; + for (Terminal *terminal_ : ends_) + { + if (!terminal_) { + continue; + } + Element *element_ = terminal_->parentElement(); + if (!element_ || (element_->linkType() & Element::AllReport)) { + continue; + } + if (own_.contains(element_->uuid())) { + continue; + } + landed_.insert(element_->uuid(), element_); + } + } + } + if (landed_.isEmpty()) { + return QVector(); + } + + //The first piece in plan order is the target, as the cable + //list chooses it + Element *target_{nullptr}; + for (Element *element_ : std::as_const(landed_)) { + if (!target_ || comparPos(element_, target_)) { + target_ = element_; + } + } + + const DiagramContext informations_{target_->elementInformations()}; + const auto installation_{informations_.value(QETInformation::ELMT_PLANT).toString()}; + const auto location_{informations_.value(QETInformation::ELMT_LOCATION).toString()}; + const auto label_{informations_.value(QETInformation::ELMT_LABEL).toString()}; + + QVector lines_; + if (!installation_.isEmpty()) { + lines_ << installation_; + } + if (!location_.isEmpty()) { + lines_ << location_; + } + if (!label_.isEmpty()) { + lines_ << label_; + } + return lines_; +} + /** * @brief RealTerminal::color * @return The color set by the user for this terminal, drawn in the diff --git a/sources/TerminalStrip/realterminal.h b/sources/TerminalStrip/realterminal.h index bf8d39bbd..61f44295a 100644 --- a/sources/TerminalStrip/realterminal.h +++ b/sources/TerminalStrip/realterminal.h @@ -55,6 +55,11 @@ class RealTerminal void setPhysicalTerminal(const QSharedPointer &phy_t); + ///Where the wires of a cable land, worked out from the project: + ///empty when no cable lands on a piece worth naming (cableTarget() + ///then falls back on the text typed in by hand). + QVector automaticCableTarget() const; + public: ~RealTerminal(); static QSharedPointer createSpare(const ElementData &data, const QUuid &uuid = QUuid()); @@ -73,7 +78,12 @@ class RealTerminal QString Xref() const; QString cable() const; QString cableWire() const; + bool cableIsAutomatic() const; bool isShield() const; + QVector cableTarget() const; + ///True when cableTarget() is worked out from the project (read only), + ///false when it is the text typed in by hand (which may be edited) + bool cableTargetIsAutomatic() const; QColor color() const; QColor stripeColor() const; qreal section() const; @@ -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"); } diff --git a/sources/TerminalStrip/terminalstrip.h b/sources/TerminalStrip/terminalstrip.h index 313f13ee0..a3b13cc9d 100644 --- a/sources/TerminalStrip/terminalstrip.h +++ b/sources/TerminalStrip/terminalstrip.h @@ -118,6 +118,10 @@ class TerminalStrip : public QObject void unBridge(const QVector> &real_terminals); bool canUnBridge(const QVector > &real_terminals) const; QSharedPointer isBridged(const QSharedPointer real_terminal) const; + ///True when at least one bridge joins terminals of this strip : + ///the bridge cells of the terminal strip editor are the only thing + ///drawn from them (see TerminalStripModel::bridgePixmapFor) + bool hasBridges() const { return !m_bridge.isEmpty(); } QSharedPointer bridgeFor (const QVector> &terminal_vector) const; QSharedPointer previousTerminalInLevel(const QSharedPointer &real_terminal) const; diff --git a/sources/TerminalStrip/terminalstripautoimport.cpp b/sources/TerminalStrip/terminalstripautoimport.cpp index 6e9508dda..808c7b2d8 100644 --- a/sources/TerminalStrip/terminalstripautoimport.cpp +++ b/sources/TerminalStrip/terminalstripautoimport.cpp @@ -128,7 +128,9 @@ namespace * whose label prefix or plant and location no longer match its strip. * * In both cases the order the user gave to a strip is never changed, - * only a strip created by the run itself is sorted. + * only a strip created by the run itself is sorted -- the way the plan + * is drawn : folio by folio, then left to right (see + * SortTerminalStripCommand::planOrderLessThan). */ TerminalStripAutoImport::ImportResult TerminalStripAutoImport::importFreeTerminals( QETProject *project, bool move_placed) @@ -187,16 +189,21 @@ TerminalStripAutoImport::ImportResult TerminalStripAutoImport::importFreeTermina for (auto it_ = groups_.begin(); it_ != groups_.end(); ++it_) { - auto by_label_ = [](const QSharedPointer &a_, - const QSharedPointer &b_) { - return QETUtils::naturalLessThan(a_->label(), b_->label()); + //The plan order : folio by folio, then left to right, so the + //strip comes out the way the plan is drawn instead of label + //by label ("N" and "PE" stay where they are on the sheet). + //SortTerminalStripCommand, pushed below for a strip this run + //creates, sorts with the same rule and cannot undo it. + auto by_plan_ = [](const QSharedPointer &a_, + const QSharedPointer &b_) { + return SortTerminalStripCommand::planOrderLessThan(a_, b_); }; std::sort(it_.value().free_terminals.begin(), - it_.value().free_terminals.end(), by_label_); + it_.value().free_terminals.end(), by_plan_); std::sort(it_.value().placed_terminals.begin(), it_.value().placed_terminals.end(), - [&by_label_](const auto &a_, const auto &b_) { - return by_label_(a_.first, b_.first); + [&by_plan_](const auto &a_, const auto &b_) { + return by_plan_(a_.first, b_.first); }); } diff --git a/sources/TerminalStrip/ui/modelTerminalData.h b/sources/TerminalStrip/ui/modelTerminalData.h index 2921b33af..ff14f21de 100644 --- a/sources/TerminalStrip/ui/modelTerminalData.h +++ b/sources/TerminalStrip/ui/modelTerminalData.h @@ -20,11 +20,32 @@ #include #include +#include #include "../../qetgraphicsitem/element.h" #include "../realterminal.h" struct modelRealTerminalData { + ///The Installation, Location and Label (BMK) of the piece the cable + ///lands on, one line by field : what the strip draws in the box + ///under its cable (RealTerminal::cableTarget) and what the Target + ///cell shows, both read line by line. A "/" written inside a field + ///is part of that field, never a separator. + ///A target typed in by hand is kept as it was written, and nothing + ///is shown when the cable lands on nothing worth naming and no text + ///was ever written in. + static QString targetText(const QVector &target_) + { + QStringList fields_; + for (const QString &field_ : target_) + { + if (!field_.isEmpty()) { + fields_ << field_; + } + } + return fields_.join(QLatin1Char('\n')); + } + static modelRealTerminalData data(const QSharedPointer &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; diff --git a/sources/TerminalStrip/ui/terminalstripeditor.cpp b/sources/TerminalStrip/ui/terminalstripeditor.cpp index 3d748fc69..8b714fa0c 100644 --- a/sources/TerminalStrip/ui/terminalstripeditor.cpp +++ b/sources/TerminalStrip/ui/terminalstripeditor.cpp @@ -35,6 +35,7 @@ #include "../terminalcatalog.h" #include "../realterminal.h" #include "../../qetinformation.h" +#include "../../cable/cable.h" #include #include @@ -95,6 +96,10 @@ TerminalStripEditor::TerminalStripEditor(QETProject *project, QWidget *parent) : } } }); + + //The cable cells are worked out from the cables of the project, + //so the table follows them while the user wires the drawing + watchCableChanges(); } /** @@ -107,9 +112,88 @@ TerminalStripEditor::~TerminalStripEditor() { void TerminalStripEditor::setProject(QETProject *project) { m_project = project; + watchCableChanges(); setCurrentStrip(nullptr); } +/** + * @brief TerminalStripEditor::watchCableChanges + * Follow the cables of the project: the cable cells are worked out from + * them, so a cable renamed, given cores or taken away changes what the + * cells say, and the window stays open while the user wires the drawing. + * The project the editor followed before is let go first, since the + * window may be shown on another project. + */ +void TerminalStripEditor::watchCableChanges() +{ + if (m_watched_project) { + //Only the connections made here go to the project + m_watched_project->disconnect(nullptr, this); + for (Cable *cable : m_watched_project->cables()) { + cable->disconnect(nullptr, this); + } + } + m_watched_project = m_project; + if (!m_project) { + return; + } + + connect(m_project, &QETProject::cableAdded, this, [this](Cable *cable) { + watchCable(cable); + }); + connect(m_project, &QETProject::cableRemoved, this, [this](Cable *cable) { + //The project stopped feeding its labels already, so the + //cells show what is left: the typed in value, or nothing + if (cable) { + cable->disconnect(nullptr, this); + } + reloadOnCableChange(); + }); + for (Cable *cable : m_project->cables()) { + watchCable(cable); + } +} + +/** + * @brief TerminalStripEditor::watchCable + * Reload the table when @a cable changes. The project runs its own + * connection first (made when the cable entered it), so the labels of + * the conductors are worked out again before this reload reads them. + * @param cable + */ +void TerminalStripEditor::watchCable(Cable *cable) +{ + if (!cable) { + return; + } + cable->disconnect(nullptr, this); + connect(cable, &Cable::changed, this, &TerminalStripEditor::reloadOnCableChange); +} + +/** + * @brief TerminalStripEditor::reloadOnCableChange + * Take up a change of the cables: reload so the cable cells say what + * they now must, never at the price of the user's own work -- cell + * edits not applied yet, or a cell being typed in, are kept and catch + * up with the next reload of any kind. + */ +void TerminalStripEditor::reloadOnCableChange() +{ + if (!m_current_strip || !m_model) { + return; + } + if (!m_model->modifiedmodelRealTerminalData().isEmpty()) { + return; + } + //A cell being typed in keeps its editor: the reload would close + //it under the fingers of the user (the edit itself is picked up + //by the next reload once it has been made) + if (ui->m_table_widget->isAncestorOf(QApplication::focusWidget())) { + return; + } + reload(); +} + /** * @brief TerminalStripEditor::setCurrentStrip * Set the current terminal strip edited to \p strip_ @@ -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')); } diff --git a/sources/TerminalStrip/ui/terminalstripeditor.h b/sources/TerminalStrip/ui/terminalstripeditor.h index 4a38d0e34..8ba7a878e 100644 --- a/sources/TerminalStrip/ui/terminalstripeditor.h +++ b/sources/TerminalStrip/ui/terminalstripeditor.h @@ -27,6 +27,7 @@ namespace Ui { } class QETProject; +class Cable; class TerminalStrip; /** @@ -58,6 +59,9 @@ class TerminalStripEditor : public QWidget void spanMultiLevelTerminals(); void selectionChanged(); QSize setUpBridgeCellWidth(); + void watchCableChanges(); + void watchCable(Cable *cable); + void reloadOnCableChange(); TerminalStripModel::Column isSingleColumnSelected() const; QPair> singleColumnData() const; @@ -79,6 +83,9 @@ class TerminalStripEditor : public QWidget private: Ui::TerminalStripEditor *ui; QPointer m_project; + //The project whose cables this editor follows, which is not + //always m_project yet when watchCableChanges() runs again + QPointer m_watched_project; QPointer m_current_strip; TerminalStripModel *m_model {nullptr}; }; diff --git a/sources/TerminalStrip/ui/terminalstripmodel.cpp b/sources/TerminalStrip/ui/terminalstripmodel.cpp index 4dfdb51e4..2fd22539a 100644 --- a/sources/TerminalStrip/ui/terminalstripmodel.cpp +++ b/sources/TerminalStrip/ui/terminalstripmodel.cpp @@ -33,6 +33,9 @@ #include #include #include +#include +#include +#include /** * Some const int who describe what a column contain @@ -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 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 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(m_physical_data.size()); for (const auto &real_t : phy_t->realTerminals()) { if (!real_t.isNull()) { mptd.real_data.append(modelRealTerminalData::data(real_t)); + m_row_index.append({physical_index, + static_cast(mptd.real_data.size()) - 1}); } } m_physical_data.append(mptd); } } + + m_row_count = m_row_index.size(); } modelRealTerminalData TerminalStripModel::dataAtRow(int row) const { - if (row > rowCount(QModelIndex())) { + if (row < 0 || row >= m_row_count) { return modelRealTerminalData(); } - else - { - auto current_row = 0; - for (const auto &physical_data : std::as_const(m_physical_data)) - { - for (const auto &real_data : physical_data.real_data) - { - if (current_row == row) { - return real_data; - } else { - ++current_row; - } - } - } - } - return modelRealTerminalData(); + const auto &place_ = m_row_index.at(row); + return m_physical_data.at(place_.physical_).real_data.at(place_.real_); } /** @@ -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= 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 &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(); + 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(); + 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(this)); + + return te; + } return QStyledItemDelegate::createEditor(parent, option, index); } +/** + * @brief TerminalStripModelDelegate::eventFilter + * The filter taken on by the Target cell editor: only the key which is + * handled here is the one giving the text back to the table. The filter + * of the base class is left aside for that cell on purpose, it is written + * for one line editors and would take Return away from the text. + * @param object the editor of the cell + * @param event the event sent to it + * @return true when the event is taken by this delegate + */ +bool TerminalStripModelDelegate::eventFilter(QObject *object, QEvent *event) +{ + if (object && object->objectName() == QLatin1String("terminal_cable_target")) + { + if (event && event->type() == QEvent::KeyPress) + { + const auto key_event = static_cast(event); + if ((key_event->key() == Qt::Key_Return || + key_event->key() == Qt::Key_Enter) && + key_event->modifiers().testFlag(Qt::ControlModifier)) + { + auto editor_ = qobject_cast(object); + emit commitData(editor_); + emit closeEditor(editor_, QAbstractItemDelegate::NoHint); + + return true; + } + } + return false; + } + + return QStyledItemDelegate::eventFilter(object, event); +} + void TerminalStripModelDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const { if (index.isValid() && @@ -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(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(editor)) { + //The lines are handed over as they were written, so a + //" / " inside a field is part of it and never a separator + model->setData(index, te->toPlainText(), Qt::EditRole); + } + } else { QStyledItemDelegate::setModelData(editor, model, index); } diff --git a/sources/TerminalStrip/ui/terminalstripmodel.h b/sources/TerminalStrip/ui/terminalstripmodel.h index 7a95f227a..a714a1631 100644 --- a/sources/TerminalStrip/ui/terminalstripmodel.h +++ b/sources/TerminalStrip/ui/terminalstripmodel.h @@ -58,14 +58,15 @@ class TerminalStripModel : public QAbstractTableModel XRef = 8, Cable = 9, CableWire = 10, - Type = 11, - Function = 12, - Led = 13, - Shield = 14, - Color = 15, - Section = 16, - Width = 17, - Separator = 18, + CableTarget = 11, + Type = 12, + Function = 13, + Led = 14, + Shield = 15, + Color = 16, + Section = 17, + Width = 18, + Separator = 19, Invalid = 99 }; @@ -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 m_terminal_strip; //Modified cells of each real terminal (spare terminals have no element) QHash> m_modified_cell; QVector m_physical_data; + //One RowPlace by row, filled with m_physical_data (see fillPhysicalTerminalData) + QVector m_row_index; + //Number of rows held in m_row_index, so rowCount() needs no walk + int m_row_count = 0; struct BridgePixmap { QPixmap top_, @@ -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; diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index cde86eef6..13774c6a7 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -3314,8 +3314,9 @@ bool QetScriptApi::bridgeTerminals(int stripIndex, const QVariantList &realTermi /** @brief QetScriptApi::sortTerminalStrip - Reorder a strip's physical positions into the canonical order the - editor's own "sort" button computes, through SortTerminalStripCommand. + Reorder a strip's physical positions into the order the plan is + drawn in -- folio by folio, then left to right -- the same + SortTerminalStripCommand the editor's own "sort" button pushes. */ bool QetScriptApi::sortTerminalStrip(int stripIndex) { diff --git a/tests/qttest/CMakeLists.txt b/tests/qttest/CMakeLists.txt index d5753ce50..4e8d6c930 100644 --- a/tests/qttest/CMakeLists.txt +++ b/tests/qttest/CMakeLists.txt @@ -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) diff --git a/tests/qttest/tst_cabletarget.cpp b/tests/qttest/tst_cabletarget.cpp new file mode 100644 index 000000000..d12490dab --- /dev/null +++ b/tests/qttest/tst_cabletarget.cpp @@ -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 . +*/ +#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 +#include +#include + +/** + The terminal strip graphic names where a cable goes: Installation, + Location and Label (BMK) of the first piece the wires of the cable + land on, past the strip's own terminals, in plan order -- the same + three fields the cable list computes for a target + (cablelistrows.cpp), so the strip and the list can never disagree. + The graphic draws them in a small box under the cable, the bundle + staying together (RealTerminal::cableTarget()). + + 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 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({ + QStringLiteral("Plant 7"), + QStringLiteral("Cabinet 7"), + QStringLiteral("-S7"), + })); + + //The other strip terminal carries no claimed wire of its own + //(584b452e joins it to a_ and is not claimed) and no cable name, + //so it has no target to name + const auto b_ = terminalInStrip(QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}")); + QVERIFY(b_); + QVERIFY(b_->cableTarget().isEmpty()); +} + +/** + A cable whose wires only ever join the terminals of the strip + itself lands on nothing past the strip: no target, no box. +*/ +void tst_cabletarget::ownStripIsNoTarget() +{ + Cable *cable_ = m_project->newCable(); + QVERIFY(cable_); + Conductor *inside_ = conductor( + QStringLiteral("{584b452e-a885-491a-98f1-fd348610b5e0}")); + QVERIFY(inside_); + claimCable(inside_, cable_->uuid()); + + const auto a_ = terminalInStrip(QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + const auto b_ = terminalInStrip(QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}")); + QVERIFY(a_); + QVERIFY(b_); + QVERIFY(a_->cableTarget().isEmpty()); + QVERIFY(b_->cableTarget().isEmpty()); +} + +/** + A spare terminal stands on no folio and is wired to nothing: it has + no target to name either. +*/ +void tst_cabletarget::spareHasNoTarget() +{ + ElementData data_; + data_.m_informations.addValue(QStringLiteral("label"), + QStringLiteral("AAA")); + const auto spare_ = RealTerminal::createSpare(data_); + QVERIFY(spare_); + QVERIFY(spare_->isSpare()); + QVERIFY(spare_->cableTarget().isEmpty()); +} + +/** + The strip graphic draws the box: the same strip painted with + something to name at the end of the cable and painted with nothing + to name come out as two different pictures -- the box is the + difference. Painting it also runs the whole drawCables() path. +*/ +void tst_cabletarget::paintsWithTargetBox() +{ + QVERIFY(wireCableToTheContact()); + + const auto strips_ = m_project->terminalStrip(); + QVERIFY(!strips_.isEmpty()); + TerminalStripDrawer::TerminalStripDrawer drawer{ + QSharedPointer{ + new TerminalStripDrawer::TrueTerminalStrip{strips_.first()}}, + QSharedPointer{new TerminalStripLayoutPattern}}; + + const QRectF bounds{drawer.boundingRect()}; + QVERIFY(!bounds.isEmpty()); + + const auto render = [&drawer, &bounds]() { + QImage image{bounds.size().toSize() + QSize{4, 4}, + QImage::Format_ARGB32_Premultiplied}; + image.fill(Qt::transparent); + QPainter painter{&image}; + painter.translate(-bounds.topLeft() + QPointF{2, 2}); + drawer.paint(&painter); + return image; + }; + + const QImage with_box{render()}; + + //Nothing left to name at the far end of the cable + Element *contact_ = element( + QStringLiteral("{fc9cc920-5822-421a-916e-a17100acdcf6}")); + QVERIFY(contact_); + ElementData contact_data_{contact_->elementData()}; + contact_data_.m_informations.addValue(QETInformation::ELMT_PLANT, QString()); + contact_data_.m_informations.addValue(QETInformation::ELMT_LOCATION, QString()); + contact_data_.m_informations.addValue(QETInformation::ELMT_LABEL, QString()); + contact_->setElementData(contact_data_); + + const QImage without_box{render()}; + QVERIFY(with_box != without_box); +} + +/** + The number of a wire is written next to its mark, in the room left + between the mark and the terminal: it shrinks to fit there, and what + still does not fit is clipped away, so a long number never runs into + the drawing of the terminal. The two pictures -- one with a number + long enough to reach into the terminal, one without any -- must be + the same above the bottom of the terminal, and differ below it (the + number is drawn somewhere). +*/ +void tst_cabletarget::wireLabelStaysOutOfTheTerminal() +{ + QVERIFY(wireCableToTheContact()); + + auto pattern_ = QSharedPointer{new TerminalStripLayoutPattern}; + const auto strips_ = m_project->terminalStrip(); + QVERIFY(!strips_.isEmpty()); + TerminalStripDrawer::TerminalStripDrawer drawer{ + QSharedPointer{ + new TerminalStripDrawer::TrueTerminalStrip{strips_.first()}}, + pattern_}; + + const QRectF bounds{drawer.boundingRect()}; + QVERIFY(!bounds.isEmpty()); + + const auto render = [&drawer, &bounds]() { + QImage image{bounds.size().toSize() + QSize{4, 4}, + QImage::Format_ARGB32_Premultiplied}; + image.fill(Qt::transparent); + QPainter painter{&image}; + painter.translate(-bounds.topLeft() + QPointF{2, 2}); + drawer.paint(&painter); + return image; + }; + + //A number much longer than the room left over the mark + Element *terminal_ = element( + QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + QVERIFY(terminal_); + ElementData terminal_data_{terminal_->elementData()}; + terminal_data_.m_informations.addValue( + RealTerminal::cableWireInfoKey(), + QStringLiteral("gnge-gnge-gnge-gnge-gnge")); + terminal_->setElementData(terminal_data_); + const QImage with_label{render()}; + + //The same strip, the wire carries no number at all + terminal_data_.m_informations.addValue(RealTerminal::cableWireInfoKey(), + QString()); + terminal_->setElementData(terminal_data_); + const QImage without_label{render()}; + + //The number is drawn: the two pictures differ + QVERIFY(with_label != without_label); + + //The terminals of this strip all stand on the last level of the + //pattern, so their bottom is the same rectangle + const qreal terminal_bottom{pattern_->m_terminal_rect.at(3).bottom()}; + const auto rows_to_check{qMin(qRound(terminal_bottom - bounds.topLeft().y() + 2), + with_label.height())}; + for (auto y = 0 ; y < rows_to_check ; ++y) + { + for (auto x = 0 ; x < with_label.width() ; ++x) + { + QCOMPARE(with_label.pixel(x, y), without_label.pixel(x, y)); + } + } +} + +/** + The box at the end of a cable keeps the font of the drawing: with + its three lines (Installation, Location and Label) and the end zone + the settings leave, the text is still written at the font of the + strip instead of being squeezed into a tiny one. The box is the + lowest thing the strip draws, so its bottom line is the lowest + painted row, and it must land where three lines of that font need + it to. +*/ +void tst_cabletarget::targetBoxKeepsTheDrawingFont() +{ + QVERIFY(wireCableToTheContact()); + + auto pattern_ = QSharedPointer{new TerminalStripLayoutPattern}; + const auto strips_ = m_project->terminalStrip(); + QVERIFY(!strips_.isEmpty()); + TerminalStripDrawer::TerminalStripDrawer drawer{ + QSharedPointer{ + new TerminalStripDrawer::TrueTerminalStrip{strips_.first()}}, + pattern_}; + + const QRectF bounds{drawer.boundingRect()}; + QVERIFY(!bounds.isEmpty()); + QImage image{bounds.size().toSize() + QSize{4, 4}, + QImage::Format_ARGB32_Premultiplied}; + image.fill(Qt::transparent); + QPainter painter{&image}; + painter.translate(-bounds.topLeft() + QPointF{2, 2}); + drawer.paint(&painter); + painter.end(); + + qreal lowest{-1}; + for (auto y = 0 ; y < image.height() ; ++y) + { + for (auto x = 0 ; x < image.width() ; ++x) + { + if (image.pixel(x, y) != qRgba(0, 0, 0, 0)) { + lowest = y; + break; + } + } + } + QVERIFY(lowest >= 0); + + //The end zone the default layout leaves is what gives the box its + //room: three lines of the default font must fit in it, or the text + //of a target is squeezed down again by the drawing itself + const QFontMetricsF metrics{pattern_->font()}; + const qreal room{pattern_->m_cable_end_length + - qMin(6, pattern_->m_cable_end_length)}; + QVERIFY2(metrics.height() * 3 + 4 <= room, + "the default end zone leaves no room for three lines of the " + "default font, a target would be squeezed again"); + + //Where the bottom line of the box lands when it holds the three + //lines of its text at the font of the drawing + const qreal terminals_bottom{pattern_->m_terminal_rect.at(3).bottom()}; + const qreal bar_2{terminals_bottom + pattern_->m_cable_wire_length + + pattern_->m_cable_length}; + const qreal box_bottom{bar_2 + qMin(6, pattern_->m_cable_end_length) + + metrics.height() * 3 + 4}; + + //The strip draws nothing under the box, so the lowest painted row + //is its bottom line -- within a pixel, the outline of a rectangle + //is painted on its edge + const qreal row{box_bottom - bounds.top() + 2}; + QVERIFY(lowest + 2 >= row); +} + +/** + The realistic style draws the cable too. The strip seen from above + reserves the zone under it when one of its terminals carries a + cable, and the wires, the cable and the box naming where it goes + are painted in it -- exactly what the schematic style draws, since + both styles now share the same Helpers::drawCables(). +*/ +void tst_cabletarget::realisticStyleDrawsTheCable() +{ + QVERIFY(wireCableToTheContact()); + + const auto strips_ = m_project->terminalStrip(); + QVERIFY(!strips_.isEmpty()); + const auto strip_ = QSharedPointer{ + new TerminalStripDrawer::TrueTerminalStrip{strips_.first()}}; + TerminalStripLayoutPattern pattern_; + + const auto bounds_of = [&strip_, &pattern_]() { + return TerminalStripDrawer::Realistic::boundingRect(strip_.data(), + pattern_); + }; + const auto render = [&strip_, &pattern_](const QRectF &bounds) { + QImage image{bounds.size().toSize() + QSize{4, 4}, + QImage::Format_ARGB32_Premultiplied}; + image.fill(Qt::transparent); + QPainter painter{&image}; + painter.translate(-bounds.topLeft() + QPointF{2, 2}); + TerminalStripDrawer::Realistic::paint( + &painter, *strip_, pattern_, + TerminalStripLayoutPattern::DrawStyle::RealisticPushIn); + return image; + }; + + const QRectF with_bounds{bounds_of()}; + const QImage with_cable{render(with_bounds)}; + + //The body of the strip ends there : everything painted under it + //belongs to the cable + const qreal body_bottom{ + pattern_.m_realistic_length_mm * pattern_.m_realistic_px_per_mm}; + QVERIFY(with_bounds.height() > body_bottom); + + int painted_{0}; + for (auto y = qRound(body_bottom) + 4 ; y + 4 < with_cable.height() ; ++y) + { + for (auto x = 0 ; x + 4 < with_cable.width() ; ++x) + { + if (with_cable.pixel(x, y) != qRgba(0, 0, 0, 0)) { + ++painted_; + } + } + } + //The wires and the cable are drawn in that zone + QVERIFY(painted_ > 0); + + //The same strip, its terminal carries no cable name any more + Element *terminal_ = element( + QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + QVERIFY(terminal_); + ElementData terminal_data_{terminal_->elementData()}; + terminal_data_.m_informations.addValue(RealTerminal::cableInfoKey(), + QString()); + terminal_->setElementData(terminal_data_); + + //No cable to let out : the strip does not reserve the zone + const QRectF without_bounds{bounds_of()}; + QVERIFY(without_bounds.height() < with_bounds.height()); +} + +/** + 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{new TerminalStripLayoutPattern}; + const auto strips_ = m_project->terminalStrip(); + QVERIFY(!strips_.isEmpty()); + TerminalStripDrawer::TerminalStripDrawer drawer{ + QSharedPointer{ + new TerminalStripDrawer::TrueTerminalStrip{strips_.first()}}, + pattern_}; + + const QRectF bounds{drawer.boundingRect()}; + QVERIFY(!bounds.isEmpty()); + + const auto render = [&drawer, &bounds]() { + QImage image{bounds.size().toSize() + QSize{4, 4}, + QImage::Format_ARGB32_Premultiplied}; + image.fill(Qt::transparent); + QPainter painter{&image}; + painter.translate(-bounds.topLeft() + QPointF{2, 2}); + drawer.paint(&painter); + painter.end(); + return image; + }; + const auto lowest_row = [](const QImage &image_) { + qreal lowest{-1}; + for (auto y = 0 ; y < image_.height() ; ++y) + { + for (auto x = 0 ; x < image_.width() ; ++x) + { + if (image_.pixel(x, y) != qRgba(0, 0, 0, 0)) { + lowest = y; + break; + } + } + } + return lowest; + }; + const auto type_in = [terminal_](const QString &text_) { + ElementData data_{terminal_->elementData()}; + data_.m_informations.addValue(RealTerminal::cableTargetInfoKey(), text_); + terminal_->setElementData(data_); + }; + + //One field: the box holds one single line + type_in(QStringLiteral("Boiler room")); + const qreal one_line{lowest_row(render())}; + QVERIFY(one_line >= 0); + + //Three fields: the box is two lines deeper, at the font of the + //drawing -- the fields are written one under the other, the cell + //holding them only tells them apart by their line breaks + type_in(QStringLiteral("Boiler room\nCabinet 3\n-K3")); + QCOMPARE(a_->cableTarget(), (QVector{QStringLiteral("Boiler room"), + QStringLiteral("Cabinet 3"), + QStringLiteral("-K3")})); + 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{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{new TerminalStripLayoutPattern}; + const auto strips_ = m_project->terminalStrip(); + QVERIFY(!strips_.isEmpty()); + TerminalStripDrawer::TerminalStripDrawer drawer{ + QSharedPointer{ + new TerminalStripDrawer::TrueTerminalStrip{strips_.first()}}, + pattern_}; + + const auto render = [&drawer](const QRectF &bounds) { + QImage image{bounds.size().toSize() + QSize{4, 4}, + QImage::Format_ARGB32_Premultiplied}; + image.fill(Qt::transparent); + QPainter painter{&image}; + painter.translate(-bounds.topLeft() + QPointF{2, 2}); + drawer.paint(&painter); + painter.end(); + return image; + }; + const auto lowest_row = [](const QImage &image_) { + qreal lowest{-1}; + for (auto y = 0 ; y < image_.height() ; ++y) + { + for (auto x = 0 ; x < image_.width() ; ++x) + { + if (image_.pixel(x, y) != qRgba(0, 0, 0, 0)) { + lowest = y; + break; + } + } + } + return lowest; + }; + + //One cable with a target : one box, in the end zone + const QRectF one_bounds{drawer.boundingRect()}; + const qreal one_row{lowest_row(render(one_bounds))}; + QVERIFY(one_row >= 0); + + //The second cable gets a target of its own : its box is wider + //than the distance between the two cables and would be written + //over the first one + ElementData second_data_{second_->elementData()}; + second_data_.m_informations.addValue(RealTerminal::cableInfoKey(), + QStringLiteral("X98")); + second_data_.m_informations.addValue(RealTerminal::cableTargetInfoKey(), + QStringLiteral("Plant 9\nCabinet 9\n-S9")); + second_->setElementData(second_data_); + QCOMPARE(b_->cableTarget(), + (QVector{QStringLiteral("Plant 9"), + QStringLiteral("Cabinet 9"), + QStringLiteral("-S9")})); + + const QRectF two_bounds{drawer.boundingRect()}; + const qreal two_row{lowest_row(render(two_bounds))}; + + //The box in the way is set lower : the strip draws a whole box + //deeper than it did with one single box -- the second box stands + //under the first one, one box height plus the gap between them + const QFontMetricsF metrics{pattern_->font()}; + const qreal box_height{metrics.height() * 3 + 4}; + QVERIFY2(two_row - one_row > box_height, + "a box reaching into another one must be set lower, not " + "written over it"); + + //And the strip is deep enough for it, or the lower box would be + //cut off by the drawing area + QVERIFY2(two_bounds.height() > one_bounds.height(), + "the strip reserves no room for the box it sets lower"); + QVERIFY2(two_bounds.bottom() - one_bounds.bottom() > 0.5 * box_height, + "the room reserved for the lower box is too small"); +} + +int main(int argc, char **argv) +{ + QStandardPaths::setTestModeEnabled(true); + return QetAppTest::run(argc, argv); +} + +#include "tst_cabletarget.moc" diff --git a/tests/qttest/tst_terminalcableauto.cpp b/tests/qttest/tst_terminalcableauto.cpp new file mode 100644 index 000000000..e0dfd7dae --- /dev/null +++ b/tests/qttest/tst_terminalcableauto.cpp @@ -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 . +*/ +#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 +#include +#include +#include +#include +#include +#include + +/** + The Cable and Color / wire number columns of the terminal strip editor + are worked out from the cables of the project: a terminal with a cable + wired to it shows that cable and its core, the cell is read only, and + nothing is written into the element -- so a cable renamed or removed + cannot leave a stale value behind. A terminal no cable is wired to + keeps the value typed in by hand, cell and all. + + 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 wiredTerminal(QETProject &project) + { + const QVector strips = project.terminalStrip(); + if (strips.isEmpty()) return {}; + for (const QSharedPointer &real_t + : strips.first()->realTerminals()) + { + Element *element = real_t ? real_t->element() : nullptr; + if (!element) continue; + for (Terminal *terminal : element->terminals()) + if (terminal && !terminal->conductors().isEmpty()) + return real_t; + } + return {}; + } + + static QList conductorsOf(const QSharedPointer &real_t) + { + QList conductors; + Element *element = real_t ? real_t->element() : nullptr; + if (!element) return conductors; + for (Terminal *terminal : element->terminals()) { + if (!terminal) continue; + for (Conductor *conductor : terminal->conductors()) { + if (conductor && !conductors.contains(conductor)) + conductors.append(conductor); + } + } + return conductors; + } + + static Conductor *conductorOf(const QSharedPointer &real_t) + { + const QList conductors = conductorsOf(real_t); + return conductors.isEmpty() ? nullptr : conductors.first(); + } + + // The wire which leaves the strip: one of its ends belongs to a piece + // outside the strip, while the wire joining two terminals of this very + // strip never leaves it -- and never has a target to name. + static Conductor *outgoingConductor(const QSharedPointer &real_t) + { + TerminalStrip *strip = real_t ? real_t->parentStrip() : nullptr; + if (!strip) return nullptr; + + QSet own; + for (const QSharedPointer &physical + : strip->physicalTerminal()) + { + if (!physical) continue; + for (const QSharedPointer &real : physical->realTerminals()) + if (real && real->element()) + own.insert(real->element()->uuid()); + } + + for (Conductor *conductor : conductorsOf(real_t)) + { + if (!conductor) continue; + Element *ends[2] = { + conductor->terminal1 ? conductor->terminal1->parentElement() : nullptr, + conductor->terminal2 ? conductor->terminal2->parentElement() : nullptr }; + for (Element *end : ends) + if (end && !own.contains(end->uuid())) + return conductor; + } + return nullptr; + } + + // The cable the create dialog hands over: owned by the project, four + // cores, one core wired to the terminal's conductor (or to @p on). + static Cable *wireCable(QETProject &project, + const QSharedPointer &real_t, + int core = 1, + const QString &designation = QStringLiteral("15W1"), + Conductor *on = nullptr) + { + if (!on) on = conductorOf(real_t); + Cable *cable = project.newCable(); + cable->setDesignation(designation); + cable->setCoreCount(4); + cable->setCoreColors({QStringLiteral("bn"), QStringLiteral("bl"), + QStringLiteral("gn/ye"), QStringLiteral("gy")}); + CableCore assignment; + assignment.core = core; + assignment.conductor = on->uuid(); + cable->setCore(assignment); + return cable; + } + +private slots: + void wiredCableIsWorkedOutAndReadsOnly() + { + const QString fixture = QFINDTESTDATA("fixtures/every_feature.qet"); + QVERIFY2(!fixture.isEmpty(), "fixture project not found"); + QETProject project(fixture); + QCOMPARE(project.state(), QETProject::Ok); + + const auto real_t = wiredTerminal(project); + QVERIFY2(!real_t.isNull(), "the fixture has a wired terminal"); + Element *element = real_t->element(); + + //Without a cable, the cells show nothing and may be typed in + QVERIFY(!real_t->cableIsAutomatic()); + TerminalStripModel model_before(project.terminalStrip().first()); + QVERIFY(model_before.rowCount() > 0); + QVERIFY(model_before.flags(model_before.index(0, TerminalStripModel::Cable)) + & Qt::ItemIsEditable); + + wireCable(project, real_t); + + //The terminal itself reads the cable and its core + QCOMPARE(real_t->cable(), QStringLiteral("15W1")); + QVERIFY(real_t->cableIsAutomatic()); + QCOMPARE(real_t->cableWire(), QStringLiteral("bl")); + + //The automatic value is never written into the element: nothing + //stays behind once the cable is renamed or removed + QVERIFY(element); + QVERIFY(element->elementData().m_informations + .value(RealTerminal::cableInfoKey()).toString().isEmpty()); + + //The model shows both cells and keeps them out of the edits + TerminalStripModel model(project.terminalStrip().first()); + QCOMPARE(model.rowCount(), model_before.rowCount()); + const QModelIndex cable_cell = model.index(0, TerminalStripModel::Cable); + const QModelIndex wire_cell = model.index(0, TerminalStripModel::CableWire); + QCOMPARE(model.data(cable_cell).toString(), QStringLiteral("15W1")); + QCOMPARE(model.data(wire_cell).toString(), QStringLiteral("bl")); + QVERIFY(!(model.flags(cable_cell) & Qt::ItemIsEditable)); + QVERIFY(!(model.flags(wire_cell) & Qt::ItemIsEditable)); + //The other cells go on being editable + QVERIFY(model.flags(model.index(0, TerminalStripModel::Label)) + & Qt::ItemIsEditable); + } + + void coreWithoutColorIsShownAsItsNumber() + { + const QString fixture = QFINDTESTDATA("fixtures/every_feature.qet"); + QETProject project(fixture); + QCOMPARE(project.state(), QETProject::Ok); + + const auto real_t = wiredTerminal(project); + QVERIFY(!real_t.isNull()); + + Cable *cable = project.newCable(); + cable->setDesignation(QStringLiteral("15W1")); + cable->setCoreCount(4); + //A type file which gives no colours: the number of the core is shown + CableCore assignment; + assignment.core = 1; + assignment.conductor = conductorOf(real_t)->uuid(); + cable->setCore(assignment); + + QCOMPARE(real_t->cable(), QStringLiteral("15W1")); + QCOMPARE(real_t->cableWire(), QStringLiteral("2")); + QVERIFY(real_t->cableIsAutomatic()); + } + + void typedValueStaysWhenNoCableIsThere() + { + const QString fixture = QFINDTESTDATA("fixtures/every_feature.qet"); + QETProject project(fixture); + QCOMPARE(project.state(), QETProject::Ok); + + const auto real_t = wiredTerminal(project); + QVERIFY(!real_t.isNull()); + Element *element = real_t->element(); + QVERIFY(element); + + //A cable designation typed in by hand in the editor + ElementData typed = element->elementData(); + typed.m_informations.addValue(RealTerminal::cableInfoKey(), + QStringLiteral("K9")); + element->setElementData(typed); + QCOMPARE(real_t->cable(), QStringLiteral("K9")); + QVERIFY(!real_t->cableIsAutomatic()); + + //Wired to a cable: the cable is shown, the typed value is set aside + Cable *cable = wireCable(project, real_t); + QCOMPARE(real_t->cable(), QStringLiteral("15W1")); + QVERIFY(real_t->cableIsAutomatic()); + QCOMPARE(real_t->cableWire(), QStringLiteral("bl")); + + //Cable removed: the typed value is back, no stale 15W1 anywhere + cable->removeCore(1); + QCOMPARE(real_t->cable(), QStringLiteral("K9")); + QVERIFY(!real_t->cableIsAutomatic()); + QVERIFY(element->elementData().m_informations + .value(RealTerminal::cableInfoKey()) + .toString() == QStringLiteral("K9")); + + TerminalStripModel model(project.terminalStrip().first()); + QVERIFY(model.flags(model.index(0, TerminalStripModel::Cable)) + & Qt::ItemIsEditable); + } + + // The window stays open while the user wires the drawing: the cells + // follow a cable wired, renamed or taken away without any reload by + // hand. + void openEditorFollowsCableChanges() + { + const QString fixture = QFINDTESTDATA("fixtures/every_feature.qet"); + QETProject project(fixture); + QCOMPARE(project.state(), QETProject::Ok); + + const auto real_t = wiredTerminal(project); + QVERIFY(!real_t.isNull()); + + TerminalStripEditor editor(&project); + editor.setCurrentStrip(project.terminalStrip().first()); + const QAbstractItemView *table = editor.findChild( + QStringLiteral("m_table_widget")); + QVERIFY2(table && table->model(), "the editor has its table"); + const QAbstractItemModel *model = table->model(); + const QModelIndex cable_cell = model->index(0, TerminalStripModel::Cable); + QVERIFY(model->data(cable_cell).toString().isEmpty()); + + //A cable wired to the terminal fills the cell in + Cable *cable = wireCable(project, real_t); + QCOMPARE(model->data(cable_cell).toString(), QStringLiteral("15W1")); + + //Renaming it follows at once, the drawing is not stale behind + cable->setDesignation(QStringLiteral("16W1")); + QCOMPARE(model->data(cable_cell).toString(), QStringLiteral("16W1")); + + //Taking it away empties the cell: nothing stale stays behind + QVERIFY(project.removeCable(cable)); + QVERIFY(model->data(cable_cell).toString().isEmpty()); + } + + // A terminal wired to two cables shows them both, comma separated. + void twoCablesOnOneTerminalAreBothShown() + { + const QString fixture = QFINDTESTDATA("fixtures/every_feature.qet"); + QETProject project(fixture); + QCOMPARE(project.state(), QETProject::Ok); + + const auto real_t = wiredTerminal(project); + QVERIFY(!real_t.isNull()); + const QList conductors = conductorsOf(real_t); + QVERIFY2(conductors.size() >= 2, + "the fixture terminal carries two wires"); + + wireCable(project, real_t, 1, QStringLiteral("15W1"), conductors.at(0)); + wireCable(project, real_t, 1, QStringLiteral("16W1"), conductors.at(1)); + + QCOMPARE(real_t->cable(), QStringLiteral("15W1, 16W1")); + QCOMPARE(real_t->cableWire(), QStringLiteral("bl, bl")); + QVERIFY(real_t->cableIsAutomatic()); + } + + // 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( + 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{QStringLiteral("Boiler room"), + QStringLiteral("Cabinet 3"), + QStringLiteral("-K3")})); + QVERIFY(!real_t->cableTargetIsAutomatic()); + + //A fresh model shows the text and still lets it be edited + TerminalStripModel model_after(project.terminalStrip().first()); + const QModelIndex after_cell = model_after.index( + 0, TerminalStripModel::CableTarget); + QCOMPARE(model_after.data(after_cell).toString(), typed); + QVERIFY(model_after.flags(after_cell) & Qt::ItemIsEditable); + + //The piece the cable lands on is named now : once the cable is + //wired the drawing gives the target of its own, and that one is + //read only while the text typed in by hand stays in the element + Element *target = (outgoing->terminal1 + && outgoing->terminal1->parentElement() == real_t->element()) + ? (outgoing->terminal2 ? outgoing->terminal2->parentElement() : nullptr) + : (outgoing->terminal1 ? outgoing->terminal1->parentElement() : nullptr); + QVERIFY(target); + ElementData named = target->elementData(); + named.m_informations.addValue(QETInformation::ELMT_PLANT, + QStringLiteral("Plant 9")); + named.m_informations.addValue(QETInformation::ELMT_LOCATION, + QStringLiteral("Cabinet 9")); + named.m_informations.addValue(QETInformation::ELMT_LABEL, + QStringLiteral("-S9")); + target->setElementData(named); + + Cable *cable = wireCable(project, real_t, 1, QStringLiteral("15W1"), + outgoing); + QVERIFY(cable); + QVERIFY(real_t->cableTargetIsAutomatic()); + QCOMPARE(real_t->cableTarget(), + (QVector{QStringLiteral("Plant 9"), + QStringLiteral("Cabinet 9"), + QStringLiteral("-S9")})); + QCOMPARE(element->elementData().m_informations + .value(RealTerminal::cableTargetInfoKey()).toString(), typed); + + TerminalStripModel model_cable(project.terminalStrip().first()); + const QModelIndex cable_cell = model_cable.index( + 0, TerminalStripModel::CableTarget); + QCOMPARE(model_cable.data(cable_cell).toString(), + QStringLiteral("Plant 9\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{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{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{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{QStringLiteral("Betrieb 1/2")}); + + typed = element->elementData(); + typed.m_informations.addValue(RealTerminal::cableTargetInfoKey(), + QStringLiteral("Betrieb 1/2 / Keller")); + element->setElementData(typed); + QCOMPARE(real_t->cableTarget(), + QVector{QStringLiteral("Betrieb 1/2 / Keller")}); + TerminalStripModel slashed(project.terminalStrip().first()); + QCOMPARE(slashed.data(slashed.index(0, TerminalStripModel::CableTarget)) + .toString(), QStringLiteral("Betrieb 1/2 / Keller")); + + //The line breaks written by hand are the fields again, whatever + //the fields themselves contain + typed = element->elementData(); + typed.m_informations.addValue(RealTerminal::cableTargetInfoKey(), + QStringLiteral("Betrieb 1/2\nKeller\n-K4")); + element->setElementData(typed); + QCOMPARE(real_t->cableTarget(), + (QVector{QStringLiteral("Betrieb 1/2"), + QStringLiteral("Keller"), + QStringLiteral("-K4")})); + } + + // The Target cell is edited line by line: its editor holds one line + // per field and hands the text back exactly as the cell holds it. + // Return writes a new line (the cell is multi line), Ctrl+Return + // gives the cell back without waiting for the focus to go somewhere + // else, and the row of the table is as tall as the lines the cell + // shows, so none of them is cut off. + void targetCellIsEditedLineByLine() + { + const QString fixture = QFINDTESTDATA("fixtures/every_feature.qet"); + QETProject project(fixture); + QCOMPARE(project.state(), QETProject::Ok); + + TerminalStripEditor editor(&project); + editor.setCurrentStrip(project.terminalStrip().first()); + QTableView *table = editor.findChild( + QStringLiteral("m_table_widget")); + QVERIFY2(table && table->model(), "the editor has its table"); + QAbstractItemModel *model = table->model(); + QVERIFY(model->rowCount() > 1); + const QModelIndex cell = model->index(0, TerminalStripModel::CableTarget); + QVERIFY(model->flags(cell) & Qt::ItemIsEditable); + + const QString typed{QStringLiteral("Betrieb 1/2\nKeller\n-K4")}; + QVERIFY(model->setData(cell, typed, Qt::EditRole)); + QCOMPARE(model->data(cell).toString(), typed); + + //The editor of the table is the one taking a cell of several + //lines, and one of its own does the very same thing for the + //keys, without a view in between + QVERIFY(qobject_cast( + table->itemDelegate())); + TerminalStripModelDelegate delegate; + + QStyleOptionViewItem option; + option.rect = QRect(0, 0, qMax( + 50, table->columnWidth(TerminalStripModel::CableTarget)), 400); + option.font = table->font(); + option.fontMetrics = QFontMetrics(option.font); + + QWidget *cell_editor = delegate.createEditor(table, option, cell); + QVERIFY(cell_editor); + auto *text_edit = qobject_cast(cell_editor); + QVERIFY2(text_edit, "the Target cell takes a multi line editor"); + QCOMPARE(text_edit->objectName(), + QStringLiteral("terminal_cable_target")); + + //The editor gives the lines back exactly as the cell holds + //them: nothing joined, nothing split again + delegate.setEditorData(cell_editor, cell); + QCOMPARE(text_edit->toPlainText(), typed); + delegate.setModelData(cell_editor, model, cell); + QCOMPARE(model->data(cell).toString(), typed); + + //Return writes a new line in the cell, and hands nothing over + //yet: Ctrl+Return is the key giving the cell back + QSignalSpy committed(&delegate, &TerminalStripModelDelegate::commitData); + QSignalSpy closed(&delegate, &TerminalStripModelDelegate::closeEditor); + text_edit->moveCursor(QTextCursor::End); + + QKeyEvent return_key{QEvent::KeyPress, Qt::Key_Return, + Qt::NoModifier}; + QCoreApplication::sendEvent(cell_editor, &return_key); + QCOMPARE(text_edit->toPlainText(), typed + QLatin1Char('\n')); + QCOMPARE(committed.count(), 0); + QCOMPARE(closed.count(), 0); + + QKeyEvent ctrl_return_key{QEvent::KeyPress, Qt::Key_Return, + Qt::ControlModifier}; + QCoreApplication::sendEvent(cell_editor, &ctrl_return_key); + QCOMPARE(committed.count(), 1); + QCOMPARE(closed.count(), 1); + delete cell_editor; + } + + // The row of the table follows the lines its Target cell holds: the + // cell is never cut off, however many lines it takes, the row grows + // to hold them all and shrinks back afterwards, while the rows + // around keep the height they have always had. + void targetRowGrowsWithTheLinesOfItsCell() + { + const QString fixture = QFINDTESTDATA("fixtures/every_feature.qet"); + QETProject project(fixture); + QCOMPARE(project.state(), QETProject::Ok); + + TerminalStripEditor editor(&project); + editor.setCurrentStrip(project.terminalStrip().first()); + QTableView *table = editor.findChild( + QStringLiteral("m_table_widget")); + QVERIFY2(table && table->model(), "the editor has its table"); + QAbstractItemModel *model = table->model(); + QVERIFY(model->rowCount() > 1); + const QModelIndex cell = model->index(0, TerminalStripModel::CableTarget); + QVERIFY(model->flags(cell) & Qt::ItemIsEditable); + QVERIFY2(model->data(cell).toString().isEmpty(), + "no target to start with, the row is the usual one"); + + const QModelIndex other_cell = model->index( + 1, TerminalStripModel::CableTarget); + const int base_row = table->rowHeight(cell.row()); + const int base_other_row = table->rowHeight(other_cell.row()); + QVERIFY(base_row > 0); + + //Three lines in the cell: the row holds them all, at the + //font of the table -- the row was already taller than that + //for what else the terminal draws, so it may stay as it is + const QString typed{QStringLiteral("Betrieb 1/2\nKeller\n-K4")}; + QVERIFY(model->setData(cell, typed, Qt::EditRole)); + QCOMPARE(model->data(cell).toString(), typed); + + const auto heightOfLines = [table](int lines_) { + return lines_ * table->fontMetrics().height(); + }; + const int three_line_row = table->rowHeight(cell.row()); + QVERIFY2(three_line_row >= qMax(base_row, heightOfLines(3)), + "the row must be as tall as both the lines the cell " + "shows and what it held before, or something is cut off"); + QVERIFY2(table->rowHeight(other_cell.row()) >= base_other_row, + "the rows around are not pushed down"); + + //From here on the rows have been worked out once: what the + //other rows hold is the height they keep + const int other_row = table->rowHeight(other_cell.row()); + + //Many more lines than anything else in the row: the row grows + //to hold them, and the rows around stay where they are + const QString many{QStringLiteral( + "Plant 6\nCabinet 6\n-K6\nZone 6\nLevel 6\nEnd 6")}; + QVERIFY(model->setData(cell, many, Qt::EditRole)); + const int six_line_row = table->rowHeight(cell.row()); + QVERIFY2(six_line_row >= heightOfLines(6), + "the row must be as tall as the lines the cell shows, " + "or the fields below the first one are cut off"); + QVERIFY2(six_line_row > qMax(base_row, three_line_row), + "a cell which took six lines must take a taller row " + "than the one it had before"); + QVERIFY2(table->rowHeight(other_cell.row()) == other_row, + "the rows around keep their own height"); + + //Back to one single line: the row follows again + QVERIFY(model->setData(cell, QStringLiteral("Keller"), Qt::EditRole)); + QVERIFY2(table->rowHeight(cell.row()) < six_line_row, + "the row follows the cell back to a single line"); + QVERIFY2(table->rowHeight(cell.row()) + <= base_row + table->fontMetrics().height(), + "the row is back to the height it had before"); + QVERIFY2(table->rowHeight(cell.row()) >= table->fontMetrics().height(), + "the single line left is still shown"); + } +}; + +QET_APP_TEST_MAIN(tst_terminalcableauto) + +#include "tst_terminalcableauto.moc" diff --git a/tests/qttest/tst_terminalstriporder.cpp b/tests/qttest/tst_terminalstriporder.cpp new file mode 100644 index 000000000..1303f5b53 --- /dev/null +++ b/tests/qttest/tst_terminalstriporder.cpp @@ -0,0 +1,272 @@ +/* + Copyright 2006-2026 The QElectroTech Team + This file is part of QElectroTech. + + QElectroTech is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 2 of the License, or + (at your option) any later version. + + QElectroTech is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with QElectroTech. If not, see . +*/ +#include "qetapptest.h" + +#include "TerminalStrip/UndoCommand/sortterminalstripcommand.h" +#include "TerminalStrip/physicalterminal.h" +#include "TerminalStrip/realterminal.h" +#include "TerminalStrip/terminalstrip.h" +#include "diagram.h" +#include "properties/elementdata.h" +#include "qetgraphicsitem/element.h" +#include "qetproject.h" +#include "utils/qetutils.h" + +#include + +/** + The terminal strip sorts into the plan's own order -- folio by folio, + then left to right -- instead of by label, so "X1:N" and "X1:PE" + stand where they are drawn rather than at the end of the strip. The + auto import and SortTerminalStripCommand share the one comparator + (SortTerminalStripCommand::planOrderLessThan), so the order the + import works out cannot be rewritten by the sort it then pushes for + the strip it just created. + + fixtures/every_feature.qet has the strip X1 with element 891b70f2 + (x=300) left of 4861e268 (x=360) on the first folio, and 4861e268 + holds an empty label where 891b70f2 has a non-empty one: a label + sort would put 4861e268 first, which is the very swap this test + pins down. +*/ +class tst_terminalstriporder : public QObject +{ + Q_OBJECT + + QETProject *m_project = nullptr; + + Element *element(const QString &uuid) const + { + for (Diagram *d_ : m_project->diagrams()) { + for (Element *e_ : d_->elements()) { + if (e_->uuid().toString() == uuid) { + return e_; + } + } + } + return nullptr; + } + + TerminalStrip *strip() const + { + const auto strips_ = m_project->terminalStrip(); + return strips_.isEmpty() ? nullptr : strips_.first(); + } + + QSharedPointer terminalInStrip(const QString &element_uuid) const + { + auto *strip_ = strip(); + if (!strip_) { + return {}; + } + for (const auto &real_ : strip_->realTerminals()) { + if (real_ && real_->element() + && real_->element()->uuid().toString() == element_uuid) { + return real_; + } + } + return {}; + } + + /// The element uuids of the strip's physical positions, in strip order. + QStringList physicalOrder() const + { + QStringList order_; + auto *strip_ = strip(); + if (!strip_) { + return order_; + } + for (const auto &physical_ : strip_->physicalTerminal()) { + if (physical_ && physical_->realTerminalCount() + && physical_->realTerminals().first()->element()) { + order_ << physical_->realTerminals().first()->element()->uuid().toString(); + } + } + return order_; + } + +private slots: + void init(); + void cleanup(); + + void positionBeatsLabel(); + void folioComesBeforePosition(); + void spareWithoutFolioGoesLast(); + void sortKeepsThePlanOrder(); + void sortFollowsMovedElements(); +}; + +void tst_terminalstriporder::init() +{ + const QString fixture_ = QFINDTESTDATA("fixtures/every_feature.qet"); + QVERIFY(!fixture_.isEmpty()); + m_project = new QETProject(fixture_); + QCOMPARE(m_project->state(), QETProject::Ok); +} + +void tst_terminalstriporder::cleanup() +{ + delete m_project; + m_project = nullptr; +} + +/** + Two terminals of one folio, 891b70f2 left of 4861e268: the position + decides, and the label says the opposite (4861e268's is empty), so a + comparator by label would order them the wrong way round. +*/ +void tst_terminalstriporder::positionBeatsLabel() +{ + const auto a_ = terminalInStrip(QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + const auto b_ = terminalInStrip(QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}")); + QVERIFY(a_); + QVERIFY(b_); + QCOMPARE(a_->element()->pos().x() < b_->element()->pos().x(), true); + + QVERIFY(SortTerminalStripCommand::planOrderLessThan(a_, b_)); + QVERIFY(!SortTerminalStripCommand::planOrderLessThan(b_, a_)); + + //The labels would sort the other way round (b's is empty), which + //is exactly the order the plan must NOT follow + QVERIFY(QETUtils::naturalLessThan(b_->label(), a_->label())); +} + +/** + The page comes before the position ("page by page, then left to + right"): a terminal moved to the second folio sorts after every + terminal of the first one, even when it stands further left than + them. The element is put back on its own folio before the test + ends, so the project is left as it was. +*/ +void tst_terminalstriporder::folioComesBeforePosition() +{ + const auto a_ = terminalInStrip(QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + const auto b_ = terminalInStrip(QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}")); + QVERIFY(a_); + QVERIFY(b_); + Element *moved_ = a_->element(); + QVERIFY(moved_); + const QList folios_ = m_project->diagrams(); + QVERIFY(folios_.size() > 1); + Diagram *own_folio_ = moved_->diagram(); + Diagram *later_folio_ = folios_.at(1); + QVERIFY(later_folio_ != own_folio_); + const QPointF original_pos_ = moved_->pos(); + + //Put a on the second folio, left of where b stands on the first + own_folio_->removeItem(moved_); + later_folio_->addItem(moved_); + moved_->setPos(QPointF(50, 600)); + QVERIFY(moved_->diagram() == later_folio_); + + const bool folio_later_ = later_folio_->folioIndex() > own_folio_->folioIndex(); + const bool a_left_of_b_ = moved_->pos().x() < b_->element()->pos().x(); + const bool b_first_ = SortTerminalStripCommand::planOrderLessThan(b_, a_); + const bool a_first_ = SortTerminalStripCommand::planOrderLessThan(a_, b_); + + //Back to where it was, so the conductors drawn to it and the + //project's cleanup see the folio they were loaded from + later_folio_->removeItem(moved_); + own_folio_->addItem(moved_); + moved_->setPos(original_pos_); + QVERIFY(moved_->diagram() == own_folio_); + + QVERIFY(folio_later_); + QVERIFY(a_left_of_b_); //the position alone would put a first + QVERIFY(b_first_); //the page decides: b (first folio) first + QVERIFY(!a_first_); +} + +/** + A spare terminal stands on no folio: it has no place in the plan + order and goes last, whatever its label would say. +*/ +void tst_terminalstriporder::spareWithoutFolioGoesLast() +{ + ElementData data_; + data_.m_informations.addValue(QStringLiteral("label"), + QStringLiteral("AAA")); + const auto spare_ = RealTerminal::createSpare(data_); + QVERIFY(spare_); + QVERIFY(spare_->isSpare()); + + const auto a_ = terminalInStrip(QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + QVERIFY(a_); + //"AAA" would sort first by label alone + QVERIFY(QETUtils::naturalLessThan(spare_->label(), a_->label())); + + QVERIFY(SortTerminalStripCommand::planOrderLessThan(a_, spare_)); + QVERIFY(!SortTerminalStripCommand::planOrderLessThan(spare_, a_)); +} + +/** + The sort command, run on the fixture as it stands, keeps the strip + the way the plan is drawn: 891b70f2 (x=300) before 4861e268 + (x=360). The old label sort swapped them, since 4861e268's label is + the empty one. +*/ +void tst_terminalstriporder::sortKeepsThePlanOrder() +{ + auto *strip_ = strip(); + QVERIFY(strip_); + QCOMPARE(strip_->physicalTerminalCount(), 2); + + SortTerminalStripCommand command_(strip_); + command_.redo(); + + QCOMPARE(physicalOrder(), + QStringList({ + QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}"), + QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}"), + })); +} + +/** + The order follows the plan, not the strip's history: once the two + elements are swapped on the folio, sorting puts the strip the other + way round too. +*/ +void tst_terminalstriporder::sortFollowsMovedElements() +{ + auto *strip_ = strip(); + QVERIFY(strip_); + Element *a_ = element(QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}")); + Element *b_ = element(QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}")); + QVERIFY(a_); + QVERIFY(b_); + + b_->setPos(100, 400); + a_->setPos(700, 400); + + SortTerminalStripCommand command_(strip_); + command_.redo(); + + QCOMPARE(physicalOrder(), + QStringList({ + QStringLiteral("{4861e268-d344-413e-a86f-034ea5ada16c}"), + QStringLiteral("{891b70f2-2822-41c3-9f94-28efb448b5c4}"), + })); +} + +int main(int argc, char **argv) +{ + QStandardPaths::setTestModeEnabled(true); + return QetAppTest::run(argc, argv); +} + +#include "tst_terminalstriporder.moc"