mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-26 03:44:14 +02:00
ffd829bb69
max_slaves records how many contacts a part is expected to carry. It was enforced as a rule the drawing had to obey, which obstructs the way both @scorpio810 and @IBSYSLevi described working in #819: draw the schematic first, choose the physical hardware afterwards. A limit that refuses the link forces the hardware decision up front, which is exactly what they said gets in the way. Two changes, both in the UI rather than in isFull(), which stays the query it always was: - MasterPropertiesWidget::on_link_button_clicked() now says the limit is reached and asks whether to link anyway, defaulting to yes, instead of refusing outright. - LinkSingleElementWidget no longer removes a full master from the candidate list. That was the worse half: a master at its limit simply was not there, indistinguishable from one that does not exist, with nothing to say why. It now stays selectable and the user decides. PLC masters are deliberately left alone. Their limit is the number of declared IO slots, which is structural rather than advisory -- a link past it would have no IO index to map to -- and PlcLinkWidget already tells the user when it hides one, via m_hidden_masters_label. Only coils that opt into a limit are affected: max_slaves defaults to -1, and no project in examples/ sets it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1054 lines
31 KiB
C++
1054 lines
31 KiB
C++
/*
|
|
* 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 "masterpropertieswidget.h"
|
|
#include "contactgroupselectiondialog.h"
|
|
#include "../qetproject.h"
|
|
#include "../diagram.h"
|
|
#include "../diagramposition.h"
|
|
#include "../elementprovider.h"
|
|
#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>
|
|
|
|
|
|
/**
|
|
* @brief MasterPropertiesWidget::MasterPropertiesWidget
|
|
* Default constructor
|
|
* @param elmt
|
|
* @param parent
|
|
*/
|
|
MasterPropertiesWidget::MasterPropertiesWidget(Element *elmt, QWidget *parent) :
|
|
AbstractElementPropertiesEditorWidget(parent),
|
|
ui(new Ui::MasterPropertiesWidget),
|
|
m_project(nullptr)
|
|
{
|
|
ui->setupUi(this);
|
|
|
|
ui->m_free_tree_widget->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
ui->m_link_tree_widget->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
|
|
QStringList list;
|
|
QSettings settings;
|
|
if (settings.value("genericpanel/folio", false).toBool()) {
|
|
list << tr("Vignette")
|
|
<< tr("Label de folio")
|
|
<< tr("Titre de folio")
|
|
<< tr("Position");
|
|
}
|
|
else {
|
|
list << tr("Vignette")
|
|
<< tr("N° de folio")
|
|
<< tr("Titre de folio")
|
|
<< tr("Position");
|
|
}
|
|
ui->m_free_tree_widget->setHeaderLabels(list);
|
|
ui->m_link_tree_widget->setHeaderLabels(list);
|
|
|
|
m_context_menu = new QMenu(this);
|
|
m_link_action = new QAction(tr("Lier l'élément"), this);
|
|
m_unlink_action = new QAction(tr("Délier l'élément"), this);
|
|
m_show_qtwi = new QAction(tr("Montrer l'élément"), this);
|
|
m_show_element = new QAction(tr("Montrer l'élément maître"), this);
|
|
m_save_header_state = new QAction(tr("Enregistrer la disposition"), this);
|
|
|
|
connect(ui->m_free_tree_widget, &QTreeWidget::itemDoubleClicked,
|
|
this, &MasterPropertiesWidget::showElementFromTWI);
|
|
connect(ui->m_link_tree_widget, &QTreeWidget::itemDoubleClicked,
|
|
this, &MasterPropertiesWidget::showElementFromTWI);
|
|
|
|
connect(ui->m_free_tree_widget, &QTreeWidget::customContextMenuRequested,
|
|
[this](QPoint point) {this->customContextMenu(point, 1);});
|
|
connect(ui->m_link_tree_widget, &QTreeWidget::customContextMenuRequested,
|
|
[this](QPoint point) {this->customContextMenu(point, 2);});
|
|
|
|
connect(m_link_action, &QAction::triggered,
|
|
this, &MasterPropertiesWidget::on_link_button_clicked);
|
|
connect(m_unlink_action, &QAction::triggered,
|
|
this, &MasterPropertiesWidget::on_unlink_button_clicked);
|
|
connect(m_show_qtwi, &QAction::triggered,
|
|
[this]() {this->showElementFromTWI(this->m_qtwi_at_context_menu,0);});
|
|
|
|
connect(m_show_element, &QAction::triggered, [this]()
|
|
{
|
|
this->m_element->diagram()->showMe();
|
|
this->m_element->setHighlighted(true);
|
|
if(this->m_showed_element)
|
|
m_showed_element->setHighlighted(false);
|
|
});
|
|
|
|
QHeaderView *qhv = ui->m_free_tree_widget->header();
|
|
qhv->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
connect(qhv, &QHeaderView::customContextMenuRequested,
|
|
this, &MasterPropertiesWidget::headerCustomContextMenuRequested);
|
|
connect(m_save_header_state, &QAction::triggered, [qhv]()
|
|
{
|
|
QByteArray qba = qhv->saveState();
|
|
QSettings settings;
|
|
settings.setValue("link-element-widget/master-state", qba);
|
|
});
|
|
|
|
setElement(elmt);
|
|
}
|
|
|
|
/**
|
|
* @brief MasterPropertiesWidget::~MasterPropertiesWidget
|
|
* Destructor
|
|
*/
|
|
MasterPropertiesWidget::~MasterPropertiesWidget()
|
|
{
|
|
if (m_showed_element)
|
|
m_showed_element->setHighlighted(false);
|
|
|
|
if(m_element)
|
|
m_element->setHighlighted(false);
|
|
|
|
delete ui;
|
|
}
|
|
|
|
/**
|
|
* @brief MasterPropertiesWidget::setElement
|
|
* Set the element to be edited
|
|
* @param element
|
|
*/
|
|
void MasterPropertiesWidget::setElement(Element *element)
|
|
{
|
|
if (m_element == element)
|
|
return;
|
|
|
|
if (m_showed_element)
|
|
{
|
|
m_showed_element->setHighlighted(false);
|
|
m_showed_element = nullptr;
|
|
}
|
|
if (m_element)
|
|
m_element->setHighlighted(false);
|
|
|
|
if (m_project)
|
|
disconnect(m_project, &QETProject::diagramRemoved, this, &MasterPropertiesWidget::diagramWasdeletedFromProject);
|
|
|
|
if(Q_LIKELY(element->diagram() && element->diagram()->project()))
|
|
{
|
|
m_project = element->diagram()->project();
|
|
connect(m_project, &QETProject::diagramRemoved, this, &MasterPropertiesWidget::diagramWasdeletedFromProject);
|
|
}
|
|
else
|
|
m_project = nullptr;
|
|
|
|
//Keep up to date this widget when the linked elements of m_element change
|
|
if (m_element)
|
|
disconnect(m_element.data(), &Element::linkedElementChanged,
|
|
this, &MasterPropertiesWidget::updateUi);
|
|
|
|
m_element = element;
|
|
connect(m_element.data(), &Element::linkedElementChanged,
|
|
this, &MasterPropertiesWidget::updateUi);
|
|
|
|
updateUi();
|
|
}
|
|
|
|
/**
|
|
* @brief MasterPropertiesWidget::apply
|
|
* If link between edited element and other change,
|
|
* apply the change with a QUndoCommand (got with method associatedUndo)
|
|
* pushed to the stack of element project.
|
|
* Return true if link change, else false
|
|
* @note is void no Return ???
|
|
*/
|
|
void MasterPropertiesWidget::apply()
|
|
{
|
|
if (QUndoCommand *undo = associatedUndo())
|
|
m_element -> diagram() -> undoStack().push(undo);
|
|
|
|
m_pending_group_indices.clear();
|
|
}
|
|
|
|
/**
|
|
* @brief MasterPropertiesWidget::reset
|
|
* Reset current widget, clear eveything and rebuild widget.
|
|
*/
|
|
void MasterPropertiesWidget::reset()
|
|
{
|
|
foreach (QTreeWidgetItem *qtwi, m_qtwi_hash.keys())
|
|
delete qtwi;
|
|
|
|
m_qtwi_hash.clear();
|
|
updateUi();
|
|
}
|
|
|
|
/**
|
|
* @brief MasterPropertiesWidget::associatedUndo
|
|
* If link between the edited element and other change,
|
|
* return a QUndoCommand with this change.
|
|
* If no change return nullptr.
|
|
* @return
|
|
*/
|
|
QUndoCommand* MasterPropertiesWidget::associatedUndo() const
|
|
{
|
|
// PLC masters manage their slave links via the IO table (setElementData),
|
|
// not via the link tree widget. The link tree is always empty for PLC
|
|
// masters, so we must not create an unlinkAll command.
|
|
if (m_element &&
|
|
m_element->elementData().m_type == ElementData::Master &&
|
|
m_element->elementData().m_master_type == ElementData::PLC)
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
QList <Element *> to_link;
|
|
QList <Element *> linked_ = m_element->linkedElements();
|
|
|
|
for (int i=0; i<ui->m_link_tree_widget->topLevelItemCount(); i++)
|
|
to_link << m_qtwi_hash[ui->m_link_tree_widget->topLevelItem(i)];
|
|
|
|
//The two list contain the same element, there is no change
|
|
if (to_link.size() == linked_.size())
|
|
{
|
|
bool equal = true;
|
|
|
|
foreach(Element *elmt, to_link)
|
|
if (!linked_.contains(elmt))
|
|
equal = false;
|
|
|
|
if(equal)
|
|
return nullptr;
|
|
}
|
|
|
|
LinkElementCommand *undo = new LinkElementCommand(m_element);
|
|
|
|
if (to_link.isEmpty())
|
|
undo->unlinkAll();
|
|
else
|
|
{
|
|
undo->setLink(to_link);
|
|
|
|
//Pass group indices for newly linked slaves
|
|
if (!m_pending_group_indices.isEmpty())
|
|
{
|
|
QMap<Element*, int> indices;
|
|
for (Element *slave : to_link)
|
|
{
|
|
if (m_pending_group_indices.contains(slave))
|
|
indices[slave] = m_pending_group_indices.value(slave);
|
|
}
|
|
if (!indices.isEmpty())
|
|
undo->setGroupIndices(indices);
|
|
}
|
|
}
|
|
|
|
return undo;
|
|
}
|
|
|
|
/**
|
|
* @brief MasterPropertiesWidget::setLiveEdit
|
|
* @param live_edit = true : live edit is enable
|
|
* else false : live edit is disable.
|
|
* @return always true because live edit is handled by this editor widget
|
|
*/
|
|
bool MasterPropertiesWidget::setLiveEdit(bool live_edit)
|
|
{
|
|
m_live_edit = live_edit;
|
|
return true;
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* @brief MasterPropertiesWidget::headerCustomContextMenuRequested
|
|
* @param pos
|
|
*/
|
|
void MasterPropertiesWidget::headerCustomContextMenuRequested(const QPoint &pos)
|
|
{
|
|
m_context_menu->clear();
|
|
m_context_menu->addAction(m_save_header_state);
|
|
m_context_menu->popup(ui->m_free_tree_widget->header()->mapToGlobal(pos));
|
|
}
|
|
|
|
/**
|
|
* @brief MasterPropertiesWidget::on_link_button_clicked
|
|
* Moves the current item from the free_list to the linked_list,
|
|
* provided the master's slave limit has not been reached.
|
|
*/
|
|
void MasterPropertiesWidget::on_link_button_clicked()
|
|
{
|
|
// Get the maximum number of allowed slaves from the element's information
|
|
QVariant max_slaves_variant = m_element->kindInformations().value("max_slaves");
|
|
|
|
if (max_slaves_variant.isValid() && !max_slaves_variant.toString().isEmpty()) {
|
|
int max_slaves = max_slaves_variant.toInt();
|
|
int current_slaves = ui->m_link_tree_widget->topLevelItemCount();
|
|
|
|
// If a limit is set and reached, say so but let the user decide.
|
|
// The limit records how many contacts the part is expected to
|
|
// carry; it is not a rule the drawing has to obey, and refusing
|
|
// the link obstructs drawing a schematic before the hardware has
|
|
// been chosen.
|
|
if (max_slaves != -1 && current_slaves >= max_slaves) {
|
|
|
|
// Show a message box with the actual window as parent to ensure it's on top
|
|
const auto answer = QMessageBox::warning(
|
|
this->window(),
|
|
tr("Nombre maximal d'esclaves atteint."),
|
|
tr("La limite fixée pour cet élément maître est atteinte (Limite: %1).\n\n"
|
|
"Voulez-vous tout de même lier ce contact esclave ?").arg(max_slaves),
|
|
QMessageBox::Yes | QMessageBox::No,
|
|
QMessageBox::Yes);
|
|
if (answer != QMessageBox::Yes) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Move current item from free_list to linked_list
|
|
QTreeWidgetItem *qtwi = ui->m_free_tree_widget->currentItem();
|
|
if (qtwi)
|
|
{
|
|
Element *slave_elmt = m_qtwi_hash.value(qtwi);
|
|
|
|
//If master has contact groups, show group selection dialog
|
|
const auto &groups = m_element->elementData().m_slave_contact_groups;
|
|
if (!groups.isEmpty() && slave_elmt)
|
|
{
|
|
// Collect already-used group indices from the master
|
|
QSet<int> used_indices;
|
|
for (Element *linked : m_element->linkedElements()) {
|
|
int idx = m_element->groupIndexForElement(linked);
|
|
if (idx >= 0) {
|
|
used_indices.insert(idx);
|
|
}
|
|
}
|
|
|
|
// Don't mark the current slave as used (it might be relinked)
|
|
if (slave_elmt->linkedElements().contains(m_element)) {
|
|
int current_idx = m_element->groupIndexForElement(slave_elmt);
|
|
if (current_idx >= 0) {
|
|
used_indices.remove(current_idx);
|
|
}
|
|
}
|
|
|
|
ContactGroupSelectionDialog dlg(groups, used_indices,
|
|
slave_elmt->elementData(), this);
|
|
if (dlg.exec() == QDialog::Accepted && dlg.selectedIndex() >= 0) {
|
|
m_pending_group_indices[slave_elmt] = dlg.selectedIndex();
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
ui->m_free_tree_widget->takeTopLevelItem(
|
|
ui->m_free_tree_widget->indexOfTopLevelItem(qtwi));
|
|
ui->m_link_tree_widget->insertTopLevelItem(0, qtwi);
|
|
|
|
if(m_live_edit)
|
|
apply();
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* @brief MasterPropertiesWidget::on_unlink_button_clicked
|
|
* move current item in linked_list to free_list
|
|
*/
|
|
void MasterPropertiesWidget::on_unlink_button_clicked()
|
|
{
|
|
//take the current item from linked_list and push it to free_list
|
|
QTreeWidgetItem *qtwi = ui->m_link_tree_widget->currentItem();
|
|
if(qtwi)
|
|
{
|
|
ui->m_link_tree_widget->takeTopLevelItem(
|
|
ui->m_link_tree_widget->indexOfTopLevelItem(qtwi));
|
|
ui->m_free_tree_widget->insertTopLevelItem(0, qtwi);
|
|
|
|
if(m_live_edit)
|
|
apply();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief MasterPropertiesWidget::showElementFromTWI
|
|
* Show the element corresponding to the given QTreeWidgetItem
|
|
* @param qtwi
|
|
* @param column
|
|
*/
|
|
void MasterPropertiesWidget::showElementFromTWI(QTreeWidgetItem *qtwi, int column)
|
|
{
|
|
Q_UNUSED(column);
|
|
if (m_showed_element)
|
|
{
|
|
disconnect(m_showed_element, &QObject::destroyed, this, &MasterPropertiesWidget::showedElementWasDeleted);
|
|
m_showed_element -> setHighlighted(false);
|
|
}
|
|
if (m_element)
|
|
m_element->setHighlighted(false);
|
|
|
|
m_showed_element = m_qtwi_hash[qtwi];
|
|
m_showed_element->diagram()->showMe();
|
|
m_showed_element->setHighlighted(true);
|
|
connect(m_showed_element, &QObject::destroyed, this, &MasterPropertiesWidget::showedElementWasDeleted);
|
|
}
|
|
|
|
/**
|
|
* @brief MasterPropertiesWidget::showedElementWasDeleted
|
|
* Set to nullptr the current showed element when he was deleted
|
|
*/
|
|
void MasterPropertiesWidget::showedElementWasDeleted()
|
|
{
|
|
m_showed_element = nullptr;
|
|
}
|
|
|
|
/**
|
|
* @brief MasterPropertiesWidget::diagramWasdeletedFromProject
|
|
* This slot is called when a diagram is removed from the parent project
|
|
* of edited element to update the content of this widget
|
|
*/
|
|
void MasterPropertiesWidget::diagramWasdeletedFromProject()
|
|
{
|
|
// We use a timer because if the removed diagram
|
|
// contains slave element linked to the edited element
|
|
// we must wait for this elements be unlinked,
|
|
// or else the linked list provides deleted elements.
|
|
QTimer::singleShot(10, this, &MasterPropertiesWidget::updateUi);
|
|
}
|
|
|
|
/**
|
|
* @brief MasterPropertiesWidget::customContextMenu
|
|
* Display a context menu
|
|
* @param pos
|
|
* @param i : the tree widget where the context menu was requested.
|
|
*/
|
|
void MasterPropertiesWidget::customContextMenu(const QPoint &pos, int i)
|
|
{
|
|
// add the size of the header to display the topleft of the QMenu
|
|
// at the position of the mouse.
|
|
// See doc about QWidget::customContextMenuRequested
|
|
// section related to QAbstractScrollArea
|
|
QPoint point = pos;
|
|
point.ry()+=ui->m_free_tree_widget->header()->height();
|
|
|
|
m_context_menu->clear();
|
|
|
|
if (i == 1)
|
|
{
|
|
point = ui->m_free_tree_widget->mapToGlobal(point);
|
|
|
|
//Context at for free tree widget
|
|
if (ui->m_free_tree_widget->currentItem())
|
|
{
|
|
m_qtwi_at_context_menu = ui->m_free_tree_widget->currentItem();
|
|
m_context_menu->addAction(m_link_action);
|
|
m_context_menu->addAction(m_show_qtwi);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
point = ui->m_link_tree_widget->mapToGlobal(point);
|
|
|
|
//context at for link tre widget
|
|
if (ui->m_link_tree_widget->currentItem())
|
|
{
|
|
m_qtwi_at_context_menu = ui->m_link_tree_widget->currentItem();
|
|
m_context_menu->addAction(m_unlink_action);
|
|
m_context_menu->addAction(m_show_qtwi);
|
|
}
|
|
}
|
|
|
|
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(6);
|
|
m_plc_table->setHorizontalHeaderLabels({
|
|
tr("Type"), tr("Adresse"), tr("Fonction"),
|
|
tr("Commentaire"), tr("Réf. croisée"), tr("Bornes")
|
|
});
|
|
m_plc_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
|
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems);
|
|
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);
|
|
|
|
// Anschlüsse (read-only)
|
|
auto *terminals_item = new QTableWidgetItem(io.terminals.join(QStringLiteral(", ")));
|
|
terminals_item->setFlags(terminals_item->flags() & ~Qt::ItemIsEditable);
|
|
m_plc_table->setItem(row, 5, terminals_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) {
|
|
// Anschlüsse (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());
|
|
|
|
// Anschlüsse (read-only)
|
|
auto *terminals_item = new QTableWidgetItem();
|
|
terminals_item->setFlags(terminals_item->flags() & ~Qt::ItemIsEditable);
|
|
m_plc_table->setItem(row, 5, terminals_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;
|
|
|
|
m_plc_updating = true;
|
|
|
|
// 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());
|
|
}
|
|
|
|
m_plc_updating = false;
|
|
plcUpdateDisplaySettings();
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
|
|
m_plc_updating = true;
|
|
|
|
// 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());
|
|
m_plc_updating = false;
|
|
plcUpdateDisplaySettings();
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
|
|
m_plc_updating = true;
|
|
|
|
// 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());
|
|
m_plc_updating = false;
|
|
plcUpdateDisplaySettings();
|
|
}
|
|
|
|
/**
|
|
* @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();
|
|
const auto orig_plc = ed.plcMasterData();
|
|
ElementData::PlcMasterData plc_data = orig_plc;
|
|
plc_data.ios.clear();
|
|
|
|
// Read IOs from table, preserving terminal data from original IOs by row index
|
|
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();
|
|
|
|
// Preserve terminal data by matching row index directly.
|
|
// This avoids address-based lookup which fails when all addresses
|
|
// are empty (common in PLC masters) — a hash collision would cause
|
|
// only the last IO's terminals to be used for all rows.
|
|
if (row < orig_plc.ios.size()) {
|
|
const auto &orig_io = orig_plc.ios.at(row);
|
|
io.terminalCount = orig_io.terminalCount;
|
|
io.terminals = orig_io.terminals;
|
|
}
|
|
|
|
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));
|
|
}
|