mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-08-03 18:44:13 +02:00
1070 lines
31 KiB
C++
1070 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, SIGNAL(diagramRemoved(QETProject*,Diagram*)),
|
|
this, SLOT(diagramWasdeletedFromProject()));
|
|
|
|
if(Q_LIKELY(element->diagram() && element->diagram()->project()))
|
|
{
|
|
m_project = element->diagram()->project();
|
|
connect(m_project, SIGNAL(diagramRemoved(QETProject*,Diagram*)),
|
|
this, SLOT(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
|
|
if (max_slaves != -1 && current_slaves >= max_slaves) {
|
|
|
|
|
|
// Show a message box with the actual window as parent to ensure it's on top
|
|
QMessageBox::warning(this->window(),
|
|
tr("Nombre maximal d'esclaves atteint."),
|
|
tr("Cet élément maître ne peut plus accepter aucun nouveau contact esclave, la limite fixée a été atteinte (Limite: %1).").arg(max_slaves));
|
|
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, SIGNAL(destroyed()),
|
|
this, SLOT(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, SIGNAL(destroyed()),
|
|
this, SLOT(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, SLOT(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(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;
|
|
|
|
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();
|
|
ElementData::PlcMasterData plc_data = ed.plcMasterData();
|
|
plc_data.ios.clear();
|
|
|
|
// Build address -> original IO lookup to correctly reattach terminal data
|
|
// after row reorder (move up/down) or row removal
|
|
QHash<QString, int> addr_to_orig_idx;
|
|
for (int i = 0; i < ed.plcMasterData().ios.size(); ++i) {
|
|
addr_to_orig_idx[ed.plcMasterData().ios.at(i).address] = i;
|
|
}
|
|
|
|
// Read IOs from table, preserving terminal data from original IOs
|
|
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 looking up original IO via address
|
|
if (addr_to_orig_idx.contains(io.address)) {
|
|
const auto &orig_io = ed.plcMasterData().ios.at(addr_to_orig_idx.value(io.address));
|
|
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));
|
|
}
|