/* Copyright 2006-2026 The QElectroTech Team This file is part of QElectroTech. QElectroTech is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version. QElectroTech is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with QElectroTech. If not, see . */ #include "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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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> masters = ep.find(ElementData::Master); QSet 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(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 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); }