From c1cc9a5b98b0aaaf18e4548e513b00b3411e1901 Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Thu, 24 Sep 2026 09:59:21 +0200 Subject: [PATCH 1/3] Show all master-defined slaves in the contact comb behind a new option Add a new cross reference setting per xref type (coil, protection, commutator, PLC), labelled "Afficher tous les esclaves definis par le maitre" and persisted as showallconfiguredslaves. When it is enabled, the contacts display is selected and the master declares contact groups, the contact comb draws every contact group of the master in the master's own order, even when no slave is linked to it yet. Masters without declared contact groups and the option turned off keep the previous behaviour exactly: linked slaves only, sorted by position. - XRefProperties: new property stored in the settings and in the project XML (attribute showallconfiguredslaves, absent means false so old files are unaffected), included in operator==. - XRefPropertiesWidget: new checkbox placed after the terminal names one, enabled only while "Afficher en contacts" is selected; its enabled state is now also set explicitly when a type is loaded (a radio button that does not change emits no toggled()). - For the PLC type the contacts/cross radios, the two display checkboxes and the cross options group are hidden: a PLC master is always drawn as its IO table, those settings have no effect there. Positioning and label settings, which the table really uses, stay. - CrossRefItem: free slots draw the symbol of the group plus the terminal names the master defines (pairs swapped for a single pole NO/NC contact, labels of a changeover contact rotated one step counter-clockwise), without position text and without hover/click. Linked slaves keep drawing from their own data at their assigned group position; links without a group are appended at the end in position order. - Xref lifecycle: the item is created and kept without linked slaves for snap-to-bottom (MasterElement::mustShowXrefWithoutSlave) and for snap-to-label (DynamicElementTextItem::updateXref and ElementTextItemGroup::updateXref, which now also run when the element lands on the scene and re-establish their project connection), so a freshly placed master shows its comb immediately instead of only after the next settings change. updateLabel() resets its geometry when the option is turned off again, so no stale ghost stays. --- sources/properties/xrefproperties.cpp | 6 + sources/properties/xrefproperties.h | 4 + sources/qetgraphicsitem/crossrefitem.cpp | 321 ++++++++++++++---- sources/qetgraphicsitem/crossrefitem.h | 15 +- .../dynamicelementtextitem.cpp | 14 +- .../qetgraphicsitem/elementtextitemgroup.cpp | 48 ++- .../qetgraphicsitem/elementtextitemgroup.h | 3 + sources/qetgraphicsitem/masterelement.cpp | 32 ++ sources/qetgraphicsitem/masterelement.h | 1 + sources/ui/xrefpropertieswidget.cpp | 23 ++ sources/ui/xrefpropertieswidget.ui | 11 + 11 files changed, 406 insertions(+), 72 deletions(-) diff --git a/sources/properties/xrefproperties.cpp b/sources/properties/xrefproperties.cpp index c14c7c643..601fb02ef 100644 --- a/sources/properties/xrefproperties.cpp +++ b/sources/properties/xrefproperties.cpp @@ -30,6 +30,7 @@ XRefProperties::XRefProperties() { m_show_power_ctc = true; m_show_terminal_name = true; + m_show_all_configured_slaves = false; m_display = Cross; m_snap_to = Bottom; m_prefix_keys << "power" << "delay" << "switch"; @@ -51,6 +52,7 @@ void XRefProperties::toSettings(QSettings &settings, { settings.setValue(prefix % "showpowerctc", m_show_power_ctc); settings.setValue(prefix % "showterminalname", m_show_terminal_name); + settings.setValue(prefix % "showallconfiguredslaves", m_show_all_configured_slaves); QString display = m_display == Cross? "cross" : "contacts"; settings.setValue(prefix % "displayhas", display); QString snap = m_snap_to == Bottom? "bottom" : "label"; @@ -84,6 +86,7 @@ void XRefProperties::fromSettings(const QSettings &settings, { m_show_power_ctc = settings.value(prefix % "showpowerctc", true).toBool(); m_show_terminal_name = settings.value(prefix % "showterminalname", true).toBool(); + m_show_all_configured_slaves = settings.value(prefix % "showallconfiguredslaves", false).toBool(); QString display = settings.value(prefix % "displayhas", "cross").toString(); display == "cross"? m_display = Cross : m_display = Contacts; QString snap = settings.value(prefix % "snapto", "label").toString(); @@ -115,6 +118,7 @@ QDomElement XRefProperties::toXml(QDomDocument &xml_document) const xml_element.setAttribute("showpowerctc", m_show_power_ctc? "true" : "false"); xml_element.setAttribute("showterminalname", m_show_terminal_name? "true" : "false"); + xml_element.setAttribute("showallconfiguredslaves", m_show_all_configured_slaves? "true" : "false"); QString display = m_display == Cross? "cross" : "contacts"; xml_element.setAttribute("displayhas", display); QString snap = m_snap_to == Bottom? "bottom" : "label"; @@ -147,6 +151,7 @@ QDomElement XRefProperties::toXml(QDomDocument &xml_document) const bool XRefProperties::fromXml(const QDomElement &xml_element) { m_show_power_ctc = xml_element.attribute("showpowerctc") == "true"; m_show_terminal_name = xml_element.attribute("showterminalname", "true") == "true"; + m_show_all_configured_slaves = xml_element.attribute("showallconfiguredslaves", "false") == "true"; QString display = xml_element.attribute("displayhas", "cross"); display == "cross"? m_display = Cross : m_display = Contacts; QString snap = xml_element.attribute("snapto", "label"); @@ -200,6 +205,7 @@ QHash XRefProperties::defaultProperties() bool XRefProperties::operator ==(const XRefProperties &xrp) const{ return (m_show_power_ctc == xrp.m_show_power_ctc && m_show_terminal_name == xrp.m_show_terminal_name + && m_show_all_configured_slaves == xrp.m_show_all_configured_slaves && m_display == xrp.m_display && m_snap_to == xrp.m_snap_to && m_prefix == xrp.m_prefix diff --git a/sources/properties/xrefproperties.h b/sources/properties/xrefproperties.h index 572481f65..e2faa015e 100644 --- a/sources/properties/xrefproperties.h +++ b/sources/properties/xrefproperties.h @@ -60,6 +60,9 @@ class XRefProperties : public PropertiesInterface void setShowTerminalName (const bool a) {m_show_terminal_name = a;} bool showTerminalName () const {return m_show_terminal_name;} + void setShowAllConfiguredSlaves (const bool a) {m_show_all_configured_slaves = a;} + bool showAllConfiguredSlaves () const {return m_show_all_configured_slaves;} + void setDisplayHas (const DisplayHas dh) {m_display = dh;} DisplayHas displayHas () const {return m_display;} @@ -88,6 +91,7 @@ class XRefProperties : public PropertiesInterface private: bool m_show_power_ctc; bool m_show_terminal_name; + bool m_show_all_configured_slaves; DisplayHas m_display; SnapTo m_snap_to; Qt::AlignmentFlag m_xref_pos; diff --git a/sources/qetgraphicsitem/crossrefitem.cpp b/sources/qetgraphicsitem/crossrefitem.cpp index d0c0c9706..d5f89f0c7 100644 --- a/sources/qetgraphicsitem/crossrefitem.cpp +++ b/sources/qetgraphicsitem/crossrefitem.cpp @@ -30,6 +30,8 @@ #include "terminal.h" #include "../properties/elementdata.h" +#include + //define the height of the header. static int header = 5; //define the minimal height of the cross (without header) @@ -194,6 +196,37 @@ QString CrossRefItem::elementPositionText( return txt; } +/** + @brief CrossRefItem::showAllConfiguredSlaves + @param elmt : the element displaying the cross reference + @param xrp : xref properties of that element + @return true when the contact comb must show every slave contact the + master defines, even those no slave is linked to yet. That is the case + when the user asked for it, when the comb (contacts) display is the + one in use, and when the master really declares contact groups -- + an element which declares none behaves exactly as before. +*/ +bool CrossRefItem::showAllConfiguredSlaves( + const Element *elmt, + const XRefProperties &xrp) +{ + if (!elmt) return false; + if (!xrp.showAllConfiguredSlaves()) return false; + if (xrp.displayHas() != XRefProperties::Contacts) return false; + + return !elmt->elementData().m_slave_contact_groups.isEmpty(); +} + +/** + @brief CrossRefItem::mustDrawAllConfiguredSlaves + @return showAllConfiguredSlaves for the element of this item and the + current properties. +*/ +bool CrossRefItem::mustDrawAllConfiguredSlaves() const +{ + return showAllConfiguredSlaves(m_element, m_properties); +} + /** @brief CrossRefItem::updateProperties update the current properties @@ -256,8 +289,11 @@ void CrossRefItem::updateLabel() QTimer::singleShot(0, this, [this]{ update(); }); return; } - //Draw cross or contact, only if master element is linked. - else if (! m_element->linkedElements().isEmpty()) + //Draw cross or contact, if master element is linked, or if the user + //asks for the contact comb to show the contact groups of the master + //even before they get a slave. + else if (! m_element->linkedElements().isEmpty() + || mustDrawAllConfiguredSlaves()) { m_update_map = true; XRefProperties::DisplayHas dh = m_properties.displayHas(); @@ -358,7 +394,8 @@ void CrossRefItem::paint( if (m_element->elementData().m_master_type == ElementData::PLC) return; - if (m_element->linkedElements().isEmpty()) return; + if (m_element->linkedElements().isEmpty() + && !mustDrawAllConfiguredSlaves()) return; QPen pen_; pen_.setWidthF(0.5); @@ -727,6 +764,41 @@ void CrossRefItem::drawAsCross(QPainter &painter) fillCrossRef(painter); } +namespace { + /** + @brief contactOption + Map the contact group declared by a master onto the CONTACTS flags + used by CrossRefItem::drawContact, so a group no slave is linked to + yet is drawn like the slave it waits for. + @param group : the contact group of the master + @return the flags describing the contact to draw + */ + int contactOption(const ElementData::SlaveContactGroup &group) + { + int option = 0; + + switch (group.type) + { + case ElementData::NO: option = CrossRefItem::NO; break; + case ElementData::NC: option = CrossRefItem::NC; break; + case ElementData::SW: option = CrossRefItem::SW; break; + case ElementData::Other: option = CrossRefItem::Other; break; + } + + switch (group.subtype) + { + case ElementData::Power: option += CrossRefItem::Power; break; + case ElementData::DelayOn: option += CrossRefItem::DelayOn; break; + case ElementData::DelayOff: option += CrossRefItem::DelayOff; break; + case ElementData::delayOnOff: option += CrossRefItem::DelayOnOff; break; + case ElementData::SSimple: + case ElementData::PLCSlave: break; + } + + return option; + } +} + /** @brief CrossRefItem::drawAsContacts Draw this crossref with symbolic contacts @@ -734,37 +806,81 @@ void CrossRefItem::drawAsCross(QPainter &painter) */ void CrossRefItem::drawAsContacts(QPainter &painter) { - if (m_element -> isFree()) + if (m_element -> isFree() && !mustDrawAllConfiguredSlaves()) return; m_drawed_contacts = 0; if (m_update_map) m_hovered_contacts_map.clear(); QRectF bounding_rect; - //Draw each linked contact - foreach (Element *elmt, m_element->linkedElements()) + //Draw every contact group of the master, in the order the master + //defines them, when the user asked for it and the master declares + //contact groups. Otherwise the comb keeps its historical behavior: + //linked slaves only, in position order. + if (mustDrawAllConfiguredSlaves()) { - DiagramContext info = elmt->kindInformations(); + const QVector groups = + m_element->elementData().m_slave_contact_groups; - for (int i=0; i slotted; + QList unassigned; + foreach (Element *elmt, m_element->linkedElements()) //position order { - int option = 0; - - QString state = info["state"].toString(); - if (state == "NO") option = NO; - else if (state == "NC") option = NC; - else if (state == "SW") option = SW; - else if (state == "Other") option = Other; - - QString type = info["type"].toString(); - if (type == "power") option += Power; - else if (type == "delayOn") option += DelayOn; - else if (type == "delayOff") option += DelayOff; - else if (type == "delayOnOff") option += DelayOnOff; - - QRectF br = drawContact(painter, option, elmt, i); - bounding_rect = bounding_rect.united(br); + const int index = m_element->groupIndexForElement(elmt); + if (index >= 0 && index < groups.size() && !slotted.contains(index)) + slotted.insert(index, elmt); + else + unassigned << elmt; } + + for (int i = 0; i < groups.size(); ++i) + { + if (Element *slave = slotted.value(i, nullptr)) + bounding_rect = bounding_rect.united( + drawLinkedSlaveContacts(painter, slave)); + else + { + //No slave is linked to this group yet: the contact + //symbol of the group is drawn with the terminal names + //the master defines for it, there is no cross reference + //to show for it. + const int option = contactOption(groups.at(i)); + const int poles = qMax(1, groups.at(i).contactCount); + QStringList labels = groups.at(i).labels; + + //A single pole simple contact (NO or NC) reads its two + //numbers the other way round; changeover contacts and + //groups with several poles keep the order the master + //defines, they are already right. + if (poles == 1 && (option & NOC)) + std::reverse(labels.begin(), labels.end()); + + for (int pole = 0; pole < poles; ++pole) + bounding_rect = bounding_rect.united( + drawContact(painter, + option, + nullptr, + pole, + labels)); + } + } + + //Slaves the master doesn't assign to one of its groups (a link + //made before the master declared groups, for example) keep their + //usual place: the end of the comb, in position order. + foreach (Element *elmt, unassigned) + bounding_rect = bounding_rect.united( + drawLinkedSlaveContacts(painter, elmt)); + } + else + { + //Draw each linked contact, in position order + foreach (Element *elmt, m_element->linkedElements()) + bounding_rect = bounding_rect.united( + drawLinkedSlaveContacts(painter, elmt)); } bounding_rect.adjust(-30, -4, 4, 4); @@ -773,17 +889,61 @@ void CrossRefItem::drawAsContacts(QPainter &painter) m_shape_path.addRect(bounding_rect); } +/** + @brief CrossRefItem::drawLinkedSlaveContacts + Draw the contact symbols of one slave linked to this master. + @param painter : painter to use + @param elmt : the slave element to draw + @return the bounding rect of the draw +*/ +QRectF CrossRefItem::drawLinkedSlaveContacts(QPainter &painter, Element *elmt) +{ + QRectF bounding_rect; + DiagramContext info = elmt->kindInformations(); + + for (int i=0; ikindInformations()["type"].toString() == "power"; + (flags & Power) + || (elmt && elmt->kindInformations()["type"].toString() == "power"); const bool is_sw = (flags & SW) && !(flags & NOC); // Check if SW terminals have explicit No/Nc/Common types bool sw_has_typed_terminals = false; - if (is_sw) { + if (is_sw && elmt) { for (Terminal *t : elmt->terminals()) { if (!t) continue; if (t->terminalType() == TerminalData::No || @@ -808,14 +969,43 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in } } - for (Terminal *t : elmt->terminals()) { - if (!t) continue; - const QString tname = t->name(); - if (!tname.isEmpty()) - terminal_names << tname; + if (elmt) { + for (Terminal *t : elmt->terminals()) { + if (!t) continue; + const QString tname = t->name(); + if (!tname.isEmpty()) + terminal_names << tname; + } + } else if (!master_labels.isEmpty()) { + //Empty slot of the contact comb: the slave is missing but the + //master already declares the terminal names of the group, so the + //slot shows them instead of staying mute. The labels are a flat + //list covering every terminal of the group, in the order a linked + //slave would receive them: two per contact (or three for a switch). + if (is_sw) { + //The labels are stored in terminal order (for a typical + //changeover contact: common, NC, NO, i.e. 11, 12, 14), while + //the symbol draws NC bottom-left, NO top-left and the common + //on the right: one step counter-clockwise puts every number + //beside its own contact. + const int base = pole_index * 3; + QStringList trio; + for (int k = 0; k < 3; ++k) + if (base + k < master_labels.size()) + trio << master_labels.at(base + k); + if (trio.size() == 3) + terminal_names << trio.at(1) << trio.at(2) << trio.at(0); + else + terminal_names << trio; //incomplete set: keep stored order + } else { + const int base = pole_index * 2; + for (int k = 0; k < 2; ++k) + if (base + k < master_labels.size()) + terminal_names << master_labels.at(base + k); + } } - if (is_power_ctc) { + if (elmt && is_power_ctc) { // Sort terminals alphanumerically so names like "R1","R2"... or "1","2"... // are ordered correctly. Extract trailing digits for numeric comparison; // fall back to full string comparison when no digits are found. @@ -860,7 +1050,7 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in QRectF bounding_rect = QRectF(0, offset, 24, 10); QPen pen = painter.pen(); - m_hovered_contact == elmt ? pen.setColor(Qt::blue) :pen.setColor(Qt::black); + elmt && m_hovered_contact == elmt ? pen.setColor(Qt::blue) :pen.setColor(Qt::black); painter.setPen(pen); //Draw NO or NC contact @@ -956,12 +1146,15 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in } } - QRectF text_rect = painter.boundingRect(QRectF(30, offset, 5, 10), Qt::AlignLeft | Qt::AlignVCenter, str); - painter.drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, str); - bounding_rect = bounding_rect.united(text_rect); + if (!str.isEmpty()) + { + QRectF text_rect = painter.boundingRect(QRectF(30, offset, 5, 10), Qt::AlignLeft | Qt::AlignVCenter, str); + painter.drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, str); + bounding_rect = bounding_rect.united(text_rect); - if (m_update_map) - m_hovered_contacts_map.insert(elmt, text_rect); + if (m_update_map) + m_hovered_contacts_map.insert(elmt, text_rect); + } ++m_drawed_contacts; } @@ -1034,17 +1227,20 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in } //Draw position text - QRectF text_rect = painter.boundingRect( - QRectF(30, offset+4, 5, 10), - Qt::AlignLeft | Qt::AlignVCenter, - str); - painter.drawText(text_rect, - Qt::AlignLeft | Qt::AlignVCenter, - str); - bounding_rect = bounding_rect.united(text_rect); + if (!str.isEmpty()) + { + QRectF text_rect = painter.boundingRect( + QRectF(30, offset+4, 5, 10), + Qt::AlignLeft | Qt::AlignVCenter, + str); + painter.drawText(text_rect, + Qt::AlignLeft | Qt::AlignVCenter, + str); + bounding_rect = bounding_rect.united(text_rect); - if (m_update_map) - m_hovered_contacts_map.insert(elmt, text_rect); + if (m_update_map) + m_hovered_contacts_map.insert(elmt, text_rect); + } //a switch contact take place of two normal contact m_drawed_contacts += 2; @@ -1066,17 +1262,20 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in painter.drawArc(r2, 160*16, 300*16); //Draw position text - QRectF text_rect = painter.boundingRect( - QRectF(30, offset, 5, 10), - Qt::AlignLeft | Qt::AlignVCenter, - str); - painter.drawText(text_rect, - Qt::AlignLeft | Qt::AlignVCenter, - str); - bounding_rect = bounding_rect.united(text_rect); + if (!str.isEmpty()) + { + QRectF text_rect = painter.boundingRect( + QRectF(30, offset, 5, 10), + Qt::AlignLeft | Qt::AlignVCenter, + str); + painter.drawText(text_rect, + Qt::AlignLeft | Qt::AlignVCenter, + str); + bounding_rect = bounding_rect.united(text_rect); - if (m_update_map) - m_hovered_contacts_map.insert(elmt, text_rect); + if (m_update_map) + m_hovered_contacts_map.insert(elmt, text_rect); + } ++m_drawed_contacts; } return bounding_rect; diff --git a/sources/qetgraphicsitem/crossrefitem.h b/sources/qetgraphicsitem/crossrefitem.h index ec3cf7070..bff9c13e8 100644 --- a/sources/qetgraphicsitem/crossrefitem.h +++ b/sources/qetgraphicsitem/crossrefitem.h @@ -63,6 +63,13 @@ class CrossRefItem : public QGraphicsObject enum { Type = UserType + 1009 }; int type() const override { return Type; } + /// Returns true when \a xrp asks the contact comb of \a elmt to show + /// every slave contact the master defines, even the ones no slave is + /// linked to yet. \a elmt must be a master element. + static bool showAllConfiguredSlaves( + const Element *elmt, + const XRefProperties &xrp); + /** @brief The CONTACTS enum */ @@ -121,7 +128,13 @@ class CrossRefItem : public QGraphicsObject void drawAsCross(QPainter &painter); void drawAsContacts(QPainter &painter); void drawAsPlcTable(QPainter &painter); - QRectF drawContact(QPainter &painter, int flags, Element *elmt, int pole_index = 0); + bool mustDrawAllConfiguredSlaves() const; + QRectF drawLinkedSlaveContacts(QPainter &painter, Element *elmt); + QRectF drawContact(QPainter &painter, + int flags, + Element *elmt, + int pole_index = 0, + const QStringList &master_labels = QStringList()); void fillCrossRef(QPainter &painter); void AddExtraInfo(QPainter &painter, const QString&); QList NOElements() const; diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.cpp b/sources/qetgraphicsitem/dynamicelementtextitem.cpp index 53b43ca8b..a42dd5442 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.cpp +++ b/sources/qetgraphicsitem/dynamicelementtextitem.cpp @@ -822,8 +822,12 @@ QVariant DynamicElementTextItem::itemChange(QGraphicsItem::GraphicsItemChange ch connect(m_parent_element.data(), &Element::linkedElementChanged, this, &DynamicElementTextItem::updateXref); if(m_parent_element.data()->diagram()) connect(m_parent_element.data()->diagram()->project(), &QETProject::XRefPropertiesChanged, this, &DynamicElementTextItem::updateXref); - if(!m_parent_element.data()->linkedElements().isEmpty()) - updateXref(); + //Also call updateXref for a master without any linked slave: + //when the contact comb must show every contact group the master + //defines, the cross ref is expected the moment the element lands + //on the diagram, not only after the first link or the first + //settings change. + updateXref(); } m_first_scene_change = false; @@ -1587,8 +1591,10 @@ void DynamicElementTextItem::updateXref() if(m_text_from == DynamicElementTextItem::ElementInfo && m_info_name == "label" && - !m_parent_element.data()->linkedElements().isEmpty() && - xrp.snapTo() == XRefProperties::Label) + xrp.snapTo() == XRefProperties::Label && + (!m_parent_element.data()->linkedElements().isEmpty() + || CrossRefItem::showAllConfiguredSlaves( + m_parent_element.data(), xrp))) { //For add a Xref, this text must not be in a group if(!parentGroup()) diff --git a/sources/qetgraphicsitem/elementtextitemgroup.cpp b/sources/qetgraphicsitem/elementtextitemgroup.cpp index a9cdeb6c0..253c10146 100644 --- a/sources/qetgraphicsitem/elementtextitemgroup.cpp +++ b/sources/qetgraphicsitem/elementtextitemgroup.cpp @@ -60,12 +60,48 @@ ElementTextItemGroup::ElementTextItemGroup(const QString &name, this, &ElementTextItemGroup::updateXref); if(parent->diagram()) - connect(parent->diagram()->project(), + m_project_xref_connection = connect( + parent->diagram()->project(), &QETProject::XRefPropertiesChanged, this, &ElementTextItemGroup::updateXref); } +/** + @brief ElementTextItemGroup::itemChange + The group is very often built while its element is not on a scene yet + (project or element loading): the connection to the project was then + impossible and the first updateXref() ran without a diagram, so the + cross ref of a master waiting for its slaves stayed invisible until an + unrelated settings change happened to refresh it. Do both here, the + moment the group really reaches the scene. + @param change + @param value + @return +*/ +QVariant ElementTextItemGroup::itemChange( + QGraphicsItem::GraphicsItemChange change, + const QVariant &value) +{ + if (change == QGraphicsItem::ItemSceneHasChanged) + { + if (m_parent_element + && m_parent_element->diagram() + && m_parent_element->diagram()->project()) + { + QETProject *project = m_parent_element->diagram()->project(); + if (!m_project_xref_connection) + m_project_xref_connection = connect( + project, + &QETProject::XRefPropertiesChanged, + this, + &ElementTextItemGroup::updateXref); + updateXref(); + } + } + return QGraphicsItemGroup::itemChange(change, value); +} + ElementTextItemGroup::~ElementTextItemGroup() {} @@ -772,13 +808,13 @@ void ElementTextItemGroup::updateXref() { QETProject *project = m_parent_element->diagram()->project(); - if(m_parent_element->linkType() == Element::Master && - !m_parent_element->linkedElements().isEmpty()) + if(m_parent_element->linkType() == Element::Master) { - XRefProperties xrp = project->defaultXRefProperties(m_parent_element->kindInformations()["type"].toString()); - - if(xrp.snapTo() == XRefProperties::Label) + + if(xrp.snapTo() == XRefProperties::Label && + (!m_parent_element->linkedElements().isEmpty() + || CrossRefItem::showAllConfiguredSlaves(m_parent_element, xrp))) { //At least one text owned by this group must be set with //textFrom -> element info and element info name -> label diff --git a/sources/qetgraphicsitem/elementtextitemgroup.h b/sources/qetgraphicsitem/elementtextitemgroup.h index da1d87e66..1e1e3c082 100644 --- a/sources/qetgraphicsitem/elementtextitemgroup.h +++ b/sources/qetgraphicsitem/elementtextitemgroup.h @@ -101,6 +101,8 @@ class ElementTextItemGroup : public QObject, public QGraphicsItemGroup void keyPressEvent(QKeyEvent *event) override; void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override; void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override; + QVariant itemChange(GraphicsItemChange change, + const QVariant &value) override; private: void updateXref(); @@ -120,6 +122,7 @@ class ElementTextItemGroup : public QObject, public QGraphicsItemGroup Element *m_parent_element = nullptr; QList m_update_slave_Xref_connection; QGraphicsTextItem *m_slave_Xref_item = nullptr; + QMetaObject::Connection m_project_xref_connection; QMetaObject::Connection m_XrefChanged_timer, m_linked_changed_timer; }; diff --git a/sources/qetgraphicsitem/masterelement.cpp b/sources/qetgraphicsitem/masterelement.cpp index 2c94f2627..e28e27563 100644 --- a/sources/qetgraphicsitem/masterelement.cpp +++ b/sources/qetgraphicsitem/masterelement.cpp @@ -167,6 +167,13 @@ QVariant MasterElement::itemChange(QGraphicsItem::GraphicsItemChange change, con m_Xref_item = new CrossRefItem(this); m_Xref_item->updateLabel(); } + // Same idea for a master whose contact comb must show every + // contact group it defines: the cross ref is expected even + // before any slave is linked to it. + else if (!m_Xref_item && mustShowXrefWithoutSlave()) + { + m_Xref_item = new CrossRefItem(this); + } } return Element::itemChange(change, value); } @@ -193,6 +200,26 @@ void MasterElement::xrefPropertiesChanged() aboutDeleteXref(); } +/** + @brief MasterElement::mustShowXrefWithoutSlave + @return true when the cross ref of this master has to be shown even + though no slave is linked to it yet: the user asks the contact comb to + display every contact group the master defines, the comb (contacts) + display is the one in use, and the cross ref is owned by the element + itself (snap to bottom). +*/ +bool MasterElement::mustShowXrefWithoutSlave() const +{ + if (!diagram() || !diagram()->project()) + return false; + + const XRefProperties xrp = diagram()->project()->defaultXRefProperties( + kindInformations()["type"].toString()); + + return xrp.snapTo() == XRefProperties::Bottom + && CrossRefItem::showAllConfiguredSlaves(this, xrp); +} + /** @brief MasterElement::aboutDeleteXref Check if Xref item must be displayed, if not, delete it. @@ -220,6 +247,11 @@ void MasterElement::aboutDeleteXref() return; } + // The contact comb shows the contact groups the master defines, linked + // or not: keep the item even when it draws nothing so far. + if (mustShowXrefWithoutSlave()) + return; + if (m_Xref_item->boundingRect().isNull()) { delete m_Xref_item; diff --git a/sources/qetgraphicsitem/masterelement.h b/sources/qetgraphicsitem/masterelement.h index 2892e68e1..638143387 100644 --- a/sources/qetgraphicsitem/masterelement.h +++ b/sources/qetgraphicsitem/masterelement.h @@ -60,6 +60,7 @@ class MasterElement : public Element private: void xrefPropertiesChanged(); void aboutDeleteXref (); + bool mustShowXrefWithoutSlave() const; void connectSlavePositionUpdates(Element *slave); void disconnectSlavePositionUpdates(Element *slave); diff --git a/sources/ui/xrefpropertieswidget.cpp b/sources/ui/xrefpropertieswidget.cpp index 97f4dadf8..c113eda45 100644 --- a/sources/ui/xrefpropertieswidget.cpp +++ b/sources/ui/xrefpropertieswidget.cpp @@ -38,6 +38,7 @@ XRefPropertiesWidget::XRefPropertiesWidget(QHash prope ui->setupUi(this); buildUi(); connect(ui->m_display_has_cross_rb, &QRadioButton::toggled, ui->m_cross_properties_gb, &QWidget::setEnabled); + connect(ui->m_display_has_contacts_rb, &QRadioButton::toggled, ui->m_show_all_slaves_cb, &QWidget::setEnabled); connect(ui->m_type_cb, qOverload(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::typeChanged); connect(ui->m_snap_to_cb, qOverload(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::enableOffsetSB); updateDisplay(); @@ -50,6 +51,7 @@ XRefPropertiesWidget::XRefPropertiesWidget(QHash prope XRefPropertiesWidget::~XRefPropertiesWidget() { disconnect(ui->m_display_has_cross_rb, &QRadioButton::toggled, ui->m_cross_properties_gb, &QWidget::setEnabled); + disconnect(ui->m_display_has_contacts_rb, &QRadioButton::toggled, ui->m_show_all_slaves_cb, &QWidget::setEnabled); disconnect(ui->m_type_cb, qOverload(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::typeChanged); disconnect(ui->m_snap_to_cb, qOverload(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::enableOffsetSB); delete ui; @@ -143,6 +145,7 @@ void XRefPropertiesWidget::saveProperties(int index) { else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "text_field") xrp.setXrefPos(Qt::AlignHCenter); xrp.setShowPowerContac(ui->m_show_power_cb->isChecked()); xrp.setShowTerminalName(ui->m_show_terminal_name_cb->isChecked()); + xrp.setShowAllConfiguredSlaves(ui->m_show_all_slaves_cb->isChecked()); xrp.setPrefix("power", ui->m_power_prefix_le->text()); xrp.setPrefix("delay", ui->m_delay_prefix_le->text()); xrp.setPrefix("switch", ui->m_switch_prefix_le->text()); @@ -200,10 +203,30 @@ void XRefPropertiesWidget::updateDisplay() else if(xrp.getXrefPos() == Qt::AlignHCenter) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("text_field")); ui->m_show_power_cb->setChecked(xrp.showPowerContact()); ui->m_show_terminal_name_cb->setChecked(xrp.showTerminalName()); + ui->m_show_all_slaves_cb->setChecked(xrp.showAllConfiguredSlaves()); + //The radio button only emits toggled() when it really changes: loading + //a type whose display did not change left the checkbox with the enabled + //state of the previously displayed type (it stayed clickable although + //the cross display was selected). Set the state explicitly here. + ui->m_show_all_slaves_cb->setEnabled( + ui->m_display_has_contacts_rb->isChecked()); ui->m_power_prefix_le-> setText(xrp.prefix("power")); ui->m_delay_prefix_le-> setText(xrp.prefix("delay")); ui->m_switch_prefix_le->setText(xrp.prefix("switch")); ui->m_cross_properties_gb->setDisabled(!ui->m_display_has_cross_rb->isChecked()); + + //The cross ref of a PLC master is always drawn as its IO table, and + //the slaves are referenced directly into that table: the contacts/ + //cross choice, the two display checkboxes and the cross options below + //have no effect at all for this type, so they are hidden instead of + //being offered for nothing. The positioning settings and the labels + //(the table really uses them) stay available. + const bool is_plc = type == QLatin1String("plc"); + ui->m_display_has_contacts_rb->setVisible(!is_plc); + ui->m_display_has_cross_rb->setVisible(!is_plc); + ui->m_show_terminal_name_cb->setVisible(!is_plc); + ui->m_show_all_slaves_cb->setVisible(!is_plc); + ui->m_cross_properties_gb->setVisible(!is_plc); } /** diff --git a/sources/ui/xrefpropertieswidget.ui b/sources/ui/xrefpropertieswidget.ui index a6bf89d39..e79070539 100644 --- a/sources/ui/xrefpropertieswidget.ui +++ b/sources/ui/xrefpropertieswidget.ui @@ -151,6 +151,16 @@ + + + + Afficher dans le peigne de contacts tous les contacts esclaves définis par le maître, même ceux qui ne sont pas encore reliés, dans l'ordre défini par le maître + + + Afficher tous les esclaves définis par le maître + + + @@ -327,6 +337,7 @@ m_slave_le m_show_power_cb m_show_terminal_name_cb + m_show_all_slaves_cb m_power_prefix_le m_delay_prefix_le m_switch_prefix_le From 800189debb7ce7872eec43c3b12d9a66f286dbfd Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Thu, 24 Sep 2026 15:11:31 +0200 Subject: [PATCH 2/3] Address review: restore hover entries for empty position text - Call updateLabel() explicitly when the xref is created in itemChange for a master that must show its configured contact groups without slaves (same pattern as the PLC branch above). - Register the hover/click hit rect of a contact independently of its position text again, as before this feature: only the drawing stays guarded by !str.isEmpty(), and the map insert is now keyed on elmt so free slots (nullptr) never enter the map. - Revert is_power_ctc to the original element-type test (with a null guard): the Power-flag term was redundant for every caller that passes an element, so no linked contact changes classification. - Clarify the label-order comment: single pole NO/NC are swapped, changeover labels are rotated per pole inside drawContact() (multi pole included), multi pole NO/NC groups keep the master order. --- sources/qetgraphicsitem/crossrefitem.cpp | 53 +++++++++++++---------- sources/qetgraphicsitem/masterelement.cpp | 1 + 2 files changed, 32 insertions(+), 22 deletions(-) diff --git a/sources/qetgraphicsitem/crossrefitem.cpp b/sources/qetgraphicsitem/crossrefitem.cpp index d5f89f0c7..836360c08 100644 --- a/sources/qetgraphicsitem/crossrefitem.cpp +++ b/sources/qetgraphicsitem/crossrefitem.cpp @@ -852,9 +852,13 @@ void CrossRefItem::drawAsContacts(QPainter &painter) QStringList labels = groups.at(i).labels; //A single pole simple contact (NO or NC) reads its two - //numbers the other way round; changeover contacts and - //groups with several poles keep the order the master - //defines, they are already right. + //numbers the other way round (checked against the + //diagram). Changeover contacts are not handled here: + //their labels are rotated one step counter-clockwise + //inside drawContact(), per pole, so multi pole + //changeovers work too. Groups with several NO/NC poles + //keep the order the master defines: a 3 pole power + //contact already reads correctly that way. if (poles == 1 && (option & NOC)) std::reverse(labels.begin(), labels.end()); @@ -951,8 +955,7 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in // For SW contacts with typed terminals (No/Nc/Common), filter by role. QStringList terminal_names; const bool is_power_ctc = - (flags & Power) - || (elmt && elmt->kindInformations()["type"].toString() == "power"); + elmt && elmt->kindInformations()["type"].toString() == "power"; const bool is_sw = (flags & SW) && !(flags & NOC); // Check if SW terminals have explicit No/Nc/Common types @@ -1146,16 +1149,20 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in } } + //The hit rect is registered even when the position text is + //empty: a linked contact had (and keeps) its hover/click entry + //in that case too, only the drawing is skipped. Free slots + //(elmt == nullptr) have nothing to click and stay out of the map. + QRectF text_rect = painter.boundingRect(QRectF(30, offset, 5, 10), Qt::AlignLeft | Qt::AlignVCenter, str); if (!str.isEmpty()) { - QRectF text_rect = painter.boundingRect(QRectF(30, offset, 5, 10), Qt::AlignLeft | Qt::AlignVCenter, str); painter.drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, str); bounding_rect = bounding_rect.united(text_rect); - - if (m_update_map) - m_hovered_contacts_map.insert(elmt, text_rect); } + if (m_update_map && elmt) + m_hovered_contacts_map.insert(elmt, text_rect); + ++m_drawed_contacts; } @@ -1227,20 +1234,21 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in } //Draw position text + QRectF text_rect = painter.boundingRect( + QRectF(30, offset+4, 5, 10), + Qt::AlignLeft | Qt::AlignVCenter, + str); if (!str.isEmpty()) { - QRectF text_rect = painter.boundingRect( - QRectF(30, offset+4, 5, 10), - Qt::AlignLeft | Qt::AlignVCenter, - str); painter.drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, str); bounding_rect = bounding_rect.united(text_rect); - - if (m_update_map) - m_hovered_contacts_map.insert(elmt, text_rect); } + //Hit rect kept even for an empty position text (as before), + //free slots are not clickable. + if (m_update_map && elmt) + m_hovered_contacts_map.insert(elmt, text_rect); //a switch contact take place of two normal contact m_drawed_contacts += 2; @@ -1262,20 +1270,21 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in painter.drawArc(r2, 160*16, 300*16); //Draw position text - if (!str.isEmpty()) - { - QRectF text_rect = painter.boundingRect( + QRectF text_rect = painter.boundingRect( QRectF(30, offset, 5, 10), Qt::AlignLeft | Qt::AlignVCenter, str); + if (!str.isEmpty()) + { painter.drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, str); bounding_rect = bounding_rect.united(text_rect); - - if (m_update_map) - m_hovered_contacts_map.insert(elmt, text_rect); } + //Hit rect kept even for an empty position text (as before), + //free slots are not clickable. + if (m_update_map && elmt) + m_hovered_contacts_map.insert(elmt, text_rect); ++m_drawed_contacts; } return bounding_rect; diff --git a/sources/qetgraphicsitem/masterelement.cpp b/sources/qetgraphicsitem/masterelement.cpp index e28e27563..52f82551b 100644 --- a/sources/qetgraphicsitem/masterelement.cpp +++ b/sources/qetgraphicsitem/masterelement.cpp @@ -173,6 +173,7 @@ QVariant MasterElement::itemChange(QGraphicsItem::GraphicsItemChange change, con else if (!m_Xref_item && mustShowXrefWithoutSlave()) { m_Xref_item = new CrossRefItem(this); + m_Xref_item->updateLabel(); } } return Element::itemChange(change, value); From 5622308ba2fd3e7c4af1e81f3eff059f96841597 Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Fri, 25 Sep 2026 11:24:35 +0200 Subject: [PATCH 3/3] Derive empty-slot labels from the group's declared terminals Both issues from the review, preview of empty contact comb slots only: - Slice the flat label list over the declared poles in drawAsContacts (terminals per pole from labels.size()/contactCount) instead of a fixed stride of 2 (3 for a switch) inside drawContact: terminalCount and contactCount are edited independently, so with 3 poles and the default terminal count of 2 the fixed stride starved every pole but the first. Available labels are now distributed across the poles. - Map changeover labels of a slot to their own position always (common=stored[0] right, NC=stored[1] bottom-left, NO=stored[2] top-left, missing entries stay empty) instead of falling back to the raw stored order, which put the numbers on the wrong contact halves when the terminal count was below three. - Element editor: changing the contact count now keeps the terminal count in step (same terminals per contact, type default 2/3 for inconsistent data, minimum 3 for a switch), so the mismatch cannot be created anymore; legacy mismatched data is handled by the new slicing. - Drop the now redundant elmt check before is_power_ctc (it already includes it) - the dead null check from the review. --- .../ui/elementpropertieseditorwidget.cpp | 23 ++++++ sources/qetgraphicsitem/crossrefitem.cpp | 70 +++++++++++-------- 2 files changed, 62 insertions(+), 31 deletions(-) diff --git a/sources/editor/ui/elementpropertieseditorwidget.cpp b/sources/editor/ui/elementpropertieseditorwidget.cpp index 6680b9152..66d6b2237 100644 --- a/sources/editor/ui/elementpropertieseditorwidget.cpp +++ b/sources/editor/ui/elementpropertieseditorwidget.cpp @@ -588,6 +588,29 @@ void ElementPropertiesEditorWidget::populateSlaveGroupsTable() contact_ct->setValue(group.contactCount); ui->m_slave_groups_table->setCellWidget(i, 2, contact_ct); + // When the contact count changes, keep the terminal count in step + // with it, otherwise the two drift apart (both are edited + // independently): the stored terminals-per-contact ratio is kept, + // or the contact type default (2, 3 for a switch) is used when the + // stored values don't divide evenly (inconsistent legacy data). + const int old_contacts = group.contactCount; + const int old_terminals = group.terminalCount; + connect(contact_ct, QOverload::of(&QSpinBox::valueChanged), + this, [this, i, old_contacts, old_terminals](int val) { + if (i < m_data.m_slave_contact_groups.size()) { + readSlaveGroupsFromTable(); + auto &group = m_data.m_slave_contact_groups[i]; + int per_pole = old_terminals / qMax(1, old_contacts); + if (per_pole < 1 || old_terminals % qMax(1, old_contacts) != 0) + per_pole = group.type == ElementData::SW ? 3 : 2; + else if (group.type == ElementData::SW && per_pole < 3) + per_pole = 3; //a switch needs common, NC and NO + group.contactCount = val; + group.terminalCount = val * per_pole; + populateSlaveGroupsTable(); + } + }); + // Terminal count auto *terminal_ct = new QSpinBox(ui->m_slave_groups_table); terminal_ct->setMinimum(1); diff --git a/sources/qetgraphicsitem/crossrefitem.cpp b/sources/qetgraphicsitem/crossrefitem.cpp index 836360c08..60b0f7ca9 100644 --- a/sources/qetgraphicsitem/crossrefitem.cpp +++ b/sources/qetgraphicsitem/crossrefitem.cpp @@ -854,21 +854,35 @@ void CrossRefItem::drawAsContacts(QPainter &painter) //A single pole simple contact (NO or NC) reads its two //numbers the other way round (checked against the //diagram). Changeover contacts are not handled here: - //their labels are rotated one step counter-clockwise - //inside drawContact(), per pole, so multi pole - //changeovers work too. Groups with several NO/NC poles - //keep the order the master defines: a 3 pole power - //contact already reads correctly that way. + //their labels are mapped to the right contact half in + //drawContact(), per pole, so multi pole changeovers work + //too. Groups with several NO/NC poles keep the order the + //master defines: a 3 pole power contact already reads + //correctly that way. if (poles == 1 && (option & NOC)) std::reverse(labels.begin(), labels.end()); + //The declared terminals are distributed over the declared + //poles. terminalCount and contactCount are edited + //independently in the element editor, so the list can be + //shorter than two entries per pole (three for a switch): + //every pole then gets its share of what exists, instead + //of a fixed 2/3 stride starving all but the first poles. + const int per_pole = labels.size() / poles; + const int extra = labels.size() % poles; + int begin = 0; for (int pole = 0; pole < poles; ++pole) + { + const int count = per_pole + (pole < extra ? 1 : 0); + const QStringList pole_labels = labels.mid(begin, count); + begin += count; bounding_rect = bounding_rect.united( drawContact(painter, option, nullptr, pole, - labels)); + pole_labels)); + } } } @@ -938,11 +952,11 @@ QRectF CrossRefItem::drawLinkedSlaveContacts(QPainter &painter, Element *elmt) defines but no slave is linked to yet: no position text, no hover/click support, and the terminal names then come from master_labels. @param pole_index : which contact of the group is drawn (0 based), used - to pick the right pair/triplet of terminal names. - @param master_labels : the terminal names the master defines for this - contact group (ElementData::SlaveContactGroup::labels), used when elmt - is nullptr so an empty slot shows the numbers the master declares, the - same way a linked slave would show them. + to pick the right pair of terminal names of a linked multi-pole contact. + @param master_labels : the terminal names the master declares for this + pole (sliced from ElementData::SlaveContactGroup::labels by the caller), + used when elmt is nullptr so an empty slot shows the numbers the master + declares, the same way a linked slave would show them. @return The bounding rect of the draw (contact + text) */ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, int pole_index, const QStringList &master_labels) @@ -981,34 +995,28 @@ QRectF CrossRefItem::drawContact(QPainter &painter, int flags, Element *elmt, in } } else if (!master_labels.isEmpty()) { //Empty slot of the contact comb: the slave is missing but the - //master already declares the terminal names of the group, so the - //slot shows them instead of staying mute. The labels are a flat - //list covering every terminal of the group, in the order a linked - //slave would receive them: two per contact (or three for a switch). + //master already declares the terminal names, so the slot shows + //them instead of staying mute. master_labels contains exactly + //the terminals of this pole (the caller slices the declared + //terminal list over the declared poles), in the order a linked + //slave would receive them. if (is_sw) { //The labels are stored in terminal order (for a typical //changeover contact: common, NC, NO, i.e. 11, 12, 14), while //the symbol draws NC bottom-left, NO top-left and the common - //on the right: one step counter-clockwise puts every number - //beside its own contact. - const int base = pole_index * 3; - QStringList trio; - for (int k = 0; k < 3; ++k) - if (base + k < master_labels.size()) - trio << master_labels.at(base + k); - if (trio.size() == 3) - terminal_names << trio.at(1) << trio.at(2) << trio.at(0); - else - terminal_names << trio; //incomplete set: keep stored order + //on the right: every stored entry goes to its own position. + //Entries the master doesn't declare (terminal count lower + //than three) simply stay empty instead of landing on the + //wrong contact half like the raw stored order would. + terminal_names << master_labels.value(1) + << master_labels.value(2) + << master_labels.value(0); } else { - const int base = pole_index * 2; - for (int k = 0; k < 2; ++k) - if (base + k < master_labels.size()) - terminal_names << master_labels.at(base + k); + terminal_names = master_labels; } } - if (elmt && is_power_ctc) { + if (is_power_ctc) { // Sort terminals alphanumerically so names like "R1","R2"... or "1","2"... // are ordered correctly. Extract trailing digits for numeric comparison; // fall back to full string comparison when no digits are found.