PLC Manager

This commit is contained in:
Kellermorph
2026-07-27 22:16:52 +02:00
parent 0a4b337115
commit b3a8ae898e
32 changed files with 4157 additions and 177 deletions
+573 -91
View File
@@ -24,9 +24,21 @@
#include "../qetgraphicsitem/element.h"
#include "../undocommand/linkelementcommand.h"
#include "ui_masterpropertieswidget.h"
#include "../properties/elementdata.h"
#include <QListWidgetItem>
#include <QMessageBox>
#include <QTableWidget>
#include <QSpinBox>
#include <QCheckBox>
#include <QComboBox>
#include <QHeaderView>
#include <QMenu>
#include <QAction>
#include <QClipboard>
#include <QApplication>
#include <QShortcut>
#include <QPushButton>
/**
@@ -260,98 +272,7 @@ bool MasterPropertiesWidget::setLiveEdit(bool live_edit)
return true;
}
/**
* @brief MasterPropertiesWidget::updateUi
* Build the interface of the widget
*/
void MasterPropertiesWidget::updateUi()
{
ui->m_free_tree_widget->clear();
ui->m_link_tree_widget->clear();
m_qtwi_hash.clear();
if (Q_UNLIKELY(!m_project))
return;
ElementProvider elmt_prov(m_project);
QSettings settings;
//Build the list of free available element
QList <QTreeWidgetItem *> items_list;
for(const auto &elmt : elmt_prov.freeElement(ElementData::Slave))
{
QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_free_tree_widget);
qtwi->setIcon(0, elmt->pixmap());
if(settings.value("genericpanel/folio", false).toBool())
{
autonum::sequentialNumbers seq;
QString F =autonum::AssignVariables::formulaToLabel(
elmt->diagram()->border_and_titleblock.folio(),
seq,
elmt->diagram(),
elmt);
qtwi->setText(1, F);
}
else
{
qtwi->setText(1, QString::number(
elmt->diagram()->folioIndex()
+ 1));
}
qtwi->setText(2, elmt->diagram()->title());
qtwi->setText(4, elmt->diagram()->convertPosition(
elmt->scenePos()).toString());
items_list.append(qtwi);
m_qtwi_hash.insert(qtwi, elmt);
}
ui->m_free_tree_widget->addTopLevelItems(items_list);
items_list.clear();
//Build the list of already linked element
const QList<Element *> link_list = m_element->linkedElements();
for(Element *elmt : link_list)
{
QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_link_tree_widget);
qtwi->setIcon(0, elmt->pixmap());
if(settings.value("genericpanel/folio", false).toBool())
{
autonum::sequentialNumbers seq;
QString F =autonum::AssignVariables::formulaToLabel(
elmt->diagram()->border_and_titleblock.folio(),
seq,
elmt->diagram(),
elmt);
qtwi->setText(1, F);
}
else
{
qtwi->setText(1, QString::number(
elmt->diagram()->folioIndex()
+ 1));
}
qtwi->setText(2, elmt->diagram()->title());
qtwi->setText(3, elmt->diagram()->convertPosition(
elmt->scenePos()).toString());
items_list.append(qtwi);
m_qtwi_hash.insert(qtwi, elmt);
}
if(items_list.count())
ui->m_link_tree_widget->addTopLevelItems(items_list);
QVariant v = settings.value("link-element-widget/master-state");
if(!v.isNull())
{
ui->m_free_tree_widget->header()->restoreState(v.toByteArray());
ui->m_link_tree_widget->header()->restoreState(v.toByteArray());
}
}
/**
* @brief MasterPropertiesWidget::headerCustomContextMenuRequested
@@ -548,3 +469,564 @@ void MasterPropertiesWidget::customContextMenu(const QPoint &pos, int i)
m_context_menu->addAction(m_show_element);
m_context_menu->popup(point);
}
/**
* @brief MasterPropertiesWidget::updateUi
* Build the interface of the widget
*/
void MasterPropertiesWidget::updateUi()
{
ui->m_free_tree_widget->clear();
ui->m_link_tree_widget->clear();
m_qtwi_hash.clear();
// Check if this is a PLC master
bool is_plc = m_element &&
m_element->elementData().m_type == ElementData::Master &&
m_element->elementData().m_master_type == ElementData::PLC;
// Show/hide normal master UI and PLC UI
ui->m_free_tree_widget->setVisible(!is_plc);
ui->m_link_tree_widget->setVisible(!is_plc);
ui->label->setVisible(!is_plc);
ui->label_2->setVisible(!is_plc);
ui->link_button->setVisible(!is_plc);
ui->unlink_button->setVisible(!is_plc);
// In PLC mode, make hidden widgets take no space in the grid layout
if (is_plc) {
for (QWidget *w : {static_cast<QWidget*>(ui->m_free_tree_widget),
static_cast<QWidget*>(ui->m_link_tree_widget),
static_cast<QWidget*>(ui->label),
static_cast<QWidget*>(ui->label_2),
static_cast<QWidget*>(ui->link_button),
static_cast<QWidget*>(ui->unlink_button)}) {
w->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
w->setMinimumSize(0, 0);
w->setMaximumSize(0, 0);
}
for (int col = 0; col < ui->gridLayout->columnCount(); ++col)
ui->gridLayout->setColumnStretch(col, col < 2 ? 1 : 0);
}
if (is_plc) {
// Create PLC widget if not yet created
if (!m_plc_widget) {
m_plc_widget = new QWidget(ui->gridLayout->parentWidget());
auto *plc_layout = new QVBoxLayout(m_plc_widget);
plc_layout->setContentsMargins(0, 0, 0, 0);
// Table
m_plc_table = new QTableWidget(m_plc_widget);
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::Stretch);
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectRows);
m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_plc_table->setMinimumHeight(200);
plc_layout->addWidget(m_plc_table);
connect(m_plc_table, &QTableWidget::cellChanged, this, &MasterPropertiesWidget::plcIOCellChanged);
// Context menu for the PLC table
m_plc_table->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_plc_table, &QTableWidget::customContextMenuRequested,
this, &MasterPropertiesWidget::plcShowTableContextMenu);
// Ctrl+V shortcut for paste
auto *paste_shortcut = new QShortcut(QKeySequence::Paste, m_plc_table);
connect(paste_shortcut, &QShortcut::activated, this, &MasterPropertiesWidget::plcPasteFromClipboard);
}
ui->gridLayout->addWidget(m_plc_widget, 0, 0, 1, 5);
m_plc_widget->setVisible(true);
// Load PLC data into table (block signals to prevent cellChanged during population)
m_plc_updating = true;
m_plc_table->blockSignals(true);
// Clear existing rows
m_plc_table->setRowCount(0);
ElementData::PlcMasterData plc_data = m_element->elementData().plcMasterData();
m_plc_table->setRowCount(plc_data.ios.size());
for (int row = 0; row < plc_data.ios.size(); ++row) {
const ElementData::PlcIO &io = plc_data.ios.at(row);
// Type combo (read-only — type is defined in the editor)
auto *type_cb = new QComboBox(m_plc_table);
type_cb->setEnabled(false);
type_cb->setStyleSheet("QComboBox { background-color: #f0f0f0; }");
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<int>(io.type));
m_plc_table->setCellWidget(row, 0, type_cb);
// Address
auto *addr_item = new QTableWidgetItem(io.address);
m_plc_table->setItem(row, 1, addr_item);
// Function text
auto *func_item = new QTableWidgetItem(io.functionText);
m_plc_table->setItem(row, 2, func_item);
// Comment
auto *comment_item = new QTableWidgetItem(io.comment);
m_plc_table->setItem(row, 3, comment_item);
// CrossRef (read-only)
auto *crossref_item = new QTableWidgetItem(io.crossRef);
crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable);
m_plc_table->setItem(row, 4, crossref_item);
}
m_plc_table->blockSignals(false);
m_plc_updating = false;
} else {
// Hide PLC widget if it was shown before
if (m_plc_widget)
m_plc_widget->setVisible(false);
// Restore normal widget size policies
for (QWidget *w : {static_cast<QWidget*>(ui->m_free_tree_widget),
static_cast<QWidget*>(ui->m_link_tree_widget),
static_cast<QWidget*>(ui->label),
static_cast<QWidget*>(ui->label_2),
static_cast<QWidget*>(ui->link_button),
static_cast<QWidget*>(ui->unlink_button)}) {
w->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
w->setMinimumSize(0, 0);
w->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
}
for (int col = 0; col < ui->gridLayout->columnCount(); ++col)
ui->gridLayout->setColumnStretch(col, 0);
if (Q_UNLIKELY(!m_project))
return;
ElementProvider elmt_prov(m_project);
QSettings settings;
//Build the list of free available element
QList <QTreeWidgetItem *> items_list;
for(const auto &elmt : elmt_prov.freeElement(ElementData::Slave))
{
// Filter out PLC-Slave elements — they should only link to PLC-Master
if (elmt->elementData().m_slave_type == ElementData::PLCSlave)
continue;
QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_free_tree_widget);
qtwi->setIcon(0, elmt->pixmap());
if(settings.value("genericpanel/folio", false).toBool())
{
autonum::sequentialNumbers seq;
QString F =autonum::AssignVariables::formulaToLabel(
elmt->diagram()->border_and_titleblock.folio(),
seq,
elmt->diagram(),
elmt);
qtwi->setText(1, F);
}
else
{
qtwi->setText(1, QString::number(
elmt->diagram()->folioIndex()
+ 1));
}
qtwi->setText(2, elmt->diagram()->title());
qtwi->setText(4, elmt->diagram()->convertPosition(
elmt->scenePos()).toString());
items_list.append(qtwi);
m_qtwi_hash.insert(qtwi, elmt);
}
ui->m_free_tree_widget->addTopLevelItems(items_list);
items_list.clear();
//Build the list of already linked element
const QList<Element *> link_list = m_element->linkedElements();
for(Element *elmt : link_list)
{
QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_link_tree_widget);
qtwi->setIcon(0, elmt->pixmap());
if(settings.value("genericpanel/folio", false).toBool())
{
autonum::sequentialNumbers seq;
QString F =autonum::AssignVariables::formulaToLabel(
elmt->diagram()->border_and_titleblock.folio(),
seq,
elmt->diagram(),
elmt);
qtwi->setText(1, F);
}
else
{
qtwi->setText(1, QString::number(
elmt->diagram()->folioIndex()
+ 1));
}
qtwi->setText(2, elmt->diagram()->title());
qtwi->setText(3, elmt->diagram()->convertPosition(
elmt->scenePos()).toString());
items_list.append(qtwi);
m_qtwi_hash.insert(qtwi, elmt);
}
if(items_list.count())
ui->m_link_tree_widget->addTopLevelItems(items_list);
QVariant v = settings.value("link-element-widget/master-state");
if(!v.isNull())
{
ui->m_free_tree_widget->header()->restoreState(v.toByteArray());
ui->m_link_tree_widget->header()->restoreState(v.toByteArray());
}
}
}
// ============================================================================
// PLC IO Table methods
// ============================================================================
/**
* @brief MasterPropertiesWidget::plcPasteFromClipboard
* Paste IO data from clipboard.
*
* Two modes:
* 1. Single-column data (no tabs, e.g. copied vertically from A1:A6):
* Values are pasted vertically down the current column.
* 2. Multi-column data (tab-separated, e.g. copied from a row in Excel):
* Each line becomes a separate IO row.
*/
void MasterPropertiesWidget::plcPasteFromClipboard()
{
if (!m_plc_table || !m_element)
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;
}
}
m_plc_updating = true;
if (!has_tabs) {
// Vertical paste: values go down the same column, within existing rows only
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;
setCellFromValue(row, target_col, lines.at(i).trimmed());
}
} else {
// Horizontal paste: each line is a separate IO row, within existing rows only
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 > 6) break;
setCellFromValue(row, c, cells.at(c).trimmed());
}
}
}
m_plc_updating = false;
plcUpdateDisplaySettings();
}
/**
* @brief MasterPropertiesWidget::setCellFromValue
* Set a single table cell value, respecting the column widget type.
*/
void MasterPropertiesWidget::setCellFromValue(int row, int col, const QString &val)
{
if (!m_plc_table || row < 0 || col < 0 || col > 6)
return;
if (col == 0) {
// Type combo
auto *type_cb = qobject_cast<QComboBox*>(m_plc_table->cellWidget(row, col));
if (!type_cb) {
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, col, type_cb);
connect(type_cb, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, [this, row](int) { plcIOCellChanged(row, 0); });
}
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 == 5) {
// Terminal count spinbox
auto *tc_sb = qobject_cast<QSpinBox*>(m_plc_table->cellWidget(row, col));
if (!tc_sb) {
tc_sb = new QSpinBox(m_plc_table);
tc_sb->setMinimum(1);
tc_sb->setMaximum(4);
m_plc_table->setCellWidget(row, col, tc_sb);
connect(tc_sb, QOverload<int>::of(&QSpinBox::valueChanged),
this, [this, row](int) { plcIOCellChanged(row, 5); });
}
bool ok;
int v = val.toInt(&ok);
if (ok && v >= 1 && v <= 4)
tc_sb->setValue(v);
}
else if (col == 6) {
// 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 MasterPropertiesWidget::plcAddRow
* Add a new empty row to the PLC IO table
*/
void MasterPropertiesWidget::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);
connect(type_cb, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, [this, row](int) { plcIOCellChanged(row, 0); });
m_plc_table->setItem(row, 1, new QTableWidgetItem());
m_plc_table->setItem(row, 2, new QTableWidgetItem());
m_plc_table->setItem(row, 3, new QTableWidgetItem());
m_plc_table->setItem(row, 4, new QTableWidgetItem());
auto *tc_sb = new QSpinBox(m_plc_table);
tc_sb->setMinimum(1);
tc_sb->setMaximum(4);
tc_sb->setValue(1);
m_plc_table->setCellWidget(row, 5, tc_sb);
connect(tc_sb, QOverload<int>::of(&QSpinBox::valueChanged),
this, [this, row](int) { plcIOCellChanged(row, 5); });
auto *crossref_item = new QTableWidgetItem();
crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable);
m_plc_table->setItem(row, 6, crossref_item);
}
/**
* @brief MasterPropertiesWidget::plcRemoveRow
* Remove selected rows from the PLC IO table
*/
void MasterPropertiesWidget::plcRemoveRow()
{
if (!m_plc_table)
return;
QModelIndexList selected = m_plc_table->selectionModel()->selectedRows();
if (selected.isEmpty())
return;
// Remove from bottom to top to preserve indices
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());
}
}
/**
* @brief MasterPropertiesWidget::plcMoveRowUp
* Move selected row up
*/
void MasterPropertiesWidget::plcMoveRowUp()
{
if (!m_plc_table)
return;
int row = m_plc_table->currentRow();
if (row <= 0)
return;
// Swap with row above
for (int col = 0; col < m_plc_table->columnCount(); ++col) {
QWidget *w1 = m_plc_table->cellWidget(row, col);
QWidget *w2 = m_plc_table->cellWidget(row - 1, col);
m_plc_table->setCellWidget(row, col, w2);
m_plc_table->setCellWidget(row - 1, col, w1);
QTableWidgetItem *i1 = m_plc_table->item(row, col);
QTableWidgetItem *i2 = m_plc_table->item(row - 1, col);
m_plc_table->setItem(row, col, i2);
m_plc_table->setItem(row - 1, col, i1);
}
m_plc_table->setCurrentCell(row - 1, m_plc_table->currentColumn());
}
/**
* @brief MasterPropertiesWidget::plcMoveRowDown
* Move selected row down
*/
void MasterPropertiesWidget::plcMoveRowDown()
{
if (!m_plc_table)
return;
int row = m_plc_table->currentRow();
if (row < 0 || row >= m_plc_table->rowCount() - 1)
return;
// Swap with row below
for (int col = 0; col < m_plc_table->columnCount(); ++col) {
QWidget *w1 = m_plc_table->cellWidget(row, col);
QWidget *w2 = m_plc_table->cellWidget(row + 1, col);
m_plc_table->setCellWidget(row, col, w2);
m_plc_table->setCellWidget(row + 1, col, w1);
QTableWidgetItem *i1 = m_plc_table->item(row, col);
QTableWidgetItem *i2 = m_plc_table->item(row + 1, col);
m_plc_table->setItem(row, col, i2);
m_plc_table->setItem(row + 1, col, i1);
}
m_plc_table->setCurrentCell(row + 1, m_plc_table->currentColumn());
}
/**
* @brief MasterPropertiesWidget::plcIOCellChanged
* Called when a cell in the PLC IO table changes
*/
void MasterPropertiesWidget::plcIOCellChanged(int row, int column)
{
Q_UNUSED(row)
Q_UNUSED(column)
if (m_plc_updating || !m_element || !m_plc_table)
return;
// Save immediately
plcUpdateDisplaySettings();
}
/**
* @brief MasterPropertiesWidget::plcUpdateDisplaySettings
* Update the IO data from the table (display settings are managed by the editor only)
*/
void MasterPropertiesWidget::plcUpdateDisplaySettings()
{
if (m_plc_updating || !m_element || !m_plc_table)
return;
m_plc_updating = true;
// Preserve existing display settings
ElementData ed = m_element->elementData();
ElementData::PlcMasterData plc_data = ed.plcMasterData();
plc_data.ios.clear();
// Read IOs from table
for (int row = 0; row < m_plc_table->rowCount(); ++row) {
ElementData::PlcIO io;
auto *type_cb = qobject_cast<QComboBox *>(m_plc_table->cellWidget(row, 0));
if (type_cb)
io.type = static_cast<ElementData::PlcIOType>(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();
plc_data.ios.append(io);
}
ed.setPlcMasterData(plc_data);
m_element->setElementData(ed);
// Trigger update of the cross ref item
if (m_element->scene())
m_element->update();
m_plc_updating = false;
}
/**
* @brief MasterPropertiesWidget::plcShowTableContextMenu
* Show context menu for the PLC IO table
*/
void MasterPropertiesWidget::plcShowTableContextMenu(const QPoint &pos)
{
Q_UNUSED(pos)
if (!m_plc_table)
return;
QMenu menu;
menu.addAction(tr("Coller depuis le presse-papiers"), this, &MasterPropertiesWidget::plcPasteFromClipboard);
menu.exec(m_plc_table->mapToGlobal(pos));
}