Merge pull request #562 from Kellermorph/PLC-Manager

PLC Manager
This commit is contained in:
Laurent Trinques
2026-07-31 09:05:30 +02:00
committed by GitHub
32 changed files with 4163 additions and 177 deletions
+5 -1
View File
@@ -28,6 +28,7 @@
#include "elementinfowidget.h"
#include "linksingleelementwidget.h"
#include "masterpropertieswidget.h"
#include "plclinkwidget.h"
#include <QLabel>
#include <QUndoStack>
@@ -303,7 +304,10 @@ void ElementPropertiesWidget::updateUi()
m_list_editor << new ElementInfoWidget(m_element, this);
break;
case Element::Slave:
m_list_editor << new LinkSingleElementWidget(m_element, this);
if (m_element->elementData().m_slave_type == ElementData::PLCSlave)
m_list_editor << new PlcLinkWidget(m_element, this);
else
m_list_editor << new LinkSingleElementWidget(m_element, this);
break;
case Element::Terminale:
m_list_editor << new ElementInfoWidget(m_element, this);
+78 -22
View File
@@ -29,6 +29,7 @@
#include "../ui_linksingleelementwidget.h"
#include <QTreeWidgetItem>
#include <QInputDialog>
/**
@@ -393,17 +394,24 @@ QVector <QPointer<Element>> LinkSingleElementWidget::availableElements()
//If element is linked, remove is parent from the list
if(!m_element->isFree()) elmt_vector.removeAll(m_element->linkedElements().first());
// Filter out all master elements from the list
// Filter out incompatible elements: PLC and non-PLC must not mix
const bool element_is_plc = (m_element->elementData().m_type == ElementData::Slave &&
m_element->elementData().m_slave_type == ElementData::PLCSlave);
for (int i = elmt_vector.size() - 1; i >= 0; --i) {
Element *elmt = elmt_vector.at(i);
// If the item in the list is a master
if (elmt->linkType() == Element::Master) {
const bool master_is_plc = (elmt->elementData().m_master_type == ElementData::PLC);
// We convert the generic element pointer into a MasterElement pointer
MasterElement *master = static_cast<MasterElement*>(elmt);
// PLC slave can only link to PLC master, and vice versa
if (element_is_plc != master_is_plc) {
elmt_vector.removeAt(i);
continue;
}
// If the master is full, we'll remove it from the list!
MasterElement *master = static_cast<MasterElement*>(elmt);
if (master->isFull()) {
elmt_vector.removeAt(i);
}
@@ -536,7 +544,7 @@ void LinkSingleElementWidget::linkTriggered()
{
if(!m_qtwi_at_context_menu)
return;
m_element_to_link = m_qtwi_elmt_hash.value(m_qtwi_at_context_menu);
m_pending_group_index = -1;
@@ -545,25 +553,73 @@ void LinkSingleElementWidget::linkTriggered()
&& m_element_to_link
&& m_element_to_link->linkType() == Element::Master)
{
const auto &groups = m_element_to_link->elementData().m_slave_contact_groups;
if (!groups.isEmpty())
// Check if this is a PLC master
if (m_element_to_link->elementData().m_master_type == ElementData::PLC)
{
// Collect already-used group indices from the master
QSet<int> used_indices;
for (Element *linked : m_element_to_link->linkedElements()) {
int idx = m_element_to_link->groupIndexForElement(linked);
if (idx >= 0) {
used_indices.insert(idx);
// Show PLC IO selection dialog
const auto &plc_data = m_element_to_link->elementData().plcMasterData();
if (!plc_data.ios.isEmpty())
{
// Collect already-used IO indices from the master
QSet<int> used_indices;
for (Element *linked : m_element_to_link->linkedElements()) {
int idx = m_element_to_link->groupIndexForElement(linked);
if (idx >= 0) {
used_indices.insert(idx);
}
}
// Build selection dialog
QStringList items;
for (int i = 0; i < plc_data.ios.size(); ++i) {
const auto &io = plc_data.ios.at(i);
QString label = QString("[%1] %2 - %3")
.arg(i + 1)
.arg(io.address)
.arg(io.functionText);
if (used_indices.contains(i))
label += tr(" (déjà utilisé)");
items << label;
}
bool ok = false;
int selected = QInputDialog::getInt(
this,
tr("Sélectionner un IO PLC"),
tr("IO disponible:"),
0, 0, plc_data.ios.size() - 1, 1, &ok);
if (ok && selected >= 0) {
m_pending_group_index = selected;
} else {
m_element_to_link = nullptr;
return;
}
}
}
else
{
// Normal contact group selection for non-PLC masters
const auto &groups = m_element_to_link->elementData().m_slave_contact_groups;
if (!groups.isEmpty())
{
// Collect already-used group indices from the master
QSet<int> used_indices;
for (Element *linked : m_element_to_link->linkedElements()) {
int idx = m_element_to_link->groupIndexForElement(linked);
if (idx >= 0) {
used_indices.insert(idx);
}
}
ContactGroupSelectionDialog dlg(groups, used_indices,
m_element->elementData(), this);
if (dlg.exec() == QDialog::Accepted && dlg.selectedIndex() >= 0) {
m_pending_group_index = dlg.selectedIndex();
} else {
m_element_to_link = nullptr;
return;
ContactGroupSelectionDialog dlg(groups, used_indices,
m_element->elementData(), this);
if (dlg.exec() == QDialog::Accepted && dlg.selectedIndex() >= 0) {
m_pending_group_index = dlg.selectedIndex();
} else {
m_element_to_link = nullptr;
return;
}
}
}
}
@@ -588,7 +644,7 @@ void LinkSingleElementWidget::linkTriggered()
Qt::NoBrush));
}
}
for (int i=0 ; i<6 ; i++)
{
m_qtwi_at_context_menu->setBackground(i,
@@ -597,7 +653,7 @@ void LinkSingleElementWidget::linkTriggered()
}
m_pending_qtwi = m_qtwi_at_context_menu;
}
}
/**
+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));
}
+25
View File
@@ -31,6 +31,10 @@ class Diagram;
class QTreeWidgetItem;
class QMenu;
class QAction;
class QTableWidget;
class QSpinBox;
class QCheckBox;
class QComboBox;
namespace Ui {
class MasterPropertiesWidget;
@@ -75,6 +79,17 @@ class MasterPropertiesWidget : public AbstractElementPropertiesEditorWidget
void diagramWasdeletedFromProject();
void customContextMenu(const QPoint &pos, int i=0);
// PLC IO table slots
void plcPasteFromClipboard();
void setCellFromValue(int row, int col, const QString &val);
void plcAddRow();
void plcRemoveRow();
void plcMoveRowUp();
void plcMoveRowDown();
void plcIOCellChanged(int row, int column);
void plcUpdateDisplaySettings();
void plcShowTableContextMenu(const QPoint &pos);
private:
Ui::MasterPropertiesWidget *ui;
QHash <QTreeWidgetItem *, Element *> m_qtwi_hash;
@@ -88,6 +103,16 @@ class MasterPropertiesWidget : public AbstractElementPropertiesEditorWidget
*m_show_qtwi,
*m_show_element,
*m_save_header_state;
// PLC-specific members
QWidget *m_plc_widget = nullptr;
QTableWidget *m_plc_table = nullptr;
QCheckBox *m_plc_break_checkboxes[4] = {nullptr, nullptr, nullptr, nullptr};
QSpinBox *m_plc_break_spinboxes[4] = {nullptr, nullptr, nullptr, nullptr};
QSpinBox *m_plc_row_height_spinbox = nullptr;
QList<QCheckBox *> m_plc_col_visibility_checkboxes;
QList<QSpinBox *> m_plc_col_width_spinboxes;
bool m_plc_updating = false; // Guard against recursive updates
};
#endif // MASTERPROPERTIESWIDGET_H
+371
View File
@@ -0,0 +1,371 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#include "plclinkwidget.h"
#include "../qetgraphicsitem/masterelement.h"
#include "../qetgraphicsitem/element.h"
#include "../elementprovider.h"
#include "../undocommand/linkelementcommand.h"
#include "../diagram.h"
#include "../qetproject.h"
#include "../properties/elementdata.h"
#include <QTreeWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QLabel>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QMenu>
#include <QAction>
#include <QFont>
#include <QTimer>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QMenu>
#include <QAction>
PlcLinkWidget::PlcLinkWidget(Element *elmt, QWidget *parent)
: AbstractElementPropertiesEditorWidget(parent)
{
auto *main_layout = new QGridLayout(this);
// Row 0: Status label + buttons (shown when already linked)
m_label = new QLabel(tr("Cet élément est déjà lié"), this);
m_unlink_pb = new QPushButton(tr("Délier"), this);
m_show_this_pb = new QPushButton(tr("Voir cet élément"), this);
main_layout->addWidget(m_label, 0, 0);
main_layout->addWidget(m_unlink_pb, 0, 1);
main_layout->addWidget(m_show_this_pb, 0, 2);
// Row 1: Search field
m_search_field = new QLineEdit(this);
m_search_field->setPlaceholderText(tr("Recherche"));
main_layout->addWidget(m_search_field, 1, 0, 1, 3);
// Row 2: Tree widget
m_tree_widget = new QTreeWidget(this);
m_tree_widget->setHeaderLabels({
tr("Label"), tr("Type"), tr("Adresse"),
tr("Fonction"), tr("Commentaire"), tr("Anschlüsse")
});
m_tree_widget->setRootIsDecorated(true);
m_tree_widget->setIndentation(20);
m_tree_widget->setSelectionMode(QAbstractItemView::SingleSelection);
m_tree_widget->setContextMenuPolicy(Qt::CustomContextMenu);
m_tree_widget->header()->setStretchLastSection(false);
m_tree_widget->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
m_tree_widget->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
m_tree_widget->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
m_tree_widget->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
m_tree_widget->header()->setSectionResizeMode(4, QHeaderView::ResizeToContents);
m_tree_widget->header()->setSectionResizeMode(5, QHeaderView::ResizeToContents);
main_layout->addWidget(m_tree_widget, 2, 0, 1, 3);
// Row 3: Hidden masters note
m_hidden_masters_label = new QLabel(
tr("Remarque : les éléments maîtres ayant atteint leur nombre maximal "
"d'esclaves sont masqués."), this);
m_hidden_masters_label->setWordWrap(true);
QFont italic_font = m_hidden_masters_label->font();
italic_font.setItalic(true);
m_hidden_masters_label->setFont(italic_font);
m_hidden_masters_label->hide();
main_layout->addWidget(m_hidden_masters_label, 3, 0, 1, 3);
main_layout->setRowStretch(2, 1);
setMinimumWidth(500);
// Connections
connect(m_unlink_pb, &QPushButton::clicked,
this, &PlcLinkWidget::on_m_unlink_pb_clicked);
connect(m_show_this_pb, &QPushButton::clicked,
this, &PlcLinkWidget::on_m_show_this_pb_clicked);
connect(m_search_field, &QLineEdit::textEdited,
this, &PlcLinkWidget::on_m_search_field_textEdited);
connect(m_tree_widget, &QTreeWidget::customContextMenuRequested,
this, &PlcLinkWidget::on_m_tree_widget_customContextMenuRequested);
if (elmt)
setElement(elmt);
}
void PlcLinkWidget::setElement(Element *element)
{
if (m_element == element)
return;
m_element = element;
updateUi();
}
void PlcLinkWidget::apply()
{
QUndoCommand *undo = associatedUndo();
if (undo)
m_element->diagram()->undoStack().push(undo);
m_element_to_link = nullptr;
m_pending_io_index = -1;
}
QUndoCommand *PlcLinkWidget::associatedUndo() const
{
if (!m_element_to_link || m_pending_io_index < 0)
return nullptr;
LinkElementCommand *undo = new LinkElementCommand(m_element);
undo->setLink(m_element_to_link);
undo->setGroupIndex(m_pending_io_index);
return undo;
}
QString PlcLinkWidget::title() const
{
return tr("Automate (PLC)");
}
void PlcLinkWidget::updateUi()
{
if (m_element->isFree())
hideButtons();
else
showButtons();
buildPlcTree();
}
/**
@brief PlcLinkWidget::buildPlcTree
Build the tree showing all PLC-Masters with their IO entries.
IO entries already linked to a slave are shown as struck-through and greyed out.
*/
void PlcLinkWidget::buildPlcTree()
{
m_tree_widget->clear();
m_io_entry_hash.clear();
if (!m_element || !m_element->diagram() || !m_element->diagram()->project())
return;
ElementProvider ep(m_element->diagram()->project());
QVector<QPointer<Element>> masters = ep.find(ElementData::Master);
QSet<int> used_io_indices;
for (Element *elmt : masters) {
if (!elmt || elmt->elementData().m_master_type != ElementData::PLC)
continue;
const auto &plc_data = elmt->elementData().plcMasterData();
if (plc_data.ios.isEmpty())
continue;
// Skip full masters
MasterElement *me = static_cast<MasterElement*>(elmt);
if (me->isFull()) {
m_hidden_masters_label->show();
continue;
}
// Collect used IO indices for this master
used_io_indices.clear();
for (Element *linked : elmt->linkedElements()) {
int idx = elmt->groupIndexForElement(linked);
if (idx >= 0)
used_io_indices.insert(idx);
}
// Create parent item for this PLC-Master
auto *parent_item = new QTreeWidgetItem(m_tree_widget);
// Show master label + diagram info
QString master_label = elmt->actualLabel();
if (master_label.isEmpty())
master_label = elmt->name();
QString folio_info;
if (elmt->diagram()) {
folio_info = QString::number(elmt->diagram()->folioIndex() + 1);
}
parent_item->setText(0, QString("%1 (%2)")
.arg(master_label)
.arg(folio_info));
QFont bold_font = parent_item->font(0);
bold_font.setBold(true);
parent_item->setFont(0, bold_font);
parent_item->setExpanded(false);
// Add child items for each IO entry
for (int i = 0; i < plc_data.ios.size(); ++i) {
const auto &io = plc_data.ios.at(i);
auto *child_item = new QTreeWidgetItem(parent_item);
child_item->setText(1, ElementData::translatedPlcIOType(io.type));
child_item->setText(2, io.address);
child_item->setText(3, io.functionText);
child_item->setText(4, io.comment);
// Terminal count
child_item->setText(5, QString::number(io.terminalCount));
PlcIoEntry entry;
entry.master = elmt;
entry.ioIndex = i;
m_io_entry_hash.insert(child_item, entry);
// If this IO is already linked to a slave, grey it out and strike through
if (used_io_indices.contains(i)) {
QFont strike_font = child_item->font(0);
strike_font.setStrikeOut(true);
child_item->setFont(0, strike_font);
child_item->setFont(1, strike_font);
child_item->setFont(2, strike_font);
child_item->setFont(3, strike_font);
child_item->setFont(4, strike_font);
child_item->setFont(5, strike_font);
QBrush grey_brush(Qt::gray);
for (int col = 0; col < 6; ++col)
child_item->setForeground(col, grey_brush);
// Show which slave is linked
for (Element *linked : elmt->linkedElements()) {
if (elmt->groupIndexForElement(linked) == i) {
child_item->setToolTip(0,
tr("Lié à: %1").arg(linked->actualLabel()));
break;
}
}
child_item->setFlags(child_item->flags() & ~Qt::ItemIsSelectable);
}
}
}
}
void PlcLinkWidget::hideButtons()
{
m_label->hide();
m_unlink_pb->hide();
m_show_this_pb->hide();
m_search_field->show();
}
void PlcLinkWidget::showButtons()
{
m_label->show();
m_unlink_pb->show();
m_show_this_pb->show();
m_search_field->hide();
}
void PlcLinkWidget::on_m_search_field_textEdited(const QString &text)
{
for (int i = 0; i < m_tree_widget->topLevelItemCount(); ++i) {
QTreeWidgetItem *parent = m_tree_widget->topLevelItem(i);
bool any_child_visible = false;
for (int j = 0; j < parent->childCount(); ++j) {
QTreeWidgetItem *child = parent->child(j);
bool match = text.isEmpty();
if (!match) {
for (int col = 0; col < child->columnCount(); ++col) {
if (child->text(col).contains(text, Qt::CaseInsensitive)) {
match = true;
break;
}
}
}
child->setHidden(!match);
if (match) any_child_visible = true;
}
// Also check if parent label matches
if (!text.isEmpty() && parent->text(0).contains(text, Qt::CaseInsensitive)) {
any_child_visible = true;
for (int j = 0; j < parent->childCount(); ++j)
parent->child(j)->setHidden(false);
}
parent->setHidden(!any_child_visible);
}
}
void PlcLinkWidget::on_m_tree_widget_customContextMenuRequested(const QPoint &pos)
{
QTreeWidgetItem *item = m_tree_widget->itemAt(pos);
if (!item)
return;
// Only show context menu for IO child items (not parent master items)
if (!m_io_entry_hash.contains(item))
return;
const PlcIoEntry &entry = m_io_entry_hash.value(item);
// Check if this IO is already linked
Element *master = entry.master;
if (!master)
return;
QSet<int> used_indices;
for (Element *linked : master->linkedElements()) {
int idx = master->groupIndexForElement(linked);
if (idx >= 0)
used_indices.insert(idx);
}
if (used_indices.contains(entry.ioIndex))
return; // Already linked, no actions
QMenu menu;
QAction *link_action = menu.addAction(tr("Connecter"));
QAction *selected_action = menu.exec(m_tree_widget->viewport()->mapToGlobal(pos));
if (selected_action == link_action) {
m_element_to_link = master;
m_pending_io_index = entry.ioIndex;
apply();
}
}
void PlcLinkWidget::on_m_unlink_pb_clicked()
{
if (!m_element || m_element->isFree())
return;
LinkElementCommand *undo = new LinkElementCommand(m_element);
undo->unlinkAll();
m_element->diagram()->undoStack().push(undo);
updateUi();
}
void PlcLinkWidget::on_m_show_this_pb_clicked()
{
if (!m_element || !m_element->diagram())
return;
m_element->diagram()->showMe();
m_element->setHighlighted(true);
}
+87
View File
@@ -0,0 +1,87 @@
/*
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 <http://www.gnu.org/licenses/>.
*/
#ifndef PLCLINKWIDGET_H
#define PLCLINKWIDGET_H
#include "abstractelementpropertieseditorwidget.h"
#include <QHash>
#include <QPointer>
#include <QSet>
class QTreeWidgetItem;
class QTreeWidget;
class QLineEdit;
class QPushButton;
class QLabel;
class Element;
/**
@brief The PlcLinkWidget class
Provides a dedicated widget for linking a PLC-Slave to a PLC-Master IO entry.
Displays all PLC-Masters as expandable items, each showing their IO entries.
Each IO entry can only be linked to one slave. Already-linked entries are
shown as struck-through and greyed out.
*/
class PlcLinkWidget : public AbstractElementPropertiesEditorWidget
{
Q_OBJECT
public:
explicit PlcLinkWidget(Element *elmt, QWidget *parent = nullptr);
~PlcLinkWidget() override = default;
void setElement(Element *element) override;
void apply() override;
QUndoCommand *associatedUndo() const override;
QString title() const override;
public slots:
void updateUi() override;
private:
void buildPlcTree();
void hideButtons();
void showButtons();
private slots:
void on_m_search_field_textEdited(const QString &text);
void on_m_tree_widget_customContextMenuRequested(const QPoint &pos);
void on_m_unlink_pb_clicked();
void on_m_show_this_pb_clicked();
private:
QLabel *m_label{nullptr};
QPushButton *m_unlink_pb{nullptr};
QPushButton *m_show_this_pb{nullptr};
QLineEdit *m_search_field{nullptr};
QTreeWidget *m_tree_widget{nullptr};
QLabel *m_hidden_masters_label{nullptr};
// Maps IO child items to (master, io_index)
struct PlcIoEntry {
QPointer<Element> master;
int ioIndex = -1;
};
QHash<QTreeWidgetItem*, PlcIoEntry> m_io_entry_hash;
Element *m_element_to_link = nullptr;
int m_pending_io_index = -1;
};
#endif // PLCLINKWIDGET_H
+1
View File
@@ -101,6 +101,7 @@ void XRefPropertiesWidget::buildUi()
ui -> m_type_cb -> addItem(tr("Bobine"), "coil");
ui -> m_type_cb -> addItem(tr("Organe de protection"), "protection");
ui -> m_type_cb -> addItem(tr("Commutateur / bouton"), "commutator");
ui -> m_type_cb -> addItem(tr("Automate (PLC)"), "plc");
ui -> m_snap_to_cb -> addItem(tr("En bas de page"), "bottom");
ui -> m_snap_to_cb -> addItem(tr("Sous le label de l'élément"), "label");