/* 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 "../../qet.h" #include "../../qetapp.h" #include "../../qetinformation.h" #include "../../TerminalStrip/terminalcatalog.h" #include "../../TerminalStrip/terminalcolor.h" #include "../../TerminalStrip/realterminal.h" #include "ui_elementpropertieseditorwidget.h" #include "../../qet.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include QWidget* EditorDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const { if(index.column() == 1) { const QString key = index.sibling(index.row(), 0) .data(Qt::UserRole).toString(); if (key == QETInformation::ELMT_WIDTH || key == QETInformation::ELMT_HEIGHT || key == QETInformation::ELMT_DEPTH) { auto *line_edit = new QLineEdit(parent); auto *validator = new QETInformation::NumericInfoValidator(line_edit); line_edit->setValidator(validator); line_edit->setPlaceholderText(tr("e.g. 80.5")); line_edit->setToolTip(tr("Decimal number with a dot as separator (e.g. 80.5)")); return line_edit; } return QItemDelegate::createEditor(parent, option, index); } return nullptr; } namespace { /** * @brief leadingNumber * Extract the first number of a text like "2,5 mm²" or "5.1 mm - WAGO". * The comma and the dot are both accepted as decimal separator. * @return the number, or 0 if the text is empty or has no number. */ double leadingNumber(const QString &text) { static const QRegularExpression re(QStringLiteral("(\\d+(?:[.,]\\d+)?)")); const auto match = re.match(text); if (!match.hasMatch()) return 0; auto number = match.captured(1); number.replace(QLatin1Char(','), QLatin1Char('.')); return number.toDouble(); } /// Value of a terminal size combo (picked in the list or typed). double comboValue(const QComboBox *cb) { return leadingNumber(cb->currentText()); } /// Color of a terminal combo as "#rrggbb" or "#rrggbb/#rrggbb" (empty = none). /// A text which is not understood keeps the value the combo was loaded with. QString colorSpec(const QComboBox *cb) { const auto index = cb->findText(cb->currentText()); if (index >= 0) return cb->itemData(index).toString(); QColor main_, stripe_; if (TerminalColor::parse(cb->currentText(), &main_, &stripe_)) return TerminalColor::toSpec(main_, stripe_); return cb->property("origSpec").toString(); } /// Set the text of an editable combo : select the item with this value or write the value. void setComboValue(QComboBox *cb, double value, const QString &suffix) { if (value <= 0) { cb->setCurrentIndex(0); return; } for (int i = 1; i < cb->count(); ++i) { if (qFuzzyCompare(1 + cb->itemData(i).toDouble(), 1 + value)) { cb->setCurrentIndex(i); return; } } cb->setCurrentText(QLocale().toString(value, 'g', 6) + suffix); } } /** * @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); QET::trackDialogGeometry(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)); const DiagramContext &info = m_data.m_informations; ui->m_auto_num_locked_cb->setChecked( QET::infoFlagIsTrue(info.value(QStringLiteral("auto_num_locked")).toString())); ui->m_potential_isolating_cb->setChecked( QET::infoFlagIsTrue(info.value(QStringLiteral("potential_isolating")).toString())); //Section (mm2) and width (mm), stored with the dot as decimal separator populateTerminalSizeCombos( info.value(RealTerminal::sectionInfoKey()).toString().toDouble(), info.value(RealTerminal::widthInfoKey()).toString().toDouble()); populateTerminalColorCombo(info.value(RealTerminal::colorInfoKey()).toString()); } ui->m_exclude_from_bom_cb->setChecked( QET::infoFlagIsTrue(m_data.m_informations.value(QStringLiteral("exclude_from_bom")).toString())); 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("Master"), ElementData::Master); ui->m_base_type_cb->addItem (tr("Slave"), ElementData::Slave); ui->m_base_type_cb->addItem (tr("Next sheet reference"), ElementData::NextReport); ui->m_base_type_cb->addItem (tr("Previous sheet reference"), ElementData::PreviousReport); ui->m_base_type_cb->addItem (tr("Terminal block"), ElementData::Terminal); ui->m_base_type_cb->addItem (tr("Thumbnail"), ElementData::Thumbnail); ui->m_base_type_cb->addItem (tr("Conductor definition"), ElementData::ConductorDefinition); // Slave option ui->m_state_cb->addItem(tr("Switch normally open"), ElementData::NO); ui->m_state_cb->addItem(tr("Switch normally closed"), ElementData::NC); ui->m_state_cb->addItem(tr("switch"), ElementData::SW); ui->m_state_cb->addItem(tr("Other"), ElementData::Other); ui->m_state_cb->addItem(tr("PLC slave"), ElementData::PLCSlave); ui->m_type_cb->addItem(tr("Simple"), ElementData::SSimple); ui->m_type_cb->addItem(tr("Power switch"), ElementData::Power); ui->m_type_cb->addItem(tr("Switch-on delayed"), ElementData::DelayOn); ui->m_type_cb->addItem(tr("Switch-off delayed"), ElementData::DelayOff); ui->m_type_cb->addItem(tr("Switch-on and off delayed"), ElementData::delayOnOff); //Master option ui->m_master_type_cb->addItem(tr("coil"), ElementData::Coil); ui->m_master_type_cb->addItem(tr("Organ of protection"), ElementData::Protection); ui->m_master_type_cb->addItem(tr("Switch / button"), ElementData::Commutator); ui->m_master_type_cb->addItem(tr("PLC module"), ElementData::PLC); ui->m_master_type_cb->setMinimumWidth(150); //Terminal option ui->m_terminal_type_cb->addItem(tr("Generic"), ElementData::TTGeneric); ui->m_terminal_type_cb->addItem(tr("Fuse"), ElementData::TTFuse); ui->m_terminal_type_cb->addItem(tr("Sectionable"), ElementData::TTSectional); ui->m_terminal_type_cb->addItem(tr("Diode"), ElementData::TTDiode); ui->m_terminal_type_cb->addItem(tr("Ground"), ElementData::TTGround); ui->m_terminal_type_cb->addItem(tr("Anti-parallel diodes"), ElementData::TTAntiParallelDiodes); ui->m_terminal_type_cb->addItem(tr("Resistor"), ElementData::TTResistor); ui->m_terminal_type_cb->addItem(tr("Varistor"), ElementData::TTVaristor); ui->m_terminal_type_cb->addItem(tr("Test socket"), ElementData::TTTestSocket); ui->m_terminal_type_cb->addItem(tr("Diode (reversed)"), ElementData::TTDiodeReversed); ui->m_terminal_func_cb->addItem(tr("Generic"), ElementData::TFGeneric); ui->m_terminal_func_cb->addItem(tr("Phase"), ElementData::TFPhase); ui->m_terminal_func_cb->addItem(tr("Neutral"), 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, &QCheckBox::toggled, ui->max_slaves_spinbox, &QWidget::setEnabled); 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->setEnabled(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::populateTerminalSizeCombos Fill the section and width combo boxes of the terminal group box and select @a section (mm2) and @a width (mm), 0 means "none". The combo boxes are editable : a value not in the list can be typed. */ void ElementPropertiesEditorWidget::populateTerminalSizeCombos(double section, double width) { QSignalBlocker blocker(ui->m_terminal_section_cb); ui->m_terminal_section_cb->clear(); ui->m_terminal_section_cb->addItem(tr("None (default)"), 0.0); for (const auto section_ : TerminalCatalog::sections()) { ui->m_terminal_section_cb->addItem( TerminalCatalog::sectionText(section_, tr("%1 mm²").arg(QLocale().toString(section_, 'g', 6))), section_); } setComboValue(ui->m_terminal_section_cb, section, tr(" mm²")); connect(ui->m_terminal_section_cb, QOverload::of(&QComboBox::currentIndexChanged), this, [this]() { refreshTerminalWidthCombo(); }); connect(ui->m_terminal_section_cb->lineEdit(), &QLineEdit::editingFinished, this, [this]() { refreshTerminalWidthCombo(); }); refreshTerminalWidthCombo(); setComboValue(ui->m_terminal_width_cb, width, tr(" mm")); } /** @brief ElementPropertiesEditorWidget::populateTerminalColorCombo Fill the color combo box with the palette of the terminal strip and select @a spec ("#rrggbb" or "#rrggbb/#rrggbb", empty = none). The combo box is editable : a color can be typed as a name or as "#rrggbb". */ void ElementPropertiesEditorWidget::populateTerminalColorCombo(const QString &spec) { auto *cb = ui->m_terminal_color_cb; cb->setProperty("origSpec", spec); cb->clear(); cb->addItem(tr("None (default)"), QString()); for (const auto &entry : TerminalColor::palette()) { QColor main_, stripe_; TerminalColor::parse(TerminalColor::specOf(entry), &main_, &stripe_); cb->addItem(QIcon(TerminalColor::swatch(main_, stripe_)), TerminalColor::translatedName(entry), TerminalColor::specOf(entry)); } if (spec.isEmpty()) { cb->setCurrentIndex(0); return; } const auto index = cb->findData(spec); if (index >= 0) { cb->setCurrentIndex(index); } else { //The color is not in the palette, keep it in the list QColor main_, stripe_; TerminalColor::parse(spec, &main_, &stripe_); cb->addItem(QIcon(TerminalColor::swatch(main_, stripe_)), spec, spec); cb->setCurrentIndex(cb->count() - 1); } } /** @brief ElementPropertiesEditorWidget::refreshTerminalWidthCombo List the manufacturer terminals compatible with the selected section. If the section has no match in the catalog (or is not set), all the widths are listed. The current width is kept. */ void ElementPropertiesEditorWidget::refreshTerminalWidthCombo() { const auto current_width = comboValue(ui->m_terminal_width_cb); const auto section = comboValue(ui->m_terminal_section_cb); auto item_text = [](const TerminalCatalog::WidthEntry &entry) { return tr("%1 mm").arg(QLocale().toString(entry.mm, 'g', 6)) + QStringLiteral(" - ") + QString::fromUtf8(entry.label); }; bool has_compatible_entry = false; if (section > 0) { for (const auto &entry : TerminalCatalog::widthEntries()) { if (qFuzzyCompare(1 + entry.section, 1 + section)) { has_compatible_entry = true; break; } } } QSignalBlocker blocker(ui->m_terminal_width_cb); ui->m_terminal_width_cb->clear(); ui->m_terminal_width_cb->addItem(tr("None (default)"), 0.0); for (const auto &entry : TerminalCatalog::widthEntries()) { if (!has_compatible_entry || qFuzzyCompare(1 + entry.section, 1 + section)) { ui->m_terminal_width_cb->addItem(item_text(entry), entry.mm); } } setComboValue(ui->m_terminal_width_cb, current_width, tr(" mm")); } /** @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 ((m_data.m_master_type == ElementData::Coil || m_data.m_master_type == ElementData::Protection || m_data.m_master_type == ElementData::Commutator) && 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); } if (m_data.m_type == ElementData::Terminal) { if (ui->m_auto_num_locked_cb->isChecked()) m_data.m_informations.addValue(QStringLiteral("auto_num_locked"), QStringLiteral("true")); else m_data.m_informations.remove(QStringLiteral("auto_num_locked")); if (ui->m_potential_isolating_cb->isChecked()) m_data.m_informations.addValue(QStringLiteral("potential_isolating"), QStringLiteral("true")); else m_data.m_informations.remove(QStringLiteral("potential_isolating")); //Section and width : stored with the dot as decimal separator, whatever the locale const auto section_ = comboValue(ui->m_terminal_section_cb); if (section_ > 0) m_data.m_informations.addValue(RealTerminal::sectionInfoKey(), QString::number(section_, 'g', 6)); else m_data.m_informations.remove(RealTerminal::sectionInfoKey()); const auto width_ = comboValue(ui->m_terminal_width_cb); if (width_ > 0) m_data.m_informations.addValue(RealTerminal::widthInfoKey(), QString::number(width_, 'g', 6)); else m_data.m_informations.remove(RealTerminal::widthInfoKey()); const auto color_ = colorSpec(ui->m_terminal_color_cb); if (!color_.isEmpty()) m_data.m_informations.addValue(RealTerminal::colorInfoKey(), color_); else m_data.m_informations.remove(RealTerminal::colorInfoKey()); } if (ui->m_exclude_from_bom_cb->isChecked()) m_data.m_informations.addValue(QStringLiteral("exclude_from_bom"), QStringLiteral("true")); else m_data.m_informations.remove(QStringLiteral("exclude_from_bom")); 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); //Every base type whose tree updateTree() enables and whose data //ElementScene::toXml() writes. These three checks were never //reconciled, which is how Terminal and Thumbnail ended up with a //working tree and write path behind a hidden tab. ui->tabWidget->setTabVisible(1, (type_ == ElementData::Simple || type_ == ElementData::Master || type_ == ElementData::Slave || type_ == ElementData::Terminal || type_ == ElementData::Thumbnail)); 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 (!m_data.m_slave_contact_groups.isEmpty()) { ui->max_slaves_spinbox->setValue(m_data.m_slave_contact_groups.size()); } } 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; // 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("Switch normally open"), ElementData::NO); type_cb->addItem(tr("Switch normally closed"), ElementData::NC); type_cb->addItem(tr("switch"), ElementData::SW); type_cb->addItem(tr("Other", "fr: 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("Power switch"), ElementData::Power); subtype_cb->addItem(tr("Switch-on delayed"), ElementData::DelayOn); subtype_cb->addItem(tr("Switch-off delayed"), ElementData::DelayOff); subtype_cb->addItem(tr("Switch-on and off delayed"), 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); // When the contact count changes, keep the terminal count in step // with it, otherwise the two drift apart (both are edited // independently): the stored terminals-per-contact ratio is kept, // or the contact type default (2, 3 for a switch) is used when the // stored values don't divide evenly (inconsistent legacy data). const int old_contacts = group.contactCount; const int old_terminals = group.terminalCount; connect(contact_ct, QOverload::of(&QSpinBox::valueChanged), this, [this, i, old_contacts, old_terminals](int val) { if (i < m_data.m_slave_contact_groups.size()) { readSlaveGroupsFromTable(); auto &group = m_data.m_slave_contact_groups[i]; int per_pole = old_terminals / qMax(1, old_contacts); if (per_pole < 1 || old_terminals % qMax(1, old_contacts) != 0) per_pole = group.type == ElementData::SW ? 3 : 2; else if (group.type == ElementData::SW && per_pole < 3) per_pole = 3; //a switch needs common, NC and NO group.contactCount = val; group.terminalCount = val * per_pole; populateSlaveGroupsTable(); } }); // Terminal count auto *terminal_ct = new QSpinBox(ui->m_slave_groups_table); terminal_ct->setMinimum(1); 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("PLC Configuration"), 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) // Both share a single vertical scrollbar on the right auto *tables_container = new QWidget(m_plc_gb); auto *tables_layout = new QHBoxLayout(tables_container); tables_layout->setContentsMargins(0, 0, 0, 0); auto *splitter = new QSplitter(Qt::Horizontal, tables_container); // IO Table m_plc_table = new QTableWidget(splitter); m_plc_table->setColumnCount(5); m_plc_table->setHorizontalHeaderLabels({ tr("Type"), tr("Address"), tr("Function"), tr("Annotation"), tr("Cross-reference") }); 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::SelectRows); m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection); m_plc_table->setMinimumHeight(200); m_plc_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Terminal table (per IO: Nb + T1-T4) m_plc_terminal_table = new QTableWidget(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); m_plc_terminal_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Shared vertical scrollbar m_plc_shared_scrollbar = new QScrollBar(Qt::Vertical, tables_container); m_plc_shared_scrollbar->setMinimum(0); splitter->addWidget(m_plc_table); splitter->addWidget(m_plc_terminal_table); splitter->setStretchFactor(0, 3); splitter->setStretchFactor(1, 1); tables_layout->addWidget(splitter); tables_layout->addWidget(m_plc_shared_scrollbar); // Bidirectional sync between tables and shared scrollbar // Use flag to prevent infinite loops connect(m_plc_shared_scrollbar, &QScrollBar::valueChanged, this, [this](int value) { if (m_plc_scroll_sync) return; m_plc_scroll_sync = true; m_plc_table->verticalScrollBar()->setValue(value); m_plc_terminal_table->verticalScrollBar()->setValue(value); m_plc_scroll_sync = false; }); connect(m_plc_table->verticalScrollBar(), &QScrollBar::valueChanged, this, [this](int value) { if (m_plc_scroll_sync) return; m_plc_scroll_sync = true; m_plc_shared_scrollbar->setValue(value); m_plc_terminal_table->verticalScrollBar()->setValue(value); m_plc_scroll_sync = false; }); connect(m_plc_terminal_table->verticalScrollBar(), &QScrollBar::valueChanged, this, [this](int value) { if (m_plc_scroll_sync) return; m_plc_scroll_sync = true; m_plc_shared_scrollbar->setValue(value); m_plc_table->verticalScrollBar()->setValue(value); m_plc_scroll_sync = false; }); // Update shared scrollbar range from both tables auto syncRange = [this]() { int max = qMax(m_plc_table->verticalScrollBar()->maximum(), m_plc_terminal_table->verticalScrollBar()->maximum()); m_plc_shared_scrollbar->blockSignals(true); m_plc_shared_scrollbar->setMaximum(max); m_plc_shared_scrollbar->blockSignals(false); }; connect(m_plc_table->verticalScrollBar(), &QScrollBar::rangeChanged, this, syncRange); connect(m_plc_terminal_table->verticalScrollBar(), &QScrollBar::rangeChanged, this, syncRange); plc_layout->addWidget(tables_container); // Font settings auto *font_layout = new QHBoxLayout(); m_plc_header_font_btn = new QPushButton(tr("Header font"), m_plc_gb); m_plc_header_font_btn->setToolTip(tr("Set the font for column headers")); 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("Text font"), m_plc_gb); m_plc_cell_font_btn->setToolTip(tr("Set the text font in the cells")); 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("Show headers on the sheet"), m_plc_gb); m_plc_show_headers_cb->setToolTip(tr("Show or hide the column headings of the PLC table on the sheet")); 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("Jump to %1 after:").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("None")); 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("Line H:"), 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("Address"), tr("Function"), tr("Annotation"), tr("Ref.")}; 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("Custom column name (blank = default)")); 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 - row 4, full width auto *gl = qobject_cast(ui->m_master_gb->layout()); gl->addWidget(m_plc_gb, 4, 0, 1, 2); // Connect signals connect(add_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcAddRow); connect(remove_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcRemoveRow); // Ctrl+V shortcut for paste auto *paste_shortcut = new QShortcut(QKeySequence::Paste, m_plc_table); connect(paste_shortcut, &QShortcut::activated, this, &ElementPropertiesEditorWidget::plcPasteFromClipboard); // Context menu for the PLC table m_plc_table->setContextMenuPolicy(Qt::CustomContextMenu); connect(m_plc_table, &QTableWidget::customContextMenuRequested, this, [this](const QPoint &pos) { QMenu menu; menu.addAction(tr("Paste from the clipboard"), this, &ElementPropertiesEditorWidget::plcPasteFromClipboard); menu.exec(m_plc_table->mapToGlobal(pos)); }); } /** * @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 = QETApp::diagramTextsFont(); m_plc_header_font.setBold(true); m_plc_header_font.setPointSize(8); } if (m_plc_cell_font.family().isEmpty()) { m_plc_cell_font = QETApp::diagramTextsFont(); m_plc_cell_font.setPointSize(8); } m_plc_header_font_btn->setText(tr("Heading font size: %1 %2pt") .arg(m_plc_header_font.family()).arg(m_plc_header_font.pointSize())); m_plc_cell_font_btn->setText(tr("Font size: %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); } } // Ensure scrollbars stay hidden and sync shared scrollbar range if (m_plc_table) { m_plc_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_plc_table->verticalScrollBar()->setVisible(false); } if (m_plc_terminal_table) { m_plc_terminal_table->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); m_plc_terminal_table->verticalScrollBar()->setVisible(false); } if (m_plc_shared_scrollbar && m_plc_table) { int max = qMax(m_plc_table->verticalScrollBar()->maximum(), m_plc_terminal_table->verticalScrollBar()->maximum()); m_plc_shared_scrollbar->setMaximum(max); } } /** * @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) Q_UNUSED(count) 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); if (lines.isEmpty()) return; bool has_tabs = false; for (const QString &line : lines) { if (line.contains('\t')) { has_tabs = true; break; } } if (!has_tabs) { // Vertical paste: values go down the same column int target_col = m_plc_table->currentColumn(); if (target_col < 0) target_col = 0; int target_row = m_plc_table->currentRow(); if (target_row < 0) target_row = 0; int max_rows = m_plc_table->rowCount(); for (int i = 0; i < lines.size(); ++i) { int row = target_row + i; if (row >= max_rows) break; plcSetCellFromValue(row, target_col, lines.at(i).trimmed()); } } else { // Horizontal paste: each line is a separate IO row int target_row = m_plc_table->currentRow(); if (target_row < 0) target_row = 0; int max_rows = m_plc_table->rowCount(); for (int i = 0; i < lines.size(); ++i) { int row = target_row + i; if (row >= max_rows) break; QStringList cells = lines.at(i).split('\t'); for (int c = 0; c < cells.size(); ++c) { if (c > 4) break; plcSetCellFromValue(row, c, cells.at(c).trimmed()); } } } } /** * @brief ElementPropertiesEditorWidget::plcSetCellFromValue * Set a single table cell value, respecting the column widget type. */ void ElementPropertiesEditorWidget::plcSetCellFromValue(int row, int col, const QString &val) { if (!m_plc_table || row < 0 || col < 0 || col > 4) return; if (col == 0) { auto *type_cb = qobject_cast(m_plc_table->cellWidget(row, col)); if (!type_cb) return; if (!val.isEmpty()) { QStringList plc_types = ElementData::plcIOTypeList(); for (int t = 0; t < plc_types.size(); ++t) { if (plc_types.at(t).compare(val, Qt::CaseInsensitive) == 0) { type_cb->setCurrentIndex(t); return; } } } } else if (col == 4) { // CrossRef - read-only auto *item = new QTableWidgetItem(val); item->setFlags(item->flags() & ~Qt::ItemIsEditable); m_plc_table->setItem(row, col, item); } else { // Text columns: Address, Function, Comment m_plc_table->setItem(row, col, new QTableWidgetItem(val)); } } /** * @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("Column header formatting")); if (ok) { m_plc_header_font = font; m_plc_header_font_btn->setText(tr("Heading font size: %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("Cell text font")); if (ok) { m_plc_cell_font = font; m_plc_cell_font_btn->setText(tr("Font size: %1 %2pt") .arg(font.family()).arg(font.pointSize())); } }