/* 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 "elementpropertieseditorwidget.h" #include "../../qetapp.h" #include "../../qetinformation.h" #include "ui_elementpropertieseditorwidget.h" #include "../../qetinformation.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /** @brief The EditorDelegate class This delegate is only use for disable the edition of the first column of the information tree widget */ class EditorDelegate : public QItemDelegate { public: EditorDelegate(QObject *parent) : QItemDelegate(parent) {} QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override { if(index.column() == 1) { return QItemDelegate::createEditor(parent, option, index); } return nullptr; } }; /** * @brief ElementPropertiesEditorWidget::ElementPropertiesEditorWidget * @param data * @param parent */ ElementPropertiesEditorWidget::ElementPropertiesEditorWidget(ElementData data, QWidget *parent) : QDialog(parent), ui(new Ui::ElementPropertiesEditorWidget), m_data(data) { ui->setupUi(this); setUpInterface(); upDateInterface(); } /** @brief ElementPropertiesEditorWidget::~ElementPropertiesEditorWidget Default destructor */ ElementPropertiesEditorWidget::~ElementPropertiesEditorWidget() { delete ui; } /** @brief ElementPropertiesEditorWidget::upDateInterface Update the interface with the current value */ void ElementPropertiesEditorWidget::upDateInterface() { ui->m_base_type_cb->setCurrentIndex( ui->m_base_type_cb->findData( m_data.m_type)); if (m_data.m_type == ElementData::Slave) { // If PLC Slave, select "Esclave PLC" in state combo (not in type combo) if (m_data.m_slave_type == ElementData::PLCSlave) { ui->m_state_cb->setCurrentIndex( ui->m_state_cb->findData( ElementData::PLCSlave)); } else { ui->m_state_cb->setCurrentIndex( ui->m_state_cb->findData( m_data.m_slave_state)); } ui->m_type_cb->setCurrentIndex ( ui->m_type_cb->findData( m_data.m_slave_type)); ui->m_number_ctc->setValue(m_data.m_contact_count); } else if (m_data.m_type == ElementData::Master) { ui->m_master_type_cb->setCurrentIndex( ui->m_master_type_cb->findData ( m_data.m_master_type)); // NEU: Checkbox und Zahlenbox für max_slaves einstellen if (m_data.m_max_slaves == -1) { ui->max_slaves_checkbox->setChecked(false); ui->max_slaves_spinbox->setEnabled(false); } else { ui->max_slaves_checkbox->setChecked(true); 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(); } // PLC configuration if (m_data.m_master_type == ElementData::PLC) { if (!m_plc_gb) { createPlcConfigWidgets(); } m_plc_gb->setVisible(true); ui->max_slaves_checkbox->setVisible(false); ui->max_slaves_spinbox->setVisible(false); ui->m_slave_groups_checkbox->setVisible(false); ui->m_slave_groups_table->setVisible(false); populatePlcTable(); } else { if (m_plc_gb) m_plc_gb->setVisible(false); ui->max_slaves_checkbox->setVisible(true); ui->max_slaves_spinbox->setVisible(true); ui->m_slave_groups_checkbox->setVisible(true); ui->m_slave_groups_table->setVisible(true); } } else if (m_data.m_type == ElementData::Terminal) { ui->m_terminal_type_cb->setCurrentIndex( ui->m_terminal_type_cb->findData( m_data.m_terminal_type)); ui->m_terminal_func_cb->setCurrentIndex( ui->m_terminal_func_cb->findData( m_data.m_terminal_function)); } on_m_base_type_cb_currentIndexChanged(ui->m_base_type_cb->currentIndex()); } /** @brief ElementPropertiesEditorWidget::setUpInterface */ void ElementPropertiesEditorWidget::setUpInterface() { // Type combo box ui->m_base_type_cb->addItem (tr("Simple"), ElementData::Simple); ui->m_base_type_cb->addItem (tr("Maître"), ElementData::Master); ui->m_base_type_cb->addItem (tr("Esclave"), ElementData::Slave); ui->m_base_type_cb->addItem (tr("Renvoi de folio suivant"), ElementData::NextReport); ui->m_base_type_cb->addItem (tr("Renvoi de folio précédent"), ElementData::PreviousReport); ui->m_base_type_cb->addItem (tr("Bornier"), ElementData::Terminal); ui->m_base_type_cb->addItem (tr("Vignette"), ElementData::Thumbnail); ui->m_base_type_cb->addItem (tr("Définition de conducteur"), ElementData::ConductorDefinition); // Slave option ui->m_state_cb->addItem(tr("Normalement ouvert"), ElementData::NO); ui->m_state_cb->addItem(tr("Normalement fermé"), ElementData::NC); ui->m_state_cb->addItem(tr("Inverseur"), ElementData::SW); ui->m_state_cb->addItem(tr("Other"), ElementData::Other); ui->m_state_cb->addItem(tr("Esclave PLC"), ElementData::PLCSlave); ui->m_type_cb->addItem(tr("Simple"), ElementData::SSimple); ui->m_type_cb->addItem(tr("Puissance"), ElementData::Power); ui->m_type_cb->addItem(tr("Temporisé travail"), ElementData::DelayOn); ui->m_type_cb->addItem(tr("Temporisé repos"), ElementData::DelayOff); ui->m_type_cb->addItem(tr("Temporisé travail & repos"), ElementData::delayOnOff); //Master option ui->m_master_type_cb->addItem(tr("Bobine"), ElementData::Coil); ui->m_master_type_cb->addItem(tr("Organe de protection"), ElementData::Protection); ui->m_master_type_cb->addItem(tr("Commutateur / bouton"), ElementData::Commutator); ui->m_master_type_cb->addItem(tr("Module PLC"), ElementData::PLC); ui->m_master_type_cb->setMinimumWidth(150); //Terminal option ui->m_terminal_type_cb->addItem(tr("Générique"), ElementData::TTGeneric); ui->m_terminal_type_cb->addItem(tr("Fusible"), ElementData::TTFuse); ui->m_terminal_type_cb->addItem(tr("Séctionnable"), ElementData::TTSectional); ui->m_terminal_type_cb->addItem(tr("Diode"), ElementData::TTDiode); ui->m_terminal_type_cb->addItem(tr("Terre"), ElementData::TTGround); ui->m_terminal_func_cb->addItem(tr("Générique"), ElementData::TFGeneric); ui->m_terminal_func_cb->addItem(tr("Phase"), ElementData::TFPhase); ui->m_terminal_func_cb->addItem(tr("Neutre"), ElementData::TFNeutral); //Disable the edition of the first column of the information tree //by this little workaround ui->m_tree->setItemDelegate(new EditorDelegate(this)); // 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(); } }); // Connect master type combo box to show/hide PLC configuration connect(ui->m_master_type_cb, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) { auto master_type = ui->m_master_type_cb->itemData(index).value(); if (master_type == ElementData::PLC) { if (!m_plc_gb) { createPlcConfigWidgets(); } m_plc_gb->setVisible(true); ui->max_slaves_checkbox->setVisible(false); ui->max_slaves_spinbox->setVisible(false); ui->m_slave_groups_checkbox->setVisible(false); ui->m_slave_groups_table->setVisible(false); populatePlcTable(); } else { if (m_plc_gb) m_plc_gb->setVisible(false); ui->max_slaves_checkbox->setVisible(true); ui->max_slaves_spinbox->setVisible(true); ui->m_slave_groups_checkbox->setVisible(true); ui->m_slave_groups_table->setVisible(true); } }); // When "Esclave PLC" is selected in state combo, disable type combo connect(ui->m_state_cb, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) { bool is_plc = (ui->m_state_cb->itemData(index) == ElementData::PLCSlave); ui->m_type_cb->setEnabled(!is_plc); if (is_plc) { ui->m_type_cb->setCurrentIndex(0); } }); populateTree(); } void ElementPropertiesEditorWidget::updateTree() { auto type_ = ui->m_base_type_cb->currentData().value(); switch (type_) { case ElementData::Simple: ui->m_tree->setEnabled(true); break; case ElementData::Thumbnail: ui->m_tree->setEnabled(true); break; case ElementData::NextReport: ui->m_tree->setDisabled(true); break; case ElementData::PreviousReport: ui->m_tree->setDisabled(true); break; case ElementData::ConductorDefinition: ui->m_tree->setDisabled(true); break; case ElementData::Master: ui->m_tree->setEnabled(true); break; case ElementData::Slave: ui->m_tree->setDisabled(true); break; case ElementData::Terminal: ui->m_tree->setEnabled(true); break; default: ui->m_tree->setDisabled(true); break; } } /** @brief ElementPropertiesEditorWidget::populateTree Create QTreeWidgetItem of the tree widget and populate it */ void ElementPropertiesEditorWidget::populateTree() { const auto keys = QETInformation::elementEditorElementInfoKeys(); // For PLC Slave: add PLC-specific info keys at the top if (m_data.m_type == ElementData::Slave && m_data.m_slave_type == ElementData::PLCSlave) { QStringList plc_keys = { QETInformation::ELMT_PLC_TYPE, QETInformation::ELMT_PLC_ADDRESS, QETInformation::ELMT_PLC_FUNCTION, QETInformation::ELMT_PLC_COMMENT, QETInformation::ELMT_PLC_CROSSREF }; for (const QString &key : plc_keys) { QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_tree); qtwi->setFlags(Qt::ItemIsEnabled | Qt::ItemIsEditable); qtwi->setData(0, Qt::DisplayRole, QETInformation::translatedInfoKey(key)); qtwi->setData(0, Qt::UserRole, key); qtwi->setText(1, m_data.m_informations.value(key).toString()); qtwi->setForeground(0, QColor(0, 100, 180)); } } for(const QString& key : keys) { QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_tree); qtwi->setFlags(Qt::ItemIsEnabled | Qt::ItemIsEditable); qtwi->setData(0, Qt::DisplayRole, QETInformation::translatedInfoKey(key)); qtwi->setData(0, Qt::UserRole, key); qtwi->setText(1, m_data.m_informations.value(key).toString()); // Adjust column width ui->m_tree->resizeColumnToContents(0); } } /** @brief ElementPropertiesEditorWidget::on_m_buttonBox_accepted Action on button accepted : the new information is set */ void ElementPropertiesEditorWidget::on_m_buttonBox_accepted() { m_data.m_type = ui->m_base_type_cb->currentData().value(); if (m_data.m_type == ElementData::Slave) { auto state_val = ui->m_state_cb->currentData(); if (state_val == ElementData::PLCSlave) { m_data.m_slave_type = ElementData::PLCSlave; m_data.m_slave_state = ElementData::Other; } else { m_data.m_slave_state = state_val.value(); 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) { m_data.m_master_type = ui->m_master_type_cb->currentData().value(); //If the checkbox is checked, save the number; otherwise, -1 (infinity) if (ui->max_slaves_checkbox->isVisible() && ui->max_slaves_checkbox->isChecked()) { m_data.m_max_slaves = ui->max_slaves_spinbox->value(); } else { m_data.m_max_slaves = -1; } if (m_data.m_master_type == ElementData::PLC) { readPlcTable(); } else { readSlaveGroupsFromTable(); } } else if (m_data.m_type == ElementData::Terminal) { m_data.m_terminal_type = ui->m_terminal_type_cb->currentData().value(); m_data.m_terminal_function = ui->m_terminal_func_cb->currentData().value(); } for (QTreeWidgetItem *qtwi : ui->m_tree->invisibleRootItem()->takeChildren()) { QString txt = qtwi->text(1); //remove line feed and carriage return txt.remove("\r"); txt.remove("\n"); m_data.m_informations.addValue(qtwi->data(0, Qt::UserRole).toString(), txt); } this->close(); } /** @brief ElementPropertiesEditorWidget::on_m_base_type_cb_currentIndexChanged @param index : Action when combo-box base type index change */ void ElementPropertiesEditorWidget::on_m_base_type_cb_currentIndexChanged(int index) { bool slave = false , master = false, terminal = false; auto type_ = ui->m_base_type_cb->itemData(index).value(); if (type_ == ElementData::Slave) slave = true; else if (type_ == ElementData::Master) master = true; else if (type_ == ElementData::Terminal) terminal = true; ui->m_slave_gb->setVisible(slave); ui->m_master_gb->setVisible(master); ui->m_terminal_gb->setVisible(terminal); #if QT_VERSION >= QT_VERSION_CHECK(5,15,0) ui->tabWidget->setTabVisible(1, (type_ == ElementData::Simple || type_ == ElementData::Master)); #endif 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); } } /** * @brief ElementPropertiesEditorWidget::createPlcConfigWidgets * Create the PLC configuration widgets programmatically and add them to m_master_gb */ void ElementPropertiesEditorWidget::createPlcConfigWidgets() { // Create PLC group box m_plc_gb = new QGroupBox(tr("Configuration PLC"), ui->m_master_gb); auto *plc_layout = new QVBoxLayout(m_plc_gb); // Toolbar auto *toolbar = new QHBoxLayout(); auto *add_btn = new QPushButton(tr("+"), m_plc_gb); auto *remove_btn = new QPushButton(tr("-"), m_plc_gb); add_btn->setMaximumWidth(30); remove_btn->setMaximumWidth(30); toolbar->addStretch(); toolbar->addWidget(add_btn); toolbar->addWidget(remove_btn); plc_layout->addLayout(toolbar); // Tables side by side: IO table (left) + Terminal table (right) auto *tables_splitter = new QSplitter(Qt::Horizontal, m_plc_gb); // IO Table m_plc_table = new QTableWidget(tables_splitter); m_plc_table->setColumnCount(5); m_plc_table->setHorizontalHeaderLabels({ tr("Type"), tr("Adresse"), tr("Fonction"), tr("Commentaire"), tr("Réf. croisée") }); m_plc_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); m_plc_table->horizontalHeader()->setSectionsMovable(true); m_plc_table->horizontalHeader()->setSectionsClickable(true); m_plc_table->horizontalHeader()->resizeSection(0, 120); m_plc_table->horizontalHeader()->resizeSection(1, 100); m_plc_table->horizontalHeader()->resizeSection(2, 150); m_plc_table->horizontalHeader()->resizeSection(3, 150); m_plc_table->horizontalHeader()->resizeSection(4, 100); m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems); m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection); m_plc_table->setMinimumHeight(200); tables_splitter->addWidget(m_plc_table); // Terminal table (per IO: Nb + T1-T4) m_plc_terminal_table = new QTableWidget(tables_splitter); m_plc_terminal_table->setColumnCount(2); m_plc_terminal_table->setHorizontalHeaderLabels({ tr("Nb."), tr("T1") }); m_plc_terminal_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); m_plc_terminal_table->horizontalHeader()->resizeSection(0, 50); m_plc_terminal_table->horizontalHeader()->resizeSection(1, 80); m_plc_terminal_table->setSelectionBehavior(QAbstractItemView::SelectItems); m_plc_terminal_table->setSelectionMode(QAbstractItemView::ExtendedSelection); m_plc_terminal_table->setMinimumHeight(200); tables_splitter->addWidget(m_plc_terminal_table); tables_splitter->setStretchFactor(0, 3); tables_splitter->setStretchFactor(1, 1); tables_splitter->setSizes({500, 150}); plc_layout->addWidget(tables_splitter); // Font settings auto *font_layout = new QHBoxLayout(); m_plc_header_font_btn = new QPushButton(tr("Police des en-têtes"), m_plc_gb); m_plc_header_font_btn->setToolTip(tr("Configurer la police des en-têtes de colonnes")); connect(m_plc_header_font_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcSelectHeaderFont); font_layout->addWidget(m_plc_header_font_btn); m_plc_cell_font_btn = new QPushButton(tr("Police du texte"), m_plc_gb); m_plc_cell_font_btn->setToolTip(tr("Configurer la police du texte dans les cellules")); connect(m_plc_cell_font_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcSelectCellFont); font_layout->addWidget(m_plc_cell_font_btn); m_plc_show_headers_cb = new QCheckBox(tr("Afficher les en-têtes sur la feuille"), m_plc_gb); m_plc_show_headers_cb->setToolTip(tr("Afficher ou masquer les en-têtes de colonnes du tableau PLC sur la feuille")); m_plc_show_headers_cb->setChecked(true); font_layout->addWidget(m_plc_show_headers_cb); font_layout->addStretch(); plc_layout->addLayout(font_layout); // Display settings auto *settings_layout = new QGridLayout(); for (int i = 0; i < 4; ++i) { m_plc_break_checkboxes[i] = new QCheckBox( tr("Saut %1 après:").arg(i + 1), m_plc_gb); settings_layout->addWidget(m_plc_break_checkboxes[i], i, 0); m_plc_break_spinboxes[i] = new QSpinBox(m_plc_gb); m_plc_break_spinboxes[i]->setMinimum(0); m_plc_break_spinboxes[i]->setMaximum(128); m_plc_break_spinboxes[i]->setSpecialValueText(tr("Aucun")); m_plc_break_spinboxes[i]->setEnabled(false); settings_layout->addWidget(m_plc_break_spinboxes[i], i, 1); connect(m_plc_break_checkboxes[i], &QCheckBox::toggled, m_plc_break_spinboxes[i], &QSpinBox::setEnabled); } settings_layout->addWidget(new QLabel(tr("H. ligne:"), m_plc_gb), 4, 0); m_plc_row_height_spinbox = new QSpinBox(m_plc_gb); m_plc_row_height_spinbox->setMinimum(4); m_plc_row_height_spinbox->setMaximum(30); m_plc_row_height_spinbox->setValue(8); m_plc_row_height_spinbox->setSuffix(tr(" mm")); settings_layout->addWidget(m_plc_row_height_spinbox, 4, 1); settings_layout->setColumnStretch(2, 1); plc_layout->addLayout(settings_layout); // Column name, visibility and width auto *col_layout = new QHBoxLayout(); QStringList col_names = {tr("Type"), tr("Adresse"), tr("Fonction"), tr("Commentaire"), tr("Réf.")}; for (int i = 0; i < 5; ++i) { auto *col_widget = new QVBoxLayout(); auto *le = new QLineEdit(m_plc_gb); le->setPlaceholderText(col_names.at(i)); le->setToolTip(tr("Nom personnalisé de la colonne (vide = par défaut)")); m_plc_col_name_edits.append(le); col_widget->addWidget(le); auto *cb = new QCheckBox(tr("Visible"), m_plc_gb); cb->setChecked(true); m_plc_col_visibility_checkboxes.append(cb); col_widget->addWidget(cb); auto *sb = new QSpinBox(m_plc_gb); sb->setMinimum(10); sb->setMaximum(500); sb->setValue(40); sb->setSuffix(tr(" mm")); m_plc_col_width_spinboxes.append(sb); col_widget->addWidget(sb); col_layout->addLayout(col_widget); } plc_layout->addLayout(col_layout); // Add to master group box ui->m_master_gb->layout()->addWidget(m_plc_gb); // Connect signals connect(add_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcAddRow); connect(remove_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcRemoveRow); } /** * @brief ElementPropertiesEditorWidget::populatePlcTable * Fill the PLC table from m_data */ void ElementPropertiesEditorWidget::populatePlcTable() { if (!m_plc_table) return; m_plc_table->setRowCount(0); if (m_plc_terminal_table) m_plc_terminal_table->setRowCount(0); const auto &plc_data = m_data.m_plc_master_data; m_plc_table->setRowCount(plc_data.ios.size()); if (m_plc_terminal_table) m_plc_terminal_table->setRowCount(plc_data.ios.size()); for (int row = 0; row < plc_data.ios.size(); ++row) { const auto &io = plc_data.ios.at(row); // --- IO Table --- auto *type_cb = new QComboBox(m_plc_table); QStringList plc_types = ElementData::plcIOTypeList(); for (int t = 0; t < plc_types.size(); ++t) { type_cb->addItem(plc_types.at(t), t); } type_cb->setCurrentIndex(static_cast(io.type)); m_plc_table->setCellWidget(row, 0, type_cb); m_plc_table->setItem(row, 1, new QTableWidgetItem(io.address)); m_plc_table->setItem(row, 2, new QTableWidgetItem(io.functionText)); m_plc_table->setItem(row, 3, new QTableWidgetItem(io.comment)); auto *crossref_item = new QTableWidgetItem(io.crossRef); crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable); m_plc_table->setItem(row, 4, crossref_item); // --- Terminal Table --- if (m_plc_terminal_table) { int tc = qMax(1, io.terminalCount); // Ensure enough columns while (m_plc_terminal_table->columnCount() < tc + 1) m_plc_terminal_table->insertColumn(m_plc_terminal_table->columnCount()); auto *tc_sb = new QSpinBox(m_plc_terminal_table); tc_sb->setMinimum(1); tc_sb->setMaximum(4); tc_sb->setValue(tc); m_plc_terminal_table->setCellWidget(row, 0, tc_sb); connect(tc_sb, QOverload::of(&QSpinBox::valueChanged), this, [this, row](int val) { plcTerminalCountChanged(row, val); }); for (int i = 0; i < m_plc_terminal_table->columnCount() - 1; ++i) { QString val = (i < io.terminals.size()) ? io.terminals.at(i) : QString(); auto *item = new QTableWidgetItem(val); if (i >= tc) { item->setFlags(item->flags() & ~Qt::ItemIsEditable); item->setBackground(QColor(230, 230, 230)); } m_plc_terminal_table->setItem(row, i + 1, item); } } } // Load display settings for (int i = 0; i < 4; ++i) { int val = plc_data.breakPositions.value(i, 0); bool enabled = (val > 0); m_plc_break_checkboxes[i]->setChecked(enabled); m_plc_break_spinboxes[i]->setValue(enabled ? val : 0); } m_plc_row_height_spinbox->setValue(static_cast(plc_data.rowHeight)); // Load fonts m_plc_header_font = plc_data.headerFont; m_plc_cell_font = plc_data.cellFont; if (m_plc_header_font.family().isEmpty()) { m_plc_header_font = QFont(m_plc_table->font()); m_plc_header_font.setBold(true); m_plc_header_font.setPointSize(8); } if (m_plc_cell_font.family().isEmpty()) { m_plc_cell_font = QFont(m_plc_table->font()); m_plc_cell_font.setPointSize(8); } m_plc_header_font_btn->setText(tr("Police des en-têtes: %1 %2pt") .arg(m_plc_header_font.family()).arg(m_plc_header_font.pointSize())); m_plc_cell_font_btn->setText(tr("Police du texte: %1 %2pt") .arg(m_plc_cell_font.family()).arg(m_plc_cell_font.pointSize())); m_plc_show_headers_cb->setChecked(plc_data.showHeaders); // UI columns 0-4 map to data model indices 0,1,2,3,4 const int ui_to_data[] = {0, 1, 2, 3, 4}; for (int i = 0; i < 5; ++i) { int di = ui_to_data[i]; m_plc_col_visibility_checkboxes.at(i)->setChecked( plc_data.colVisible.value(di, true)); m_plc_col_width_spinboxes.at(i)->setValue( static_cast(plc_data.colWidths.value(di, 40))); if (i < plc_data.columnNames.size()) m_plc_col_name_edits.at(i)->setText(plc_data.columnNames.at(i)); else m_plc_col_name_edits.at(i)->clear(); } // Restore column visual order (translate data model index → table col index) QHeaderView *hdr = m_plc_table->horizontalHeader(); if (!plc_data.columnOrder.isEmpty()) { const int data_to_table[] = {0, 1, 2, 3, 4}; for (int visual = 0; visual < plc_data.columnOrder.size(); ++visual) { int data_idx = plc_data.columnOrder.at(visual); if (data_idx < 0 || data_idx > 4) continue; int logical = data_to_table[data_idx]; if (logical < 0 || logical >= 5) continue; if (hdr->visualIndex(logical) != visual) hdr->moveSection(hdr->visualIndex(logical), visual); } } } /** * @brief ElementPropertiesEditorWidget::readPlcTable * Read the PLC data from the table back into m_data */ void ElementPropertiesEditorWidget::readPlcTable() { if (!m_plc_table) return; ElementData::PlcMasterData plc_data; for (int row = 0; row < m_plc_table->rowCount(); ++row) { ElementData::PlcIO io; auto *type_cb = qobject_cast(m_plc_table->cellWidget(row, 0)); if (type_cb) io.type = static_cast(type_cb->currentData().toInt()); auto *addr_item = m_plc_table->item(row, 1); if (addr_item) io.address = addr_item->text(); auto *func_item = m_plc_table->item(row, 2); if (func_item) io.functionText = func_item->text(); auto *comment_item = m_plc_table->item(row, 3); if (comment_item) io.comment = comment_item->text(); auto *crossref_item = m_plc_table->item(row, 4); if (crossref_item) io.crossRef = crossref_item->text(); // Read terminal data from terminal table if (m_plc_terminal_table && row < m_plc_terminal_table->rowCount()) { auto *tc_sb = qobject_cast(m_plc_terminal_table->cellWidget(row, 0)); if (tc_sb) { io.terminalCount = tc_sb->value(); for (int i = 0; i < io.terminalCount && i < 4; ++i) { auto *term_item = m_plc_terminal_table->item(row, i + 1); io.terminals.append(term_item ? term_item->text() : QString()); } } } plc_data.ios.append(io); } plc_data.rowHeight = m_plc_row_height_spinbox->value(); // Save break positions for (int i = 0; i < 4; ++i) { if (m_plc_break_checkboxes[i]->isChecked()) plc_data.breakPositions.append(m_plc_break_spinboxes[i]->value()); else plc_data.breakPositions.append(0); } // UI columns 0-4 map to data model indices 0,1,2,3,4 const int ui_to_data[] = {0, 1, 2, 3, 4}; for (int i = 0; i < 5; ++i) { int di = ui_to_data[i]; plc_data.colVisible[di] = m_plc_col_visibility_checkboxes.at(i)->isChecked(); plc_data.colWidths[di] = m_plc_col_width_spinboxes.at(i)->value(); QString name = m_plc_col_name_edits.at(i)->text().trimmed(); if (!name.isEmpty()) plc_data.columnNames.append(name); else plc_data.columnNames.append(QString()); } // Save current column visual order (translate table col index → data model index) QHeaderView *hdr = m_plc_table->horizontalHeader(); const int table_to_data[] = {0, 1, 2, 3, 4}; for (int visual = 0; visual < 5; ++visual) { int table_col = hdr->logicalIndex(visual); plc_data.columnOrder.append(table_to_data[table_col]); } plc_data.headerFont = m_plc_header_font; plc_data.cellFont = m_plc_cell_font; plc_data.showHeaders = m_plc_show_headers_cb->isChecked(); m_data.setPlcMasterData(plc_data); } /** * @brief ElementPropertiesEditorWidget::plcAddRow * Add a new empty row to the PLC IO table */ void ElementPropertiesEditorWidget::plcAddRow() { if (!m_plc_table) return; int row = m_plc_table->rowCount(); m_plc_table->insertRow(row); // Type combo auto *type_cb = new QComboBox(m_plc_table); QStringList plc_types = ElementData::plcIOTypeList(); for (int t = 0; t < plc_types.size(); ++t) { type_cb->addItem(plc_types.at(t), t); } m_plc_table->setCellWidget(row, 0, type_cb); m_plc_table->setItem(row, 1, new QTableWidgetItem()); m_plc_table->setItem(row, 2, new QTableWidgetItem()); m_plc_table->setItem(row, 3, new QTableWidgetItem()); auto *crossref_item = new QTableWidgetItem(); crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable); m_plc_table->setItem(row, 4, crossref_item); // Terminal table row if (m_plc_terminal_table) { m_plc_terminal_table->insertRow(row); // Ensure enough columns (at least Nb + T1) while (m_plc_terminal_table->columnCount() < 2) m_plc_terminal_table->insertColumn(m_plc_terminal_table->columnCount()); auto *tc_sb = new QSpinBox(m_plc_terminal_table); tc_sb->setMinimum(1); tc_sb->setMaximum(4); tc_sb->setValue(1); m_plc_terminal_table->setCellWidget(row, 0, tc_sb); connect(tc_sb, QOverload::of(&QSpinBox::valueChanged), this, [this, row](int val) { plcTerminalCountChanged(row, val); }); // T1 editable, rest gray auto *t1_item = new QTableWidgetItem(); m_plc_terminal_table->setItem(row, 1, t1_item); for (int i = 2; i < m_plc_terminal_table->columnCount(); ++i) { auto *item = new QTableWidgetItem(); item->setFlags(item->flags() & ~Qt::ItemIsEditable); item->setBackground(QColor(230, 230, 230)); m_plc_terminal_table->setItem(row, i, item); } } } /** * @brief ElementPropertiesEditorWidget::plcTerminalCountChanged * Enable/disable T1-T4 cells and grow columns based on terminal count spinner for a row */ void ElementPropertiesEditorWidget::plcTerminalCountChanged(int row, int count) { Q_UNUSED(row) if (!m_plc_terminal_table) return; // Find max terminal count across all rows int max_tc = 1; for (int r = 0; r < m_plc_terminal_table->rowCount(); ++r) { auto *sb = qobject_cast(m_plc_terminal_table->cellWidget(r, 0)); if (sb && sb->value() > max_tc) max_tc = sb->value(); } // Ensure enough columns int needed = max_tc + 1; // +1 for Nb column while (m_plc_terminal_table->columnCount() < needed) m_plc_terminal_table->insertColumn(m_plc_terminal_table->columnCount()); // Rebuild headers QStringList th; th << tr("Nb."); for (int i = 1; i < m_plc_terminal_table->columnCount(); ++i) th << tr("T%1").arg(i); m_plc_terminal_table->setHorizontalHeaderLabels(th); // Enable/disable cells per row for (int r = 0; r < m_plc_terminal_table->rowCount(); ++r) { auto *sb = qobject_cast(m_plc_terminal_table->cellWidget(r, 0)); int tc = sb ? sb->value() : 1; for (int c = 1; c < m_plc_terminal_table->columnCount(); ++c) { auto *item = m_plc_terminal_table->item(r, c); if (!item) { item = new QTableWidgetItem(); m_plc_terminal_table->setItem(r, c, item); } if (c <= tc) { item->setFlags(item->flags() | Qt::ItemIsEditable); item->setBackground(Qt::NoBrush); } else { item->setFlags(item->flags() & ~Qt::ItemIsEditable); item->setText(QString()); item->setBackground(QColor(230, 230, 230)); } } } } /** * @brief ElementPropertiesEditorWidget::plcRemoveRow * Remove selected rows from the PLC IO table */ void ElementPropertiesEditorWidget::plcRemoveRow() { if (!m_plc_table) return; QModelIndexList selected = m_plc_table->selectionModel()->selectedRows(); if (selected.isEmpty()) return; // Remove from bottom to top std::sort(selected.begin(), selected.end(), [](const QModelIndex &a, const QModelIndex &b) { return a.row() > b.row(); }); for (const QModelIndex &idx : selected) { m_plc_table->removeRow(idx.row()); if (m_plc_terminal_table && idx.row() < m_plc_terminal_table->rowCount()) m_plc_terminal_table->removeRow(idx.row()); } } /** * @brief ElementPropertiesEditorWidget::plcPasteFromClipboard * Paste IO data from clipboard (tab-separated, e.g. from Excel) */ void ElementPropertiesEditorWidget::plcPasteFromClipboard() { if (!m_plc_table) return; QString clipboard_text = QApplication::clipboard()->text(); if (clipboard_text.isEmpty()) return; QStringList lines = clipboard_text.split('\n', Qt::SkipEmptyParts); int start_row = m_plc_table->rowCount(); m_plc_table->setRowCount(start_row + lines.size()); for (int i = 0; i < lines.size(); ++i) { QStringList cells = lines.at(i).split('\t'); int row = start_row + i; // Type combo auto *type_cb = new QComboBox(m_plc_table); QStringList plc_types = ElementData::plcIOTypeList(); for (int t = 0; t < plc_types.size(); ++t) { type_cb->addItem(plc_types.at(t), t); } // Try to match type from clipboard if (!cells.isEmpty()) { QString type_str = cells.at(0).trimmed(); int type_idx = -1; for (int t = 0; t < plc_types.size(); ++t) { if (plc_types.at(t).compare(type_str, Qt::CaseInsensitive) == 0) { type_idx = t; break; } } if (type_idx >= 0) type_cb->setCurrentIndex(type_idx); } m_plc_table->setCellWidget(row, 0, type_cb); // Address m_plc_table->setItem(row, 1, new QTableWidgetItem( cells.size() > 1 ? cells.at(1).trimmed() : QString())); // Function text m_plc_table->setItem(row, 2, new QTableWidgetItem( cells.size() > 2 ? cells.at(2).trimmed() : QString())); // Comment m_plc_table->setItem(row, 3, new QTableWidgetItem( cells.size() > 3 ? cells.at(3).trimmed() : QString())); // CrossRef (read-only) auto *crossref_item = new QTableWidgetItem( cells.size() > 4 ? cells.at(4).trimmed() : QString()); crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable); m_plc_table->setItem(row, 4, crossref_item); } } /** * @brief ElementPropertiesEditorWidget::plcSelectHeaderFont * Open font dialog to select column header font */ void ElementPropertiesEditorWidget::plcSelectHeaderFont() { bool ok; QFont font = QFontDialog::getFont(&ok, m_plc_header_font, this, tr("Police des en-têtes de colonnes")); if (ok) { m_plc_header_font = font; m_plc_header_font_btn->setText(tr("Police des en-têtes: %1 %2pt") .arg(font.family()).arg(font.pointSize())); } } /** * @brief ElementPropertiesEditorWidget::plcSelectCellFont * Open font dialog to select cell text font */ void ElementPropertiesEditorWidget::plcSelectCellFont() { bool ok; QFont font = QFontDialog::getFont(&ok, m_plc_cell_font, this, tr("Police du texte des cellules")); if (ok) { m_plc_cell_font = font; m_plc_cell_font_btn->setText(tr("Police du texte: %1 %2pt") .arg(font.family()).arg(font.pointSize())); } }