From b025bd205dd8e01822233a011ef84e95e7e9649b Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Fri, 17 Jul 2026 09:47:32 +0200 Subject: [PATCH] =?UTF-8?q?Implement=20slave=20contact=20groups=20?= =?UTF-8?q?=E2=80=94=20label=20transfer,=20terminal=20assignment=20and=20U?= =?UTF-8?q?I=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- cmake/qet_compilation_vars.cmake | 2 + sources/editor/elementscene.cpp | 3 + sources/editor/elementscene.h | 2 + sources/editor/graphicspart/partterminal.cpp | 16 ++ sources/editor/graphicspart/partterminal.h | 10 + .../ui/elementpropertieseditorwidget.cpp | 235 ++++++++++++++++- .../editor/ui/elementpropertieseditorwidget.h | 4 + .../ui/elementpropertieseditorwidget.ui | 49 ++++ sources/editor/ui/qetelementeditor.cpp | 6 + sources/editor/ui/terminaleditor.cpp | 128 ++++++++++ sources/editor/ui/terminaleditor.h | 13 +- sources/editor/ui/terminaleditor.ui | 26 ++ sources/properties/elementdata.cpp | 114 +++++++-- sources/properties/elementdata.h | 35 +++ sources/properties/terminaldata.cpp | 10 + sources/properties/terminaldata.h | 5 + sources/qetgraphicsitem/element.cpp | 56 ++++- sources/qetgraphicsitem/element.h | 20 +- sources/qetgraphicsitem/terminal.cpp | 55 +++- sources/qetgraphicsitem/terminal.h | 4 + sources/qetproject.cpp | 16 ++ sources/ui/contactgroupselectiondialog.cpp | 238 ++++++++++++++++++ sources/ui/contactgroupselectiondialog.h | 61 +++++ sources/ui/linksingleelementwidget.cpp | 40 ++- sources/ui/linksingleelementwidget.h | 4 + sources/ui/masterpropertieswidget.cpp | 51 ++++ sources/ui/masterpropertieswidget.h | 2 + sources/undocommand/linkelementcommand.cpp | 126 ++++++++++ sources/undocommand/linkelementcommand.h | 5 + 29 files changed, 1305 insertions(+), 31 deletions(-) create mode 100644 sources/ui/contactgroupselectiondialog.cpp create mode 100644 sources/ui/contactgroupselectiondialog.h diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index 88366a91a..211c06b72 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -616,6 +616,8 @@ set(QET_SRC_FILES ${QET_DIR}/sources/ui/borderpropertieswidget.h ${QET_DIR}/sources/ui/compositetexteditdialog.cpp ${QET_DIR}/sources/ui/compositetexteditdialog.h + ${QET_DIR}/sources/ui/contactgroupselectiondialog.cpp + ${QET_DIR}/sources/ui/contactgroupselectiondialog.h ${QET_DIR}/sources/ui/conductorpropertiesdialog.cpp ${QET_DIR}/sources/ui/conductorpropertiesdialog.h ${QET_DIR}/sources/ui/conductorpropertieswidget.cpp diff --git a/sources/editor/elementscene.cpp b/sources/editor/elementscene.cpp index bd2c9fdd4..7d139b058 100644 --- a/sources/editor/elementscene.cpp +++ b/sources/editor/elementscene.cpp @@ -85,11 +85,14 @@ ElementData ElementScene::elementData() { void ElementScene::setElementData(ElementData data) { bool emit_ = m_element_data.m_informations != data.m_informations; + bool type_changed = m_element_data.m_type != data.m_type; m_element_data = data; if (emit_) emit elementInfoChanged(); + if (type_changed) + emit elementTypeChanged(); } /** diff --git a/sources/editor/elementscene.h b/sources/editor/elementscene.h index 3a3362b72..f3d77748c 100644 --- a/sources/editor/elementscene.h +++ b/sources/editor/elementscene.h @@ -177,6 +177,8 @@ class ElementScene : public QGraphicsScene /// Signal emitted when need zoomFit void needZoomFit(); void elementInfoChanged(); + /// Signal emitted when the element type changes + void elementTypeChanged(); }; Q_DECLARE_OPERATORS_FOR_FLAGS(ElementScene::ItemOptions) diff --git a/sources/editor/graphicspart/partterminal.cpp b/sources/editor/graphicspart/partterminal.cpp index d6ff367d1..194ae0aec 100644 --- a/sources/editor/graphicspart/partterminal.cpp +++ b/sources/editor/graphicspart/partterminal.cpp @@ -460,6 +460,22 @@ void PartTerminal::setLabelColor(QColor color) emit labelColorChanged(); } +void PartTerminal::setUseMasterLabel(bool use) +{ + if (d->m_use_master_label == use) return; + d->m_use_master_label = use; + update(); + emit useMasterLabelChanged(); +} + +void PartTerminal::setMasterLabelIndex(int index) +{ + if (d->m_master_label_index == index) return; + d->m_master_label_index = index; + update(); + emit masterLabelIndexChanged(); +} + /** Updates the position of the second point according to the position and orientation of the terminal. diff --git a/sources/editor/graphicspart/partterminal.h b/sources/editor/graphicspart/partterminal.h index c13db27c5..d0ba05da9 100644 --- a/sources/editor/graphicspart/partterminal.h +++ b/sources/editor/graphicspart/partterminal.h @@ -42,6 +42,8 @@ class PartTerminal : public CustomElementGraphicPart Q_PROPERTY(Qt::Alignment label_valignment READ labelVAlignment WRITE setLabelVAlignment) Q_PROPERTY(bool label_frame READ labelFrame WRITE setLabelFrame) Q_PROPERTY(QColor label_color READ labelColor WRITE setLabelColor) + Q_PROPERTY(bool use_master_label READ useMasterLabel WRITE setUseMasterLabel) + Q_PROPERTY(int master_label_index READ masterLabelIndex WRITE setMasterLabelIndex) public: // constructors, destructor @@ -62,6 +64,8 @@ class PartTerminal : public CustomElementGraphicPart void labelVAlignmentChanged(); void labelFrameChanged(); void labelColorChanged(); + void useMasterLabelChanged(); + void masterLabelIndexChanged(); // methods public: @@ -130,6 +134,12 @@ class PartTerminal : public CustomElementGraphicPart QColor labelColor() const { return d->m_label_color; } void setLabelColor(QColor color); + bool useMasterLabel() const { return d->m_use_master_label; } + void setUseMasterLabel(bool use); + + int masterLabelIndex() const { return d->m_master_label_index; } + void setMasterLabelIndex(int index); + void setNewUuid(); QRectF labelRect() const; diff --git a/sources/editor/ui/elementpropertieseditorwidget.cpp b/sources/editor/ui/elementpropertieseditorwidget.cpp index 397eadb64..40b3f8731 100644 --- a/sources/editor/ui/elementpropertieseditorwidget.cpp +++ b/sources/editor/ui/elementpropertieseditorwidget.cpp @@ -23,6 +23,11 @@ #include "../../qetinformation.h" #include +#include +#include +#include +#include +#include /** @brief The EditorDelegate class @@ -108,6 +113,13 @@ void ElementPropertiesEditorWidget::upDateInterface() ui->max_slaves_spinbox->setEnabled(true); ui->max_slaves_spinbox->setValue(m_data.m_max_slaves); } + + // Slave contact groups checkbox + ui->m_slave_groups_checkbox->setChecked(m_data.m_slave_contact_groups_enabled); + ui->m_slave_groups_table->setEnabled(m_data.m_slave_contact_groups_enabled); + if (m_data.m_slave_contact_groups_enabled) { + populateSlaveGroupsTable(); + } } else if (m_data.m_type == ElementData::Terminal) { ui->m_terminal_type_cb->setCurrentIndex( ui->m_terminal_type_cb->findData( @@ -168,6 +180,11 @@ void ElementPropertiesEditorWidget::setUpInterface() // NEU: Checkbox mit der Zahlenbox verbinden (Aktivieren/Deaktivieren) connect(ui->max_slaves_checkbox, SIGNAL(toggled(bool)), ui->max_slaves_spinbox, SLOT(setEnabled(bool))); + connect(ui->max_slaves_spinbox, QOverload::of(&QSpinBox::valueChanged), [this](int) { + if (ui->m_slave_groups_checkbox->isChecked()) { + populateSlaveGroupsTable(); + } + }); populateTree(); } @@ -241,7 +258,7 @@ void ElementPropertiesEditorWidget::on_m_buttonBox_accepted() m_data.m_slave_type = ui->m_type_cb->currentData().value(); m_data.m_contact_count = ui->m_number_ctc->value(); } - else if (m_data.m_type == ElementData::Master) { + else if (m_data.m_type == ElementData::Master) { m_data.m_master_type = ui->m_master_type_cb->currentData().value(); //If the checkbox is checked, save the number; otherwise, -1 (infinity) @@ -250,6 +267,8 @@ void ElementPropertiesEditorWidget::on_m_buttonBox_accepted() } else { m_data.m_max_slaves = -1; } + + readSlaveGroupsFromTable(); } else if (m_data.m_type == ElementData::Terminal) { @@ -299,3 +318,217 @@ void ElementPropertiesEditorWidget::on_m_base_type_cb_currentIndexChanged(int in updateTree(); } + +/** + * @brief ElementPropertiesEditorWidget::on_max_slaves_checkbox_toggled + * When max_slaves checkbox is unchecked, also uncheck slave groups checkbox + */ +void ElementPropertiesEditorWidget::on_max_slaves_checkbox_toggled(bool checked) +{ + if (!checked && ui->m_slave_groups_checkbox->isChecked()) { + ui->m_slave_groups_checkbox->setChecked(false); + } +} + +/** + * @brief ElementPropertiesEditorWidget::on_m_slave_groups_checkbox_toggled + * When slave groups checkbox is toggled, enable/disable the table + * Also ensure max_slaves checkbox is checked when enabling groups + */ +void ElementPropertiesEditorWidget::on_m_slave_groups_checkbox_toggled(bool checked) +{ + ui->m_slave_groups_table->setEnabled(checked); + + if (checked && !ui->max_slaves_checkbox->isChecked()) { + ui->max_slaves_checkbox->setChecked(true); + } + + if (checked) { + populateSlaveGroupsTable(); + } +} + +/** + * @brief ElementPropertiesEditorWidget::populateSlaveGroupsTable + * Fill the slave contact groups table from m_data + */ +void ElementPropertiesEditorWidget::populateSlaveGroupsTable() +{ + QSignalBlocker blocker_table(ui->m_slave_groups_table); + QSignalBlocker blocker_spinbox(ui->max_slaves_spinbox); + ui->m_slave_groups_table->setRowCount(0); + + int row_count = ui->max_slaves_checkbox->isChecked() + ? ui->max_slaves_spinbox->value() : 0; + + // If we have existing groups, use their count (up to max_slaves) + int existing_groups = m_data.m_slave_contact_groups.size(); + + // Adjust the groups list to match the spinbox value + while (m_data.m_slave_contact_groups.size() < row_count) { + ElementData::SlaveContactGroup group; + group.type = ElementData::NO; + group.subtype = ElementData::SSimple; + group.contactCount = 1; + group.terminalCount = 2; + m_data.m_slave_contact_groups.append(group); + } + while (m_data.m_slave_contact_groups.size() > row_count) { + m_data.m_slave_contact_groups.removeLast(); + } + + // Find max terminal count across all groups to determine T columns + int max_tc = 0; + for (const auto &g : m_data.m_slave_contact_groups) { + max_tc = qMax(max_tc, g.terminalCount); + } + max_tc = qMax(max_tc, 2); // at least T1, T2 + + // Set up 4 fixed columns + max_tc label columns + int total_cols = 4 + max_tc; + ui->m_slave_groups_table->setColumnCount(total_cols); + + // Set T column headers + for (int t = 0; t < max_tc; ++t) { + ui->m_slave_groups_table->setHorizontalHeaderItem( + 4 + t, new QTableWidgetItem(tr("T%1").arg(t + 1))); + } + + // Set column widths for readability + ui->m_slave_groups_table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + ui->m_slave_groups_table->horizontalHeader()->setMinimumSectionSize(60); + ui->m_slave_groups_table->setColumnWidth(0, 180); // Type + ui->m_slave_groups_table->setColumnWidth(1, 180); // Subtype + ui->m_slave_groups_table->setColumnWidth(2, 80); // Contacts + ui->m_slave_groups_table->setColumnWidth(3, 80); // Bornes + + ui->m_slave_groups_table->setRowCount(m_data.m_slave_contact_groups.size()); + + for (int i = 0; i < m_data.m_slave_contact_groups.size(); ++i) { + auto &group = m_data.m_slave_contact_groups[i]; + + // Type column + auto *type_cb = new QComboBox(ui->m_slave_groups_table); + type_cb->addItem(tr("Normalement ouvert"), ElementData::NO); + type_cb->addItem(tr("Normalement fermé"), ElementData::NC); + type_cb->addItem(tr("Inverseur"), ElementData::SW); + type_cb->addItem(tr("Autre"), ElementData::Other); + type_cb->setCurrentIndex(type_cb->findData(group.type)); + ui->m_slave_groups_table->setCellWidget(i, 0, type_cb); + + // Subtype column + auto *subtype_cb = new QComboBox(ui->m_slave_groups_table); + subtype_cb->addItem(tr("Simple"), ElementData::SSimple); + subtype_cb->addItem(tr("Puissance"), ElementData::Power); + subtype_cb->addItem(tr("Temporisé travail"), ElementData::DelayOn); + subtype_cb->addItem(tr("Temporisé repos"), ElementData::DelayOff); + subtype_cb->addItem(tr("Temporisé travail & repos"), ElementData::delayOnOff); + subtype_cb->setCurrentIndex(subtype_cb->findData(group.subtype)); + ui->m_slave_groups_table->setCellWidget(i, 1, subtype_cb); + + // Contact count + auto *contact_ct = new QSpinBox(ui->m_slave_groups_table); + contact_ct->setMinimum(1); + contact_ct->setMaximum(20); + contact_ct->setValue(group.contactCount); + ui->m_slave_groups_table->setCellWidget(i, 2, contact_ct); + + // Terminal count + auto *terminal_ct = new QSpinBox(ui->m_slave_groups_table); + terminal_ct->setMinimum(1); + terminal_ct->setMaximum(20); + terminal_ct->setValue(group.terminalCount); + ui->m_slave_groups_table->setCellWidget(i, 3, terminal_ct); + + // When terminal count changes, rebuild the table to update T columns + connect(terminal_ct, QOverload::of(&QSpinBox::valueChanged), + this, [this, terminal_ct, i](int val) { + if (i < m_data.m_slave_contact_groups.size()) { + m_data.m_slave_contact_groups[i].terminalCount = val; + readSlaveGroupsFromTable(); + populateSlaveGroupsTable(); + } + }); + + // Auto-generate labels if empty + QStringList labels = group.labels; + while (labels.size() < group.terminalCount) { + labels << tr("T%1").arg(labels.size() + 1); + } + + // Fill T1..TN columns + for (int t = 0; t < max_tc; ++t) { + auto *item = new QTableWidgetItem( + t < labels.size() ? labels.at(t) : QString()); + if (t >= group.terminalCount) { + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + item->setBackground(QBrush(QColor(240, 240, 240))); + } + ui->m_slave_groups_table->setItem(i, 4 + t, item); + } + + // Store updated labels back + group.labels = labels; + } +} + +/** + * @brief ElementPropertiesEditorWidget::readSlaveGroupsFromTable + * Read the slave contact groups from the table back into m_data + */ +void ElementPropertiesEditorWidget::readSlaveGroupsFromTable() +{ + m_data.m_slave_contact_groups.clear(); + + if (!ui->m_slave_groups_checkbox->isChecked()) { + m_data.m_slave_contact_groups_enabled = false; + return; + } + + m_data.m_slave_contact_groups_enabled = true; + + for (int i = 0; i < ui->m_slave_groups_table->rowCount(); ++i) { + ElementData::SlaveContactGroup group; + + auto *type_cb = qobject_cast( + ui->m_slave_groups_table->cellWidget(i, 0)); + if (type_cb) { + group.type = type_cb->currentData().value(); + } + + auto *subtype_cb = qobject_cast( + ui->m_slave_groups_table->cellWidget(i, 1)); + if (subtype_cb) { + group.subtype = subtype_cb->currentData().value(); + } + + auto *contact_ct = qobject_cast( + ui->m_slave_groups_table->cellWidget(i, 2)); + if (contact_ct) { + group.contactCount = contact_ct->value(); + } + + auto *terminal_ct = qobject_cast( + ui->m_slave_groups_table->cellWidget(i, 3)); + if (terminal_ct) { + group.terminalCount = terminal_ct->value(); + } + + // Read labels from T1..TN columns + for (int t = 0; t < group.terminalCount; ++t) { + int col = 4 + t; + if (col < ui->m_slave_groups_table->columnCount()) { + auto *item = ui->m_slave_groups_table->item(i, col); + if (item && !item->text().isEmpty()) { + group.labels.append(item->text()); + } else { + group.labels << tr("T%1").arg(t + 1); + } + } else { + group.labels << tr("T%1").arg(t + 1); + } + } + + m_data.m_slave_contact_groups.append(group); + } +} diff --git a/sources/editor/ui/elementpropertieseditorwidget.h b/sources/editor/ui/elementpropertieseditorwidget.h index 8effe6531..46f0b1e51 100644 --- a/sources/editor/ui/elementpropertieseditorwidget.h +++ b/sources/editor/ui/elementpropertieseditorwidget.h @@ -49,11 +49,15 @@ class ElementPropertiesEditorWidget : public QDialog void setUpInterface(); void updateTree(); void populateTree(); + void populateSlaveGroupsTable(); + void readSlaveGroupsFromTable(); //SLOTS private slots: void on_m_buttonBox_accepted(); void on_m_base_type_cb_currentIndexChanged(int index); + void on_m_slave_groups_checkbox_toggled(bool checked); + void on_max_slaves_checkbox_toggled(bool checked); //ATTRIBUTES private: diff --git a/sources/editor/ui/elementpropertieseditorwidget.ui b/sources/editor/ui/elementpropertieseditorwidget.ui index a7a8a7d49..ed2ae55c0 100644 --- a/sources/editor/ui/elementpropertieseditorwidget.ui +++ b/sources/editor/ui/elementpropertieseditorwidget.ui @@ -121,6 +121,55 @@ + + + + Définir les éléments esclave + + + + + + + false + + + + 0 + 150 + + + + QAbstractItemView::SingleSelection + + + QAbstractItemView::SelectRows + + + 4 + + + + Type + + + + + Contact + + + + + Nb. contacts + + + + + Nb. bornes + + + + diff --git a/sources/editor/ui/qetelementeditor.cpp b/sources/editor/ui/qetelementeditor.cpp index 31ce8482d..1b772773c 100644 --- a/sources/editor/ui/qetelementeditor.cpp +++ b/sources/editor/ui/qetelementeditor.cpp @@ -1192,6 +1192,12 @@ void QETElementEditor::initGui() updateInformations(); fillPartsList(); + // When the element type changes, update the terminal editor master label visibility + connect(m_elmt_scene, &ElementScene::elementTypeChanged, this, [this]() { + auto *te = static_cast(m_editors["terminal"]); + if (te) te->refreshMasterLabelVisibility(); + }); + statusBar()->showMessage(tr("Éditeur d'éléments", "status bar message")); } diff --git a/sources/editor/ui/terminaleditor.cpp b/sources/editor/ui/terminaleditor.cpp index b6feb09f3..97f378f00 100644 --- a/sources/editor/ui/terminaleditor.cpp +++ b/sources/editor/ui/terminaleditor.cpp @@ -25,6 +25,8 @@ #include #include +#include "../elementscene.h" +#include "qetelementeditor.h" /** * @brief TerminalEditor::TerminalEditor @@ -101,6 +103,26 @@ void TerminalEditor::updateForm() ui->m_text_props_gb->setEnabled(m_part->showName()); + // Update master label fields + bool is_slave = updateMasterLabelVisibility(); + if (is_slave) { + PartTerminal *pt = m_part; + if (pt) { + ui->m_use_master_label_cb->setChecked(pt->useMasterLabel()); + ui->m_master_label_cb->setEnabled(pt->useMasterLabel()); + ui->m_name_le->setEnabled(!pt->useMasterLabel()); + int idx = ui->m_master_label_cb->findData(pt->masterLabelIndex()); + if (idx >= 0) { + ui->m_master_label_cb->setCurrentIndex(idx); + } + // Show T-label in name field when master label is active + if (pt->useMasterLabel()) { + int label_idx = pt->masterLabelIndex(); + ui->m_name_le->setText(tr("T%1").arg(label_idx + 1)); + } + } + } + activeConnections(true); } @@ -165,6 +187,14 @@ void TerminalEditor::init() ui->m_type_cb->addItem(tr("Commun (contact SW)"), TerminalData::Common); ui->m_text_props_gb->setEnabled(false); + + // Populate master label dropdown (T1-T20) + for (int i = 1; i <= 20; ++i) { + ui->m_master_label_cb->addItem(tr("T%1").arg(i), i - 1); + } + + // Check if parent element is a Slave to show/hide master label group + updateMasterLabelVisibility(); } /** @@ -427,6 +457,10 @@ void TerminalEditor::activeConnections(bool active) this, &TerminalEditor::labelAlignClicked); m_editor_connections << connect(ui->m_label_frame_cb, &QCheckBox::toggled, this, &TerminalEditor::labelFrameEdited); + m_editor_connections << connect(ui->m_use_master_label_cb, &QCheckBox::toggled, + this, &TerminalEditor::useMasterLabelEdited); + m_editor_connections << connect(ui->m_master_label_cb, QOverload::of(&QComboBox::activated), + this, &TerminalEditor::masterLabelIndexEdited); } else { for (auto const & con : qAsConst(m_editor_connections)) { QObject::disconnect(con); @@ -452,6 +486,8 @@ void TerminalEditor::activeChangeConnections(bool active) m_change_connections << connect(m_part, &PartTerminal::labelVAlignmentChanged, this, &TerminalEditor::updateForm); m_change_connections << connect(m_part, &PartTerminal::labelFrameChanged, this, &TerminalEditor::updateForm); m_change_connections << connect(m_part, &PartTerminal::labelColorChanged, this, &TerminalEditor::updateForm); + m_change_connections << connect(m_part, &PartTerminal::useMasterLabelChanged, this, &TerminalEditor::updateForm); + m_change_connections << connect(m_part, &PartTerminal::masterLabelIndexChanged, this, &TerminalEditor::updateForm); } else { for (auto &con : m_change_connections) { QObject::disconnect(con); @@ -459,3 +495,95 @@ void TerminalEditor::activeChangeConnections(bool active) m_change_connections.clear(); } } + +void TerminalEditor::useMasterLabelEdited() +{ + if (m_locked) return; + m_locked = true; + + bool use = ui->m_use_master_label_cb->isChecked(); + ui->m_master_label_cb->setEnabled(use); + + QSignalBlocker name_blocker(ui->m_name_le); + + if (m_part->useMasterLabel() != use) { + auto undo = new QPropertyUndoCommand(m_part, "use_master_label", + m_part->useMasterLabel(), use); + undo->setText(tr("Modifier l'étiquette du maître")); + undoStack().push(undo); + } + + if (use) { + int idx = ui->m_master_label_cb->currentData().toInt(); + QString t_label = tr("T%1").arg(idx + 1); + ui->m_name_le->setText(t_label); + ui->m_name_le->setEnabled(false); + if (m_part->terminalName() != t_label) { + auto undo = new QPropertyUndoCommand(m_part, "terminal_name", + m_part->terminalName(), t_label); + undo->setText(tr("Modifier le nom de la borne")); + undoStack().push(undo); + } + } else { + ui->m_name_le->setEnabled(true); + if (!m_part->terminalName().isEmpty()) { + auto undo = new QPropertyUndoCommand(m_part, "terminal_name", + m_part->terminalName(), QString()); + undo->setText(tr("Modifier le nom de la borne")); + undoStack().push(undo); + } + ui->m_name_le->clear(); + } + + m_locked = false; +} + +void TerminalEditor::masterLabelIndexEdited() +{ + if (m_locked) return; + m_locked = true; + + int idx = ui->m_master_label_cb->currentData().toInt(); + + if (m_part->masterLabelIndex() != idx) { + auto undo = new QPropertyUndoCommand(m_part, "master_label_index", + m_part->masterLabelIndex(), idx); + undo->setText(tr("Modifier l'index de l'étiquette du maître")); + undoStack().push(undo); + } + + if (ui->m_use_master_label_cb->isChecked()) { + QString t_label = tr("T%1").arg(idx + 1); + ui->m_name_le->setText(t_label); + if (m_part->terminalName() != t_label) { + auto undo = new QPropertyUndoCommand(m_part, "terminal_name", + m_part->terminalName(), t_label); + undo->setText(tr("Modifier le nom de la borne")); + undoStack().push(undo); + } + } + + m_locked = false; +} + +bool TerminalEditor::updateMasterLabelVisibility() +{ + QETElementEditor *editor = elementEditor(); + if (!editor || !editor->elementScene()) { + ui->m_master_label_gb->setVisible(false); + return false; + } + + ElementData data = editor->elementScene()->elementData(); + bool is_slave = (data.m_type == ElementData::Slave); + ui->m_master_label_gb->setVisible(is_slave); + return is_slave; +} + +void TerminalEditor::refreshMasterLabelVisibility() +{ + updateMasterLabelVisibility(); + if (m_part) { + updateForm(); + } +} diff --git a/sources/editor/ui/terminaleditor.h b/sources/editor/ui/terminaleditor.h index 4fd1e50bd..d0ebf6098 100644 --- a/sources/editor/ui/terminaleditor.h +++ b/sources/editor/ui/terminaleditor.h @@ -49,6 +49,8 @@ class TerminalEditor : public ElementItemEditor bool setPart(CustomElementPart *new_part) override; CustomElementPart *currentPart() const override; QList currentParts() const override {return QList();} + public slots: + void refreshMasterLabelVisibility(); private: void init(); @@ -62,10 +64,13 @@ class TerminalEditor : public ElementItemEditor void labelSizeEdited(); void labelRotationEdited(); void labelAlignClicked(); - void labelFrameEdited(); - void labelColorClicked(); - void activeConnections(bool active); - void activeChangeConnections(bool active); + void labelFrameEdited(); + void labelColorClicked(); + void activeConnections(bool active); + void activeChangeConnections(bool active); + void useMasterLabelEdited(); + void masterLabelIndexEdited(); + bool updateMasterLabelVisibility(); private: Ui::TerminalEditor *ui; diff --git a/sources/editor/ui/terminaleditor.ui b/sources/editor/ui/terminaleditor.ui index 4d99560bf..6dc2388cb 100644 --- a/sources/editor/ui/terminaleditor.ui +++ b/sources/editor/ui/terminaleditor.ui @@ -214,6 +214,32 @@ + + + + Étiquette du maître + + + false + + + + + + Reprendre du maître + + + + + + + false + + + + + + diff --git a/sources/properties/elementdata.cpp b/sources/properties/elementdata.cpp index 9ae459fc3..53e653f4d 100644 --- a/sources/properties/elementdata.cpp +++ b/sources/properties/elementdata.cpp @@ -49,18 +49,7 @@ bool ElementData::fromXml(const QDomElement &xml_element) return false; } - // --- OUR DEBUG BLOCK STARTS HERE --- - // We retrieve the string from the XML and store it temporarily - QString raw_type_string = xml_element.attribute(QStringLiteral("link_type"), QStringLiteral("simple")); - - qDebug() << "\n=== NEW COMPONENT IS LOADING ==="; - qDebug() << "[XML Parser] Raw “link_type” string from the .elmt file:" << raw_type_string; - - // Now we’ll pass it on to your translation function - m_type = typeFromString(raw_type_string); - - qDebug() << "[XML Parser] Translated ElementData type:" << typeToString(m_type); - // --- THIS IS WHERE OUR DEBUG BLOCK ENDS --- + m_type = typeFromString(xml_element.attribute(QStringLiteral("link_type"), QStringLiteral("simple"))); kindInfoFromXml(xml_element); m_informations.fromXml(xml_element.firstChildElement(QStringLiteral("elementInformations")), @@ -98,6 +87,31 @@ QDomElement ElementData::kindInfoToXml(QDomDocument &document) returned_elmt.appendChild(xml_max_slaves); } + + // Save slave contact groups if enabled + if (m_slave_contact_groups_enabled) { + auto xml_groups = document.createElement(QStringLiteral("slaveContactGroups")); + for (const auto &group : m_slave_contact_groups) { + auto xml_group = document.createElement(QStringLiteral("group")); + xml_group.setAttribute(QStringLiteral("type"), + slaveStateToString(group.type)); + xml_group.setAttribute(QStringLiteral("subtype"), + slaveTypeToString(group.subtype)); + xml_group.setAttribute(QStringLiteral("contactCount"), + group.contactCount); + xml_group.setAttribute(QStringLiteral("terminalCount"), + group.terminalCount); + + for (const auto &label : group.labels) { + auto xml_label = document.createElement(QStringLiteral("label")); + auto label_txt = document.createTextNode(label); + xml_label.appendChild(label_txt); + xml_group.appendChild(xml_label); + } + xml_groups.appendChild(xml_group); + } + returned_elmt.appendChild(xml_groups); + } } else if (m_type == ElementData::Slave) { @@ -247,6 +261,15 @@ bool ElementData::operator==(const ElementData &data) const if(data.m_master_type != m_master_type) { return false; } + if (data.m_max_slaves != m_max_slaves) { + return false; + } + if (data.m_slave_contact_groups_enabled != m_slave_contact_groups_enabled) { + return false; + } + if (data.m_slave_contact_groups != m_slave_contact_groups) { + return false; + } } else if (data.m_type == ElementData::Slave) { if (data.m_slave_state != m_slave_state || @@ -591,7 +614,7 @@ void ElementData::kindInfoFromXml(const QDomElement &xml_element) m_max_slaves = dom_elmt.text().toInt(); } } - else if (m_type == ElementData::Slave ) { + else if (m_type == ElementData::Slave) { if (name == QLatin1String("type")) { m_slave_type = slaveTypeFromString(dom_elmt.text()); } else if (name == QLatin1String("state")) { @@ -606,8 +629,71 @@ void ElementData::kindInfoFromXml(const QDomElement &xml_element) } else if (name == QLatin1String("function")) { m_terminal_function = terminalFunctionFromString(dom_elmt.text()); } - + } + } + } + + // Parse slave contact groups for Master elements + if (m_type == ElementData::Master) { + auto xml_kind = xml_element.firstChildElement(QStringLiteral("kindInformations")); + auto xml_groups = xml_kind.firstChildElement(QStringLiteral("slaveContactGroups")); + if (!xml_groups.isNull()) { + m_slave_contact_groups_enabled = true; + auto group_list = QETXML::findInDomElement(xml_groups, QStringLiteral("group")); + for (const auto &xml_group : group_list) { + SlaveContactGroup group; + group.type = slaveStateFromString( + xml_group.attribute(QStringLiteral("type"), QStringLiteral("NO"))); + group.subtype = slaveTypeFromString( + xml_group.attribute(QStringLiteral("subtype"), QStringLiteral("simple"))); + group.contactCount = xml_group.attribute( + QStringLiteral("contactCount"), QStringLiteral("1")).toInt(); + group.terminalCount = xml_group.attribute( + QStringLiteral("terminalCount"), QStringLiteral("1")).toInt(); + + auto label_list = QETXML::findInDomElement(xml_group, QStringLiteral("label")); + for (const auto &xml_label : label_list) { + group.labels.append(xml_label.text()); + } + m_slave_contact_groups.append(group); } } } } + +/** + * @brief ElementData::slaveContactGroupTypeToString + * Convert a SlaveState to string for XML storage in slave contact groups. + * Maps: NO -> "NO", NC -> "NC", SW -> "SW", Other -> "Other" + */ +QString ElementData::slaveContactGroupTypeToString(ElementData::SlaveState type) +{ + return slaveStateToString(type); +} + +/** + * @brief ElementData::slaveContactGroupTypeFromString + * Convert a string from XML to SlaveState for slave contact groups. + */ +ElementData::SlaveState ElementData::slaveContactGroupTypeFromString(const QString &string) +{ + return slaveStateFromString(string); +} + +/** + * @brief ElementData::slaveContactGroupSubtypeToString + * Convert a SlaveType to string for XML storage in slave contact groups. + */ +QString ElementData::slaveContactGroupSubtypeToString(ElementData::SlaveType type) +{ + return slaveTypeToString(type); +} + +/** + * @brief ElementData::slaveContactGroupSubtypeFromString + * Convert a string from XML to SlaveType for slave contact groups. + */ +ElementData::SlaveType ElementData::slaveContactGroupSubtypeFromString(const QString &string) +{ + return slaveTypeFromString(string); +} diff --git a/sources/properties/elementdata.h b/sources/properties/elementdata.h index 565253874..4fa264ab4 100644 --- a/sources/properties/elementdata.h +++ b/sources/properties/elementdata.h @@ -22,6 +22,9 @@ #include "../diagramcontext.h" #include "../NameList/nameslist.h" +#include +#include + /** * @brief The ElementData class * WARNING @@ -86,6 +89,31 @@ class ElementData : public PropertiesInterface }; Q_ENUM(TerminalFunction) + /** + * @brief The SlaveContactGroup struct + * Defines a contact group that a master element provides for slave elements. + * Each group specifies the type, subtype, number of displayed contacts, + * number of terminals, and the labels for each terminal (T1-T20). + */ + struct SlaveContactGroup { + SlaveState type = NO; ///< Contact type (NO/NC/SW/Other) + SlaveType subtype = SSimple; ///< Contact subtype (Simple/Power/Delay*) + int contactCount = 1; ///< Number of displayed contacts + int terminalCount = 1; ///< Number of terminals (Anschlüsse) + QStringList labels; ///< Terminal labels (T1-T20), max 20 entries + + bool operator==(const SlaveContactGroup &other) const { + return type == other.type + && subtype == other.subtype + && contactCount == other.contactCount + && terminalCount == other.terminalCount + && labels == other.labels; + } + bool operator!=(const SlaveContactGroup &other) const { + return !(*this == other); + } + }; + ElementData() {} ~ElementData() override {} @@ -130,12 +158,19 @@ class ElementData : public PropertiesInterface static ElementData::TerminalFunction terminalFunctionFromString(const QString &string); static QString translatedTerminalFunction(ElementData::TerminalFunction function); + static QString slaveContactGroupTypeToString(ElementData::SlaveState type); + static ElementData::SlaveState slaveContactGroupTypeFromString(const QString &string); + static QString slaveContactGroupSubtypeToString(ElementData::SlaveType type); + static ElementData::SlaveType slaveContactGroupSubtypeFromString(const QString &string); + // must be public, because this class is a private member // of Element/ element editor and they must access this data ElementData::Type m_type = ElementData::Simple; ElementData::MasterType m_master_type = ElementData::Coil; int m_max_slaves{-1}; + bool m_slave_contact_groups_enabled{false}; ///< Whether slave contact groups table is active + QVector m_slave_contact_groups; ///< Contact groups defined by master ElementData::SlaveType m_slave_type = ElementData::SSimple; ElementData::SlaveState m_slave_state = ElementData::NO; diff --git a/sources/properties/terminaldata.cpp b/sources/properties/terminaldata.cpp index 2bf60d914..57b135421 100644 --- a/sources/properties/terminaldata.cpp +++ b/sources/properties/terminaldata.cpp @@ -125,6 +125,12 @@ QDomElement TerminalData::toXml(QDomDocument &xml_document) const xml_element.setAttribute("label_color", m_label_color.name()); } + // Save master label override settings + if (m_use_master_label) { + xml_element.setAttribute("use_master_label", "true"); + xml_element.setAttribute("master_label_index", m_master_label_index); + } + return(xml_element); } @@ -197,6 +203,10 @@ bool TerminalData::fromXml (const QDomElement &xml_element) m_label_color = QColor(color_str); } + // Read master label override settings + m_use_master_label = (xml_element.attribute("use_master_label") == QLatin1String("true")); + m_master_label_index = xml_element.attribute("master_label_index", "0").toInt(); + return true; } diff --git a/sources/properties/terminaldata.h b/sources/properties/terminaldata.h index 145fbc7c1..9032e6b2c 100644 --- a/sources/properties/terminaldata.h +++ b/sources/properties/terminaldata.h @@ -134,6 +134,11 @@ class TerminalData : public PropertiesInterface /// Color of the text label QColor m_label_color = Qt::black; + /// Whether this terminal uses a label from the master's contact group + bool m_use_master_label = false; + /// Index into the master's contact group labels (T1=0, T2=1, ..., T20=19) + int m_master_label_index = 0; + private: QGraphicsObject* q{nullptr}; }; diff --git a/sources/qetgraphicsitem/element.cpp b/sources/qetgraphicsitem/element.cpp index 74322b66c..fd723da7d 100644 --- a/sources/qetgraphicsitem/element.cpp +++ b/sources/qetgraphicsitem/element.cpp @@ -741,7 +741,9 @@ bool Element::fromXml(QDomElement &e, QStringLiteral("links_uuids"), QStringLiteral("link_uuid")); foreach (QDomElement qdo, uuid_list) { - tmp_uuids_link << QUuid(qdo.attribute(QStringLiteral("uuid"))); + QUuid uuid(qdo.attribute(QStringLiteral("uuid"))); + int group_index = qdo.attribute(QStringLiteral("group_index"), QStringLiteral("-1")).toInt(); + tmp_uuids_link << LinkInfo(uuid, group_index); } //uuid of this element @@ -932,6 +934,13 @@ QDomElement Element::toXml( QDomElement link_uuid = document.createElement(QStringLiteral("link_uuid")); link_uuid.setAttribute(QStringLiteral("uuid"), elmt->uuid().toString()); + + // Save group index if assigned (for slave->master links) + int gi = m_group_index_map.value(elmt, -1); + if (gi >= 0) { + link_uuid.setAttribute(QStringLiteral("group_index"), gi); + } + links_uuids.appendChild(link_uuid); } element.appendChild(links_uuids); @@ -1267,8 +1276,21 @@ void Element::initLink(QETProject *prj) if (tmp_uuids_link.isEmpty()) return; ElementProvider ep(prj); - foreach (Element *elmt, ep.fromUuids(tmp_uuids_link)) { - elmt->linkToElement(this); + QList uuids; + for (const auto &linkInfo : tmp_uuids_link) { + uuids.append(linkInfo.uuid); + } + QList elements = ep.fromUuids(uuids); + for (int i = 0; i < tmp_uuids_link.size(); ++i) { + for (Element *elmt : elements) { + if (elmt->uuid() == tmp_uuids_link[i].uuid) { + elmt->linkToElement(this); + if (tmp_uuids_link[i].group_index >= 0) { + m_group_index_map[elmt] = tmp_uuids_link[i].group_index; + } + break; + } + } } tmp_uuids_link.clear(); } @@ -1304,6 +1326,34 @@ QString Element::linkTypeToString() const } } +/** + * @brief Element::groupIndexForElement + * Returns the group index assigned to the given linked element. + * For slave elements, this indicates which contact group of the master + * this slave is assigned to. + * @param elmt the linked element to query + * @return group index, or -1 if not assigned + */ +int Element::groupIndexForElement(Element *elmt) const +{ + return m_group_index_map.value(elmt, -1); +} + +/** + * @brief Element::setGroupIndexForElement + * Sets the group index for a linked element. + * @param elmt the linked element + * @param index the group index to assign + */ +void Element::setGroupIndexForElement(Element *elmt, int index) +{ + if (index >= 0) { + m_group_index_map[elmt] = index; + } else { + m_group_index_map.remove(elmt); + } +} + /** @brief Element::setElementInformations Set new information for this element. diff --git a/sources/qetgraphicsitem/element.h b/sources/qetgraphicsitem/element.h index 2c3ea5b6a..0dc3f7c8f 100644 --- a/sources/qetgraphicsitem/element.h +++ b/sources/qetgraphicsitem/element.h @@ -62,6 +62,20 @@ class Element : public QetGraphicsItem Thumbnail = 64, ConductorDefinition = 128}; + /** + * @brief The LinkInfo struct + * Stores link data for element connections. + * For slave elements, group_index indicates which contact group + * of the master this slave is assigned to (-1 = no group assigned). + */ + struct LinkInfo { + QUuid uuid; + int group_index = -1; ///< Index into master's slaveContactGroups, -1 = not assigned + + LinkInfo() = default; + LinkInfo(const QUuid &u, int gi = -1) : uuid(u), group_index(gi) {} + }; + Element(const ElementsLocation &location, QGraphicsItem * = nullptr, int *state = nullptr, @@ -181,6 +195,9 @@ class Element : public QetGraphicsItem virtual void initLink(QETProject *); QList linkedElements (); + int groupIndexForElement(Element *elmt) const; + void setGroupIndexForElement(Element *elmt, int index); + /** * @brief linkType * use elementData function instead @@ -228,7 +245,8 @@ class Element : public QetGraphicsItem protected: //ATTRIBUTES related to linked element QList connected_elements; - QList tmp_uuids_link; + QList tmp_uuids_link; + QHash m_group_index_map; ///< Maps linked elements to their group index (for slave->master links) QUuid m_uuid; kind m_link_type = Element::Simple; diff --git a/sources/qetgraphicsitem/terminal.cpp b/sources/qetgraphicsitem/terminal.cpp index 7b0f58be3..32584aebb 100644 --- a/sources/qetgraphicsitem/terminal.cpp +++ b/sources/qetgraphicsitem/terminal.cpp @@ -274,7 +274,8 @@ void Terminal::paint( } // Draw label if show_name is enabled - if (d->m_show_name && !d->m_name.isEmpty()) { + const QString display_name = name(); + if (d->m_show_name && !display_name.isEmpty()) { painter->setRenderHint(QPainter::Antialiasing, true); painter->setRenderHint(QPainter::TextAntialiasing, true); painter->setFont(d->m_label_font); @@ -282,7 +283,7 @@ void Terminal::paint( QPointF label_pos = d->m_pos + d->m_label_pos; QFontMetrics fm(d->m_label_font); - QSizeF text_size = fm.size(Qt::TextSingleLine, d->m_name); + QSizeF text_size = fm.size(Qt::TextSingleLine, display_name); if (!qFuzzyIsNull(d->m_label_rotation)) { painter->save(); @@ -290,7 +291,7 @@ void Terminal::paint( painter->rotate(d->m_label_rotation); QRectF text_rect(-text_size.width()/2.0, -text_size.height()/2.0, text_size.width(), text_size.height()); - painter->drawText(text_rect, static_cast(d->m_label_halignment | d->m_label_valignment), d->m_name); + painter->drawText(text_rect, static_cast(d->m_label_halignment | d->m_label_valignment), display_name); painter->restore(); } else { qreal dx = 0, dy = 0; @@ -306,7 +307,7 @@ void Terminal::paint( if (d->m_label_frame) { painter->drawRect(text_rect.adjusted(-1, -1, 1, 1)); } - painter->drawText(text_rect, static_cast(Qt::AlignLeft | Qt::AlignTop), d->m_name); + painter->drawText(text_rect, static_cast(Qt::AlignLeft | Qt::AlignTop), display_name); } } @@ -378,12 +379,13 @@ QLineF Terminal::HelpLine() const @return Le rectangle (en precision flottante) delimitant la borne et ses alentours. */ QRectF Terminal::boundingRect() const { - if (!d->m_show_name || d->m_name.isEmpty()) { + const QString display_name = name(); + if (!d->m_show_name || display_name.isEmpty()) { return m_br; } QFontMetrics fm(d->m_label_font); - QSizeF text_size = fm.size(Qt::TextSingleLine, d->m_name); + QSizeF text_size = fm.size(Qt::TextSingleLine, display_name); QPointF label_pos = d->m_pos + d->m_label_pos; qreal dx = 0, dy = 0; @@ -808,6 +810,25 @@ QUuid Terminal::uuid() const QString Terminal::name() const { + if (d->m_use_master_label && parent_element_) { + // Find the master element in the slave's linked elements + for (Element *elmt : parent_element_->linkedElements()) { + if (elmt->linkType() == Element::Master) { + int group_idx = elmt->groupIndexForElement(parent_element_); + if (group_idx >= 0) { + const auto &groups = elmt->elementData().m_slave_contact_groups; + if (group_idx < groups.size()) { + int label_idx = d->m_master_label_index; + const QStringList &labels = groups.at(group_idx).labels; + if (label_idx >= 0 && label_idx < labels.size()) { + return labels.at(label_idx); + } + } + } + break; + } + } + } return d->m_name; } @@ -820,6 +841,28 @@ TerminalData::Type Terminal::terminalType() const return d->m_type; } +/** + @brief Terminal::setUseMasterLabel + Set whether this terminal uses a label from the master's contact group + @param use true to use master label +*/ +void Terminal::setUseMasterLabel(bool use) +{ + d->m_use_master_label = use; + update(); +} + +/** + @brief Terminal::setMasterLabelIndex + Set the index into the master's contact group labels + @param index the label index (0-based) +*/ +void Terminal::setMasterLabelIndex(int index) +{ + d->m_master_label_index = index; + update(); +} + /** @brief Conductor::relatedPotentialTerminal Return terminal at the same potential from the same diff --git a/sources/qetgraphicsitem/terminal.h b/sources/qetgraphicsitem/terminal.h index d447aa982..eac284553 100644 --- a/sources/qetgraphicsitem/terminal.h +++ b/sources/qetgraphicsitem/terminal.h @@ -77,6 +77,10 @@ class Terminal : public QGraphicsObject QUuid uuid () const; QString name () const; TerminalData::Type terminalType() const; + bool useMasterLabel() const { return d->m_use_master_label; } + void setUseMasterLabel(bool use); + int masterLabelIndex() const { return d->m_master_label_index; } + void setMasterLabelIndex(int index); QList conductors() const; Qet::Orientation orientation() const; diff --git a/sources/qetproject.cpp b/sources/qetproject.cpp index aabd128de..f55a46749 100644 --- a/sources/qetproject.cpp +++ b/sources/qetproject.cpp @@ -1179,6 +1179,22 @@ ElementsLocation QETProject::importElement(ElementsLocation &location) } //Erase the existing element, and use the newer instead else if (action == QET::Erase) { + // Warn if the new element introduces slave contact groups + QDomElement new_kind = location.xml().firstChildElement("kindInformations"); + if (!new_kind.firstChildElement("slaveContactGroups").isNull()) { + QMessageBox::StandardButton answer = QMessageBox::warning(nullptr, + tr("Système de contacts modifié"), + tr("Le nouvel élément définit des groupes de contacts esclaves.\n" + "Les éléments esclaves existants ne seront pas automatiquement " + "assignés. Vous devrez relier manuellement les esclaves " + "et assigner les groupes de contacts.\n\n" + "Voulez-vous continuer ?"), + QMessageBox::Yes | QMessageBox::No, + QMessageBox::Yes); + if (answer == QMessageBox::No) { + return ElementsLocation(); + } + } ElementsLocation parent_loc = existing_location.parent(); return m_elements_collection->copy(location, parent_loc); } diff --git a/sources/ui/contactgroupselectiondialog.cpp b/sources/ui/contactgroupselectiondialog.cpp new file mode 100644 index 000000000..d1c88a002 --- /dev/null +++ b/sources/ui/contactgroupselectiondialog.cpp @@ -0,0 +1,238 @@ +/* + 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 "contactgroupselectiondialog.h" + +#include +#include +#include +#include +#include +#include +#include + +ContactGroupSelectionDialog::ContactGroupSelectionDialog( + const QVector &groups, + const QSet &usedGroupIndices, + const ElementData &slaveData, + QWidget *parent) + : QDialog(parent) +{ + m_used_indices = usedGroupIndices; + setWindowTitle(tr("Sélectionner un groupe de contacts")); + + auto *main_layout = new QVBoxLayout(this); + + auto *info_label = new QLabel( + tr("Sélectionnez le groupe de contacts à assigner à cet élément esclave :")); + main_layout->addWidget(info_label); + + // Determine max terminal count for dynamic columns + int max_terminals = 0; + for (const auto &g : groups) { + if (g.terminalCount > max_terminals) + max_terminals = g.terminalCount; + } + + // Build column headers: #, Type, Sous-type, Contacts, Bornes, T1, T2, ... + QStringList headers; + headers << tr("#") + << tr("Type") + << tr("Sous-type") + << tr("Contacts") + << tr("Bornes"); + for (int t = 0; t < max_terminals; ++t) { + headers << tr("T%1").arg(t + 1); + } + + m_table = new QTableWidget(groups.size(), headers.size(), this); + m_table->setHorizontalHeaderLabels(headers); + m_table->setSelectionBehavior(QTableWidget::SelectRows); + m_table->setSelectionMode(QTableWidget::SingleSelection); + m_table->setEditTriggers(QTableWidget::NoEditTriggers); + m_table->verticalHeader()->setVisible(false); + m_table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents); + m_table->horizontalHeader()->setStretchLastSection(false); + m_table->verticalHeader()->setDefaultSectionSize(24); + + // Populate table rows + for (int row = 0; row < groups.size(); ++row) { + const auto &g = groups.at(row); + + auto *num_item = new QTableWidgetItem(QString::number(row + 1)); + num_item->setTextAlignment(Qt::AlignCenter); + m_table->setItem(row, 0, num_item); + + m_table->setItem(row, 1, new QTableWidgetItem(typeToString(g.type))); + m_table->setItem(row, 2, new QTableWidgetItem(subtypeToString(g.subtype))); + + auto *ctc_item = new QTableWidgetItem(QString::number(g.contactCount)); + ctc_item->setTextAlignment(Qt::AlignCenter); + m_table->setItem(row, 3, ctc_item); + + auto *term_item = new QTableWidgetItem(QString::number(g.terminalCount)); + term_item->setTextAlignment(Qt::AlignCenter); + m_table->setItem(row, 4, term_item); + + // Fill T1..TN label columns + for (int t = 0; t < max_terminals; ++t) { + int col = 5 + t; + QString label; + if (t < g.labels.size()) { + label = g.labels.at(t); + } else if (t < g.terminalCount) { + label = tr("T%1").arg(t + 1); + } + auto *label_item = new QTableWidgetItem(label); + label_item->setTextAlignment(Qt::AlignCenter); + m_table->setItem(row, col, label_item); + } + } + + // Mark used group rows as disabled (grayed out) + QFont disabled_font; + disabled_font.setStrikeOut(true); + QColor disabled_color(Qt::gray); + + for (int row = 0; row < groups.size(); ++row) { + bool disabled = false; + QString reason; + + if (m_used_indices.contains(row)) { + disabled = true; + reason = tr("(déjà assigné)"); + } else { + const auto &g = groups.at(row); + if (g.type != slaveData.m_slave_state) { + disabled = true; + reason = tr("(état ne correspond pas)"); + } else if (g.subtype != slaveData.m_slave_type) { + disabled = true; + reason = tr("(sous-type ne correspond pas)"); + } else if (g.contactCount != slaveData.m_contact_count) { + disabled = true; + reason = tr("(nombre de contacts ne correspond pas)"); + } + } + + if (disabled) { + m_disabled_rows.insert(row); + for (int col = 0; col < m_table->columnCount(); ++col) { + auto *item = m_table->item(row, col); + if (item) { + item->setForeground(disabled_color); + item->setFont(disabled_font); + item->setFlags(item->flags() & ~Qt::ItemIsSelectable); + item->setToolTip(reason); + } + } + // Show circled "?" as a widget in the first column + auto *num_item = m_table->item(row, 0); + if (num_item) { + num_item->setToolTip(reason); + auto *question_label = new QLabel(this); + question_label->setText("?"); + question_label->setAlignment(Qt::AlignCenter); + question_label->setStyleSheet( + "QLabel {" + " color: #1a73e8;" + " font-weight: bold;" + " font-size: 8px;" + " border: 1.5px solid #1a73e8;" + " border-radius: 7px;" + " min-width: 12px; max-width: 12px;" + " min-height: 12px; max-height: 12px;" + "}"); + question_label->setToolTip(reason); + m_table->setCellWidget(row, 0, question_label); + } + } + } + + // Select first available (non-disabled) row + int first_available = 0; + for (int row = 0; row < groups.size(); ++row) { + if (!m_disabled_rows.contains(row)) { + first_available = row; + break; + } + } + m_table->selectRow(first_available); + main_layout->addWidget(m_table); + + // Calculate width based on column count: base + ~60px per column + int table_width = 50 + headers.size() * 65; + int table_height = 60 + groups.size() * 26; + m_table->setMinimumWidth(table_width); + m_table->setMinimumHeight(table_height); + + // Buttons + auto *button_layout = new QHBoxLayout(); + button_layout->addStretch(); + + m_ok_button = new QPushButton(tr("OK"), this); + button_layout->addWidget(m_ok_button); + + auto *cancel_button = new QPushButton(tr("Annuler"), this); + button_layout->addWidget(cancel_button); + + main_layout->addLayout(button_layout); + + // Connections + connect(m_ok_button, &QPushButton::clicked, this, [this]() { + auto *item = m_table->currentItem(); + if (item && !m_disabled_rows.contains(item->row())) { + m_selected_index = item->row(); + accept(); + } + }); + connect(cancel_button, &QPushButton::clicked, this, &QDialog::reject); + connect(m_table, &QTableWidget::cellDoubleClicked, this, [this](int row, int) { + if (!m_disabled_rows.contains(row)) { + m_selected_index = row; + accept(); + } + }); +} + +int ContactGroupSelectionDialog::selectedIndex() const +{ + return m_selected_index; +} + +QString ContactGroupSelectionDialog::typeToString(ElementData::SlaveState type) +{ + switch (type) { + case ElementData::NO: return tr("Normalement ouvert"); + case ElementData::NC: return tr("Normalement fermé"); + case ElementData::SW: return tr("Inverseur"); + case ElementData::Other: return tr("Autre"); + default: return tr("Inconnu"); + } +} + +QString ContactGroupSelectionDialog::subtypeToString(ElementData::SlaveType subtype) +{ + switch (subtype) { + case ElementData::SSimple: return tr("Simple"); + case ElementData::Power: return tr("Puissance"); + case ElementData::DelayOn: return tr("Temporisé travail"); + case ElementData::DelayOff: return tr("Temporisé repos"); + case ElementData::delayOnOff: return tr("Temporisé travail & repos"); + default: return tr("Inconnu"); + } +} diff --git a/sources/ui/contactgroupselectiondialog.h b/sources/ui/contactgroupselectiondialog.h new file mode 100644 index 000000000..04bf07f52 --- /dev/null +++ b/sources/ui/contactgroupselectiondialog.h @@ -0,0 +1,61 @@ +/* + 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 . +*/ +#ifndef CONTACTGROUPSELECTIONDIALOG_H +#define CONTACTGROUPSELECTIONDIALOG_H + +#include +#include +#include + +#include "../properties/elementdata.h" + +class QTableWidget; +class QTableWidgetItem; +class QPushButton; + +/** + @brief The ContactGroupSelectionDialog class + A dialog that displays all slave contact groups defined by a master element + in a table format. The user can select one group to assign to a slave element. + Columns: #, Type, Subtype, Contacts, Terminals, T1, T2, ..., TN +*/ +class ContactGroupSelectionDialog : public QDialog +{ + Q_OBJECT + + public: + ContactGroupSelectionDialog( + const QVector &groups, + const QSet &usedGroupIndices, + const ElementData &slaveData, + QWidget *parent = nullptr); + + int selectedIndex() const; + + private: + QTableWidget *m_table = nullptr; + QPushButton *m_ok_button = nullptr; + int m_selected_index = -1; + QSet m_used_indices; + QSet m_disabled_rows; + + static QString typeToString(ElementData::SlaveState type); + static QString subtypeToString(ElementData::SlaveType subtype); +}; + +#endif // CONTACTGROUPSELECTIONDIALOG_H diff --git a/sources/ui/linksingleelementwidget.cpp b/sources/ui/linksingleelementwidget.cpp index 852e4ac7c..8f3f962be 100644 --- a/sources/ui/linksingleelementwidget.cpp +++ b/sources/ui/linksingleelementwidget.cpp @@ -16,6 +16,7 @@ along with QElectroTech. If not, see . */ #include "linksingleelementwidget.h" +#include "contactgroupselectiondialog.h" #include "../qetgraphicsitem/masterelement.h" #include "../qetgraphicsitem/conductor.h" #include "../diagram.h" @@ -29,6 +30,7 @@ #include + /** @brief LinkSingleElementWidget::LinkSingleElementWidget Default constructor @@ -175,6 +177,7 @@ void LinkSingleElementWidget::apply() m_unlink = false; m_element_to_link = nullptr; m_pending_qtwi = nullptr; + m_pending_group_index = -1; } /** @@ -188,8 +191,12 @@ QUndoCommand *LinkSingleElementWidget::associatedUndo() const if (m_element_to_link || m_unlink) { - if (m_element_to_link) + if (m_element_to_link) { undo->setLink(m_element_to_link); + if (m_pending_group_index >= 0) { + undo->setGroupIndex(m_pending_group_index); + } + } else if (m_unlink) undo->unlinkAll(); @@ -531,7 +538,36 @@ void LinkSingleElementWidget::linkTriggered() return; m_element_to_link = m_qtwi_elmt_hash.value(m_qtwi_at_context_menu); - + m_pending_group_index = -1; + + //If linking a slave to a master with contact groups, show group selection dialog + if (m_element->linkType() == Element::Slave + && m_element_to_link + && m_element_to_link->linkType() == Element::Master) + { + const auto &groups = m_element_to_link->elementData().m_slave_contact_groups; + if (!groups.isEmpty()) + { + // Collect already-used group indices from the master + QSet used_indices; + for (Element *linked : m_element_to_link->linkedElements()) { + int idx = m_element_to_link->groupIndexForElement(linked); + if (idx >= 0) { + used_indices.insert(idx); + } + } + + ContactGroupSelectionDialog dlg(groups, used_indices, + m_element->elementData(), this); + if (dlg.exec() == QDialog::Accepted && dlg.selectedIndex() >= 0) { + m_pending_group_index = dlg.selectedIndex(); + } else { + m_element_to_link = nullptr; + return; + } + } + } + if(m_live_edit) { apply(); diff --git a/sources/ui/linksingleelementwidget.h b/sources/ui/linksingleelementwidget.h index 3c0b79ee2..d477d107b 100644 --- a/sources/ui/linksingleelementwidget.h +++ b/sources/ui/linksingleelementwidget.h @@ -22,6 +22,8 @@ #include "abstractelementpropertieseditorwidget.h" #include +#include +#include class QTreeWidgetItem; class Element; @@ -101,6 +103,8 @@ class LinkSingleElementWidget : public AbstractElementPropertiesEditorWidget Element *m_showed_element = nullptr, *m_element_to_link = nullptr; + int m_pending_group_index = -1; + QMenu *m_context_menu{nullptr}; QAction *m_link_action{nullptr}, *m_show_qtwi{nullptr}, diff --git a/sources/ui/masterpropertieswidget.cpp b/sources/ui/masterpropertieswidget.cpp index 0cb2548eb..8c56f5757 100644 --- a/sources/ui/masterpropertieswidget.cpp +++ b/sources/ui/masterpropertieswidget.cpp @@ -16,6 +16,7 @@ * along with QElectroTech. If not, see . */ #include "masterpropertieswidget.h" +#include "contactgroupselectiondialog.h" #include "../qetproject.h" #include "../diagram.h" #include "../diagramposition.h" @@ -27,6 +28,7 @@ #include #include + /** * @brief MasterPropertiesWidget::MasterPropertiesWidget * Default constructor @@ -176,6 +178,8 @@ void MasterPropertiesWidget::apply() { if (QUndoCommand *undo = associatedUndo()) m_element -> diagram() -> undoStack().push(undo); + + m_pending_group_indices.clear(); } /** @@ -224,8 +228,23 @@ QUndoCommand* MasterPropertiesWidget::associatedUndo() const if (to_link.isEmpty()) undo->unlinkAll(); else + { undo->setLink(to_link); + //Pass group indices for newly linked slaves + if (!m_pending_group_indices.isEmpty()) + { + QMap indices; + for (Element *slave : to_link) + { + if (m_pending_group_indices.contains(slave)) + indices[slave] = m_pending_group_indices.value(slave); + } + if (!indices.isEmpty()) + undo->setGroupIndices(indices); + } + } + return undo; } @@ -375,6 +394,38 @@ void MasterPropertiesWidget::on_link_button_clicked() QTreeWidgetItem *qtwi = ui->m_free_tree_widget->currentItem(); if (qtwi) { + Element *slave_elmt = m_qtwi_hash.value(qtwi); + + //If master has contact groups, show group selection dialog + const auto &groups = m_element->elementData().m_slave_contact_groups; + if (!groups.isEmpty() && slave_elmt) + { + // Collect already-used group indices from the master + QSet used_indices; + for (Element *linked : m_element->linkedElements()) { + int idx = m_element->groupIndexForElement(linked); + if (idx >= 0) { + used_indices.insert(idx); + } + } + + // Don't mark the current slave as used (it might be relinked) + if (slave_elmt->linkedElements().contains(m_element)) { + int current_idx = m_element->groupIndexForElement(slave_elmt); + if (current_idx >= 0) { + used_indices.remove(current_idx); + } + } + + ContactGroupSelectionDialog dlg(groups, used_indices, + slave_elmt->elementData(), this); + if (dlg.exec() == QDialog::Accepted && dlg.selectedIndex() >= 0) { + m_pending_group_indices[slave_elmt] = dlg.selectedIndex(); + } else { + return; + } + } + ui->m_free_tree_widget->takeTopLevelItem( ui->m_free_tree_widget->indexOfTopLevelItem(qtwi)); ui->m_link_tree_widget->insertTopLevelItem(0, qtwi); diff --git a/sources/ui/masterpropertieswidget.h b/sources/ui/masterpropertieswidget.h index 75442298a..a66f49835 100644 --- a/sources/ui/masterpropertieswidget.h +++ b/sources/ui/masterpropertieswidget.h @@ -20,6 +20,7 @@ #include #include +#include #include "abstractelementpropertieseditorwidget.h" @@ -77,6 +78,7 @@ class MasterPropertiesWidget : public AbstractElementPropertiesEditorWidget private: Ui::MasterPropertiesWidget *ui; QHash m_qtwi_hash; + QMap m_pending_group_indices; QTreeWidgetItem *m_qtwi_at_context_menu = nullptr; QPointer m_showed_element; QETProject *m_project; diff --git a/sources/undocommand/linkelementcommand.cpp b/sources/undocommand/linkelementcommand.cpp index 594e0dcc0..799835d3a 100644 --- a/sources/undocommand/linkelementcommand.cpp +++ b/sources/undocommand/linkelementcommand.cpp @@ -21,6 +21,7 @@ #include "../diagram.h" #include "../qetgraphicsitem/conductor.h" #include "../qetgraphicsitem/element.h" +#include "../qetgraphicsitem/terminal.h" #include "../ui/potentialselectordialog.h" /** @@ -176,6 +177,54 @@ void LinkElementCommand::unlinkAll() void LinkElementCommand::undo() { if(m_element->diagram()) m_element->diagram()->showMe(); + + //Clear group index for slave elements when undoing + if (m_element->linkType() == Element::Slave) + { + int group_idx = m_group_index; + if (m_group_indices.contains(m_element)) + group_idx = m_group_indices.value(m_element); + + if (group_idx >= 0) + { + // Reset master labels on slave terminals + QList slave_terms = m_element->terminals(); + for (Terminal *t : slave_terms) + { + t->setUseMasterLabel(false); + t->setMasterLabelIndex(0); + } + + foreach(Element *elmt, m_element->linkedElements()) + { + if (elmt->linkType() == Element::Master) + { + elmt->setGroupIndexForElement(m_element, -1); + break; + } + } + } + } + else if (m_element->linkType() == Element::Master) + { + for (auto it = m_group_indices.constBegin(); it != m_group_indices.constEnd(); ++it) + { + Element *slave = it.key(); + if (m_element->linkedElements().contains(slave)) + { + // Reset master labels on slave terminals + QList slave_terms = slave->terminals(); + for (Terminal *t : slave_terms) + { + t->setUseMasterLabel(false); + t->setMasterLabelIndex(0); + } + + m_element->setGroupIndexForElement(slave, -1); + } + } + } + makeLink(m_linked_before); QUndoCommand::undo(); } @@ -275,6 +324,83 @@ void LinkElementCommand::makeLink(const QList &element_list) foreach(Element *elmt, element_list) m_element->linkToElement(elmt); + //Set group index for slave-master links + if (m_element->linkType() == Element::Slave) + { + int group_idx = m_group_index; + + //Check if we have a per-slave group index + if (m_group_indices.contains(m_element)) + group_idx = m_group_indices.value(m_element); + + if (group_idx >= 0) + { + foreach(Element *elmt, element_list) + { + if (elmt->linkType() == Element::Master) + { + elmt->setGroupIndexForElement(m_element, group_idx); + + // Set master labels on slave terminals + const auto &groups = elmt->elementData().m_slave_contact_groups; + if (group_idx < groups.size()) + { + const QStringList &labels = groups.at(group_idx).labels; + QList slave_terms = m_element->terminals(); + // Sort terminals by name (T1, T2, T3...) to match label order + std::sort(slave_terms.begin(), slave_terms.end(), + [](Terminal *a, Terminal *b) { + return a->name() < b->name(); + }); + for (int i = 0; i < slave_terms.size(); ++i) + { + if (i < labels.size()) + { + slave_terms.at(i)->setUseMasterLabel(true); + slave_terms.at(i)->setMasterLabelIndex(i); + } + } + } + break; + } + } + } + } + else if (m_element->linkType() == Element::Master) + { + //For master linking to slaves, set group indices for each slave + for (auto it = m_group_indices.constBegin(); it != m_group_indices.constEnd(); ++it) + { + Element *slave = it.key(); + int group_idx = it.value(); + if (group_idx >= 0 && element_list.contains(slave)) + { + m_element->setGroupIndexForElement(slave, group_idx); + + // Set master labels on slave terminals + const auto &groups = m_element->elementData().m_slave_contact_groups; + if (group_idx < groups.size()) + { + const QStringList &labels = groups.at(group_idx).labels; + QList slave_terms = slave->terminals(); + // Sort terminals by name (T1, T2, T3...) to match label order + std::sort(slave_terms.begin(), slave_terms.end(), + [](Terminal *a, Terminal *b) { + return a->name() < b->name(); + }); + for (int i = 0; i < slave_terms.size(); ++i) + { + if (i < labels.size()) + { + slave_terms.at(i)->setUseMasterLabel(true); + slave_terms.at(i)->setMasterLabelIndex(i); + } + } + } + } + } + } + /* At this point there may be unwanted linked elements to m_element. * We must unlink it. * Elements from element_list are wanted so we compare element_list diff --git a/sources/undocommand/linkelementcommand.h b/sources/undocommand/linkelementcommand.h index 51b47c355..70650b82b 100644 --- a/sources/undocommand/linkelementcommand.h +++ b/sources/undocommand/linkelementcommand.h @@ -19,6 +19,7 @@ #define LINKELEMENTCOMMAND_H #include +#include class Element; @@ -42,6 +43,8 @@ class LinkElementCommand : public QUndoCommand void setLink (Element *element_); void unlink (QList element_list); void unlinkAll (); + void setGroupIndex(int index) { m_group_index = index; } + void setGroupIndices(const QMap &indices) { m_group_indices = indices; } void undo() override; void redo() override; @@ -53,6 +56,8 @@ class LinkElementCommand : public QUndoCommand private: Element *m_element; bool m_first_redo; + int m_group_index = -1; + QMap m_group_indices; QList m_linked_before; // m_linked_after; //