From b3a8ae898e086b59927939fd0b016b103fdb8a2a Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Mon, 27 Jul 2026 22:16:52 +0200 Subject: [PATCH 1/2] PLC Manager --- cmake/qet_compilation_vars.cmake | 4 + sources/autoNum/assignvariables.cpp | 6 + sources/dataBase/projectdatabase.cpp | 5 + sources/editor/elementscene.cpp | 56 +- sources/editor/graphicspart/partplctable.cpp | 696 +++++++++++++++++ sources/editor/graphicspart/partplctable.h | 89 +++ sources/editor/ui/dynamictextfieldeditor.cpp | 27 +- .../ui/elementpropertieseditorwidget.cpp | 721 +++++++++++++++++- .../editor/ui/elementpropertieseditorwidget.h | 32 + .../ui/elementpropertieseditorwidget.ui | 22 +- sources/factory/elementpicturefactory.cpp | 250 +++++- sources/factory/elementpicturefactory.h | 1 + sources/properties/elementdata.cpp | 305 ++++++++ sources/properties/elementdata.h | 87 ++- sources/properties/xrefproperties.cpp | 2 +- sources/qetgraphicsitem/crossrefitem.cpp | 221 +++++- sources/qetgraphicsitem/crossrefitem.h | 1 + .../dynamicelementtextitem.cpp | 29 +- sources/qetgraphicsitem/element.cpp | 278 ++++++- sources/qetgraphicsitem/element.h | 13 +- sources/qetgraphicsitem/masterelement.cpp | 78 +- sources/qetgraphicsitem/masterelement.h | 6 + sources/qetinformation.cpp | 10 + sources/qetinformation.h | 7 + sources/ui/elementpropertieswidget.cpp | 6 +- sources/ui/linksingleelementwidget.cpp | 100 ++- sources/ui/masterpropertieswidget.cpp | 664 +++++++++++++--- sources/ui/masterpropertieswidget.h | 25 + sources/ui/plclinkwidget.cpp | 371 +++++++++ sources/ui/plclinkwidget.h | 87 +++ sources/ui/xrefpropertieswidget.cpp | 1 + sources/undocommand/linkelementcommand.cpp | 134 ++++ 32 files changed, 4157 insertions(+), 177 deletions(-) create mode 100644 sources/editor/graphicspart/partplctable.cpp create mode 100644 sources/editor/graphicspart/partplctable.h create mode 100644 sources/ui/plclinkwidget.cpp create mode 100644 sources/ui/plclinkwidget.h diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake index d7eab57be..c314cac81 100644 --- a/cmake/qet_compilation_vars.cmake +++ b/cmake/qet_compilation_vars.cmake @@ -331,6 +331,8 @@ set(QET_SRC_FILES ${QET_DIR}/sources/editor/graphicspart/partpolygon.h ${QET_DIR}/sources/editor/graphicspart/partrectangle.cpp ${QET_DIR}/sources/editor/graphicspart/partrectangle.h + ${QET_DIR}/sources/editor/graphicspart/partplctable.cpp + ${QET_DIR}/sources/editor/graphicspart/partplctable.h ${QET_DIR}/sources/editor/graphicspart/partterminal.cpp ${QET_DIR}/sources/editor/graphicspart/partterminal.h ${QET_DIR}/sources/editor/graphicspart/parttext.cpp @@ -673,6 +675,8 @@ set(QET_SRC_FILES ${QET_DIR}/sources/ui/marginseditdialog.h ${QET_DIR}/sources/ui/masterpropertieswidget.cpp ${QET_DIR}/sources/ui/masterpropertieswidget.h + ${QET_DIR}/sources/ui/plclinkwidget.cpp + ${QET_DIR}/sources/ui/plclinkwidget.h ${QET_DIR}/sources/ui/multipastedialog.cpp ${QET_DIR}/sources/ui/multipastedialog.h ${QET_DIR}/sources/ui/potentialselectordialog.cpp diff --git a/sources/autoNum/assignvariables.cpp b/sources/autoNum/assignvariables.cpp index c4810906e..8f58adff1 100644 --- a/sources/autoNum/assignvariables.cpp +++ b/sources/autoNum/assignvariables.cpp @@ -275,6 +275,12 @@ namespace autonum str.replace("%{void}", QString()); + str.replace("%{plc_type}", dc.value("plc_type").toString()); + str.replace("%{plc_address}", dc.value("plc_address").toString()); + str.replace("%{plc_function}", dc.value("plc_function").toString()); + str.replace("%{plc_comment}", dc.value("plc_comment").toString()); + str.replace("%{plc_crossref}", dc.value("plc_crossref").toString()); + return str; } diff --git a/sources/dataBase/projectdatabase.cpp b/sources/dataBase/projectdatabase.cpp index 5bec7b231..b1cd048bb 100644 --- a/sources/dataBase/projectdatabase.cpp +++ b/sources/dataBase/projectdatabase.cpp @@ -114,6 +114,11 @@ QSqlQuery projectDataBase::newQuery(const QString &query) { */ void projectDataBase::addElement(Element *element) { + if (!element || !element->diagram()) { + qDebug() << "projectDataBase::addElement: null element or diagram"; + return; + } + m_insert_elements_query.bindValue(":uuid", element->uuid().toString()); m_insert_elements_query.bindValue(":diagram_uuid", element->diagram()->uuid().toString()); m_insert_elements_query.bindValue(":pos", element->diagram()->convertPosition(element->scenePos()).toString()); diff --git a/sources/editor/elementscene.cpp b/sources/editor/elementscene.cpp index 121c905d5..226380fc4 100644 --- a/sources/editor/elementscene.cpp +++ b/sources/editor/elementscene.cpp @@ -31,6 +31,7 @@ #include "graphicspart/partline.h" #include "graphicspart/partpolygon.h" #include "graphicspart/partrectangle.h" +#include "graphicspart/partplctable.h" #include "graphicspart/partterminal.h" #include "graphicspart/parttext.h" #include "ui/qetelementeditor.h" @@ -84,12 +85,12 @@ ElementData ElementScene::elementData() { void ElementScene::setElementData(ElementData data) { - bool emit_ = m_element_data.m_informations != data.m_informations; + bool emit_info = (m_element_data != data); bool type_changed = m_element_data.m_type != data.m_type; m_element_data = data; - if (emit_) + if (emit_info) emit elementInfoChanged(); if (type_changed) emit elementTypeChanged(); @@ -926,9 +927,41 @@ void ElementScene::slot_editProperties() if (m_element_data != epew.editedData()) { + ElementData new_data = epew.editedData(); + + // Check PLC state BEFORE pushing (push calls redo which changes m_element_data) + bool old_plc = (m_element_data.m_type == ElementData::Master && + m_element_data.m_master_type == ElementData::PLC); + bool new_plc = (new_data.m_type == ElementData::Master && + new_data.m_master_type == ElementData::PLC); + undoStack().push(new changeElementDataCommand(this, m_element_data, - epew.editedData())); + new_data)); + + if (new_plc && !old_plc) { + // Switched TO PLC: create table if not present + bool has_plc = false; + for (QGraphicsItem *item : items()) { + if (dynamic_cast(item)) { + has_plc = true; + break; + } + } + if (!has_plc) { + PartPlcTable *pt = new PartPlcTable(m_element_editor); + addItem(pt); + } + } else if (!new_plc && old_plc) { + // Switched FROM PLC: remove table + for (QGraphicsItem *item : items()) { + if (PartPlcTable *pt = dynamic_cast(item)) { + removeItem(pt); + delete pt; + break; + } + } + } } } @@ -1172,15 +1205,16 @@ ElementContent ElementScene::loadContent(const QDomDocument &xml_document) CustomElementPart *cep = nullptr; PartDynamicTextField *pdtf = nullptr; - if (qde.tagName() == "line") cep = new PartLine (m_element_editor); - else if (qde.tagName() == "rect") cep = new PartRectangle(m_element_editor); - else if (qde.tagName() == "ellipse") cep = new PartEllipse (m_element_editor); - else if (qde.tagName() == "circle") cep = new PartEllipse (m_element_editor); - else if (qde.tagName() == "polygon") cep = new PartPolygon (m_element_editor); - else if (qde.tagName() == "terminal") cep = new PartTerminal (m_element_editor); - else if (qde.tagName() == "text") cep = new PartText (m_element_editor); - else if (qde.tagName() == "arc") cep = new PartArc (m_element_editor); + if (qde.tagName() == "line") cep = new PartLine (m_element_editor); + else if (qde.tagName() == "rect") cep = new PartRectangle (m_element_editor); + else if (qde.tagName() == "ellipse") cep = new PartEllipse (m_element_editor); + else if (qde.tagName() == "circle") cep = new PartEllipse (m_element_editor); + else if (qde.tagName() == "polygon") cep = new PartPolygon (m_element_editor); + else if (qde.tagName() == "terminal") cep = new PartTerminal (m_element_editor); + else if (qde.tagName() == "text") cep = new PartText (m_element_editor); + else if (qde.tagName() == "arc") cep = new PartArc (m_element_editor); else if (qde.tagName() == "dynamic_text") cep = new PartDynamicTextField (m_element_editor); + else if (qde.tagName() == "plc_table") cep = new PartPlcTable (m_element_editor); //For the input (aka the old text field) we try to convert it to the new partDynamicTextField else if (qde.tagName() == "input") cep = pdtf = new PartDynamicTextField(m_element_editor); else continue; diff --git a/sources/editor/graphicspart/partplctable.cpp b/sources/editor/graphicspart/partplctable.cpp new file mode 100644 index 000000000..00014a4a9 --- /dev/null +++ b/sources/editor/graphicspart/partplctable.cpp @@ -0,0 +1,696 @@ +/* + 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 "partplctable.h" + +#include "../../QPropertyUndoCommand/qpropertyundocommand.h" +#include "../../QetGraphicsItemModeler/qetgraphicshandleritem.h" +#include "../../QetGraphicsItemModeler/qetgraphicshandlerutility.h" +#include "../../properties/elementdata.h" +#include "../elementscene.h" +#include "../editorcommands.h" +#include "../ui/qetelementeditor.h" + +#include +#include + +/** + @brief PartPlcTable::PartPlcTable + Constructor + @param editor the QETElementEditor of this item + @param parent parent item +*/ +PartPlcTable::PartPlcTable(QETElementEditor *editor, QGraphicsItem *parent) : + CustomElementGraphicPart(editor, parent) +{ +} + +/** + @brief PartPlcTable::~PartPlcTable +*/ +PartPlcTable::~PartPlcTable() +{ + removeHandler(); +} + +/** + @brief PartPlcTable::calculateTableSize + Calculate the table size from PLC data in the element scene. + @return the calculated size in item coordinates +*/ +QSizeF PartPlcTable::calculateTableSize() const +{ + if (!elementEditor() || !elementScene()) + return QSizeF(100, 50); + + ElementData ed = elementScene()->elementData(); + if (ed.m_master_type != ElementData::PLC) + return QSizeF(100, 50); + + const auto &plc_data = ed.plcMasterData(); + if (plc_data.ios.isEmpty()) + return QSizeF(100, 50); + + const int COL_COUNT = 5; + + // Build list of visible columns + QList visible_cols; + if (!plc_data.columnOrder.isEmpty()) { + for (int logical : plc_data.columnOrder) { + if (logical >= 0 && logical < COL_COUNT + && plc_data.colVisible.value(logical, true) + && !visible_cols.contains(logical)) + visible_cols.append(logical); + } + for (int i = 0; i < COL_COUNT; ++i) { + if (plc_data.colVisible.value(i, true) && !visible_cols.contains(i)) + visible_cols.append(i); + } + } else { + for (int i = 0; i < COL_COUNT; ++i) { + if (plc_data.colVisible.value(i, true)) + visible_cols.append(i); + } + } + if (visible_cols.isEmpty()) + visible_cols << 0 << 1 << 2; // fallback: Type, Address, Function + + // Default column widths + QMap col_widths; + for (int col : visible_cols) { + if (plc_data.colWidths.contains(col) && plc_data.colWidths[col] > 0) + col_widths[col] = plc_data.colWidths[col]; + else { + switch (col) { + case 0: col_widths[col] = 35; break; // Type + case 1: col_widths[col] = 25; break; // Address + case 2: col_widths[col] = 50; break; // Function + case 3: col_widths[col] = 40; break; // Comment + case 5: col_widths[col] = 30; break; // CrossRef + default: col_widths[col] = 30; break; + } + } + } + + qreal row_h = plc_data.rowHeight > 0 ? plc_data.rowHeight : 8.0; + qreal header_h = plc_data.showHeaders ? (row_h + 2.0) : 0; + + qreal total_width = 0; + for (int col : visible_cols) + total_width += col_widths[col]; + + int total_ios = plc_data.ios.size(); + + // Collect active break positions (sorted) + QList breaks; + for (int bp : plc_data.breakPositions) { + if (bp > 0 && bp < total_ios && !breaks.contains(bp)) + breaks.append(bp); + } + std::sort(breaks.begin(), breaks.end()); + + // Build block boundaries + QList block_starts; + block_starts.append(0); + for (int bp : breaks) + block_starts.append(bp); + + qreal total_height; + int block_count = block_starts.size(); + + if (block_count > 1) { + // Find the tallest block (most rows) + int max_rows = 0; + for (int b = 0; b < block_count; ++b) { + int start = block_starts.at(b); + int end = (b + 1 < block_starts.size()) ? block_starts.at(b + 1) : total_ios; + max_rows = qMax(max_rows, end - start); + } + total_height = header_h + max_rows * row_h; + total_width = total_width * block_count + (block_count - 1) * 3; + } else { + total_height = header_h + total_ios * row_h; + } + + return QSizeF(total_width, total_height); +} + +/** + @brief PartPlcTable::paint + Draw this PLC table + @param painter + @param options + @param widget +*/ +void PartPlcTable::paint(QPainter *painter, const QStyleOptionGraphicsItem *options, QWidget *widget) +{ + Q_UNUSED(widget); + Q_UNUSED(options); + + // Auto-size from PLC data + QSizeF table_size = calculateTableSize(); + if (m_rect.size() != table_size) { + prepareGeometryChange(); + QPointF top_left = m_rect.topLeft(); + m_rect = QRectF(top_left, table_size); + } + + applyStylesToQPainter(*painter); + QPen t = painter->pen(); + t.setCosmetic(options && options->levelOfDetailFromTransform(painter->worldTransform()) < 1.0); + if (isSelected()) + t.setColor(Qt::red); + t.setJoinStyle(Qt::MiterJoin); + if (!m_rect.width() || !m_rect.height()) + t.setWidth(0); + painter->setPen(t); + + // Get PLC data + ElementData ed = (elementEditor() && elementScene()) ? elementScene()->elementData() : ElementData(); + if (ed.m_master_type != ElementData::PLC) { + // Draw placeholder + painter->setBrush(QColor(255, 255, 200)); + painter->drawRect(m_rect); + painter->drawText(m_rect, Qt::AlignCenter, QObject::tr("Table PLC")); + return; + } + + const auto &plc_data = ed.plcMasterData(); + if (plc_data.ios.isEmpty()) { + painter->setBrush(QColor(255, 255, 200)); + painter->drawRect(m_rect); + painter->drawText(m_rect, Qt::AlignCenter, QObject::tr("Table PLC (vide)")); + return; + } + + const int COL_TYPE = 0; + const int COL_ADDRESS = 1; + const int COL_FUNCTION = 2; + const int COL_COMMENT = 3; + const int COL_CROSSREF = 4; + const int COL_COUNT = 5; + + QMap headers; + headers[COL_TYPE] = QObject::tr("Type"); + headers[COL_ADDRESS] = QObject::tr("Adresse"); + headers[COL_FUNCTION] = QObject::tr("Fonction"); + headers[COL_COMMENT] = QObject::tr("Commentaire"); + headers[COL_CROSSREF] = QObject::tr("Réf. croisée"); + + // Override with custom column names if set + if (!plc_data.columnNames.isEmpty()) { + QList all_cols; + all_cols << COL_TYPE << COL_ADDRESS << COL_FUNCTION << COL_COMMENT << COL_CROSSREF; + for (int i = 0; i < qMin(plc_data.columnNames.size(), all_cols.size()); ++i) { + if (!plc_data.columnNames.at(i).isEmpty()) + headers[all_cols.at(i)] = plc_data.columnNames.at(i); + } + } + + QList visible_cols; + if (!plc_data.columnOrder.isEmpty()) { + for (int logical : plc_data.columnOrder) { + if (logical >= 0 && logical < COL_COUNT + && plc_data.colVisible.value(logical, true) + && !visible_cols.contains(logical)) + visible_cols.append(logical); + } + for (int i = 0; i < COL_COUNT; ++i) { + if (plc_data.colVisible.value(i, true) && !visible_cols.contains(i)) + visible_cols.append(i); + } + } else { + for (int i = 0; i < COL_COUNT; ++i) { + if (plc_data.colVisible.value(i, true)) + visible_cols.append(i); + } + } + if (visible_cols.isEmpty()) + return; + + QMap col_widths; + for (int col : visible_cols) { + if (plc_data.colWidths.contains(col) && plc_data.colWidths[col] > 0) + col_widths[col] = plc_data.colWidths[col]; + else { + switch (col) { + case COL_TYPE: col_widths[col] = 35; break; + case COL_ADDRESS: col_widths[col] = 25; break; + case COL_FUNCTION: col_widths[col] = 50; break; + case COL_COMMENT: col_widths[col] = 40; break; + case COL_CROSSREF: col_widths[col] = 30; break; + default: col_widths[col] = 30; break; + } + } + } + + qreal row_h = plc_data.rowHeight > 0 ? plc_data.rowHeight : 8.0; + qreal header_h = plc_data.showHeaders ? (row_h + 2.0) : 0; + + int total_ios = plc_data.ios.size(); + + // Collect active break positions (sorted) + QList breaks; + for (int bp : plc_data.breakPositions) { + if (bp > 0 && bp < total_ios && !breaks.contains(bp)) + breaks.append(bp); + } + std::sort(breaks.begin(), breaks.end()); + + // Build block boundaries: start, break1, break2, ..., end + QList block_starts; + block_starts.append(0); + for (int bp : breaks) + block_starts.append(bp); + int block_count = block_starts.size(); + + // Draw background + painter->save(); + painter->setPen(Qt::NoPen); + painter->setBrush(Qt::white); + painter->drawRect(m_rect); + painter->restore(); + + // Draw outer border + QPen border_pen(Qt::black, 0.5); + painter->setPen(border_pen); + painter->setBrush(Qt::NoBrush); + painter->drawRect(m_rect); + + // Draw each block + for (int block = 0; block < block_count; ++block) { + qreal block_w = 0; + for (int col : visible_cols) + block_w += col_widths[col]; + + qreal block_x = m_rect.x() + block * (block_w + 3); + qreal cx = block_x; + + // Draw column headers + QFont header_font = plc_data.headerFont.family().isEmpty() + ? painter->font() : plc_data.headerFont; + header_font.setBold(true); + painter->setFont(header_font); + + for (int col : visible_cols) { + QRectF header_rect(cx, m_rect.y(), col_widths[col], header_h); + painter->fillRect(header_rect, QColor(220, 220, 220)); + painter->setPen(border_pen); + painter->drawRect(header_rect); + QString header_text = headers.value(col, QString()); + painter->drawText(header_rect, Qt::AlignCenter, header_text); + cx += col_widths[col]; + } + + // Draw IO rows + QFont cell_font = plc_data.cellFont.family().isEmpty() + ? painter->font() : plc_data.cellFont; + painter->setFont(cell_font); + int start_idx = block_starts.at(block); + int end_idx = (block + 1 < block_starts.size()) + ? block_starts.at(block + 1) : total_ios; + + for (int row = 0; row < (end_idx - start_idx); ++row) { + int io_idx = start_idx + row; + const ElementData::PlcIO &io = plc_data.ios.at(io_idx); + + qreal ry = m_rect.y() + header_h + row * row_h; + cx = block_x; + + for (int col : visible_cols) { + QRectF cell_rect(cx, ry, col_widths[col], row_h); + painter->setPen(border_pen); + painter->drawRect(cell_rect); + + QString cell_text; + switch (col) { + case COL_TYPE: cell_text = ElementData::translatedPlcIOType(io.type); break; + case COL_ADDRESS: cell_text = io.address; break; + case COL_FUNCTION: cell_text = io.functionText; break; + case COL_COMMENT: cell_text = io.comment; break; + case COL_CROSSREF: cell_text = io.crossRef; break; + } + + QRectF text_rect = cell_rect.adjusted(1, 0, -1, 0); + painter->drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, cell_text); + + cx += col_widths[col]; + } + } + } + + if (m_hovered) + drawShadowShape(painter); + + if (isSelected()) + drawCross(m_rect.center(), painter); +} + +/** + @brief PartPlcTable::toXml + Export this PLC table part in xml + @param xml_document : Xml document to use for create the xml element. + @return an xml element that describe this part +*/ +const QDomElement PartPlcTable::toXml(QDomDocument &xml_document) const +{ + QDomElement xml_element = xml_document.createElement("plc_table"); + qreal x = qRound(m_rect.x() * 100.0) / 100.0; + qreal y = qRound(m_rect.y() * 100.0) / 100.0; + + xml_element.setAttribute("x", QString::number(x)); + xml_element.setAttribute("y", QString::number(y)); + + stylesToXml(xml_element); + return xml_element; +} + +/** + @brief PartPlcTable::fromXml + Import the properties of this PLC table part from a xml element. + @param qde : Xml document to use. +*/ +void PartPlcTable::fromXml(const QDomElement &qde) +{ + stylesFromXml(qde); + qreal x = qde.attribute("x", "0").toDouble(); + qreal y = qde.attribute("y", "0").toDouble(); + setPos(mapFromScene(x, y)); + + // Auto-size from PLC data + QSizeF table_size = calculateTableSize(); + prepareGeometryChange(); + m_rect = QRectF(QPointF(0, 0), table_size); + update(); +} + +/** + @brief PartPlcTable::rect + @return : Returns the item's rectangle. +*/ +QRectF PartPlcTable::rect() const +{ + return m_rect; +} + +/** + @brief PartPlcTable::setRect + Sets the item's rectangle to be the given rectangle. + @param rect +*/ +void PartPlcTable::setRect(const QRectF &rect) +{ + if (rect == m_rect) return; + prepareGeometryChange(); + m_rect = rect; + adjustHandlerPos(); + update(); +} + +/** + @brief PartPlcTable::sceneGeometricRect + @return the minimum, margin-less rectangle this part can fit into, in scene + coordinates. +*/ +QRectF PartPlcTable::sceneGeometricRect() const +{ + return(mapToScene(m_rect).boundingRect()); +} + +/** + @brief PartPlcTable::shape + @return the shape of this item +*/ +QPainterPath PartPlcTable::shape() const +{ + QPainterPath fill; + fill.addRect(m_rect); + + QPainterPath stroke; + stroke.addRect(m_rect); + + QPainterPathStroker pps; + pps.setWidth(m_hovered? penWeight()+SHADOWS_HEIGHT : penWeight()); + stroke = pps.createStroke(stroke); + + return fill.united(stroke); +} + +QPainterPath PartPlcTable::shadowShape() const +{ + QPainterPath shape; + shape.addRect(m_rect); + + QPainterPathStroker pps; + pps.setWidth(penWeight()); + + return (pps.createStroke(shape)); +} + +/** + @brief PartPlcTable::boundingRect + @return Bounding rectangle this part can fit into +*/ +QRectF PartPlcTable::boundingRect() const +{ + qreal adjust = (SHADOWS_HEIGHT + penWeight()) / 2; + if (penWeight() == 0) adjust += 0.5; + + QRectF r = m_rect.normalized(); + r.adjust(-adjust, -adjust, adjust, adjust); + + return(r); +} + +/** + @brief PartPlcTable::isUseless + @return true if this part is irrelevant and does not deserve to be saved. + A PLC table part is always relevant. +*/ +bool PartPlcTable::isUseless() const +{ + return false; +} + +/** + @brief PartPlcTable::startUserTransformation + @param initial_selection_rect +*/ +void PartPlcTable::startUserTransformation(const QRectF &initial_selection_rect) +{ + Q_UNUSED(initial_selection_rect) + saved_points_.clear(); + saved_points_ << mapToScene(m_rect.topLeft()) << mapToScene(m_rect.bottomRight()); +} + +/** + @brief PartPlcTable::handleUserTransformation + @param initial_selection_rect + @param new_selection_rect +*/ +void PartPlcTable::handleUserTransformation(const QRectF &initial_selection_rect, const QRectF &new_selection_rect) +{ + QList mapped_points = mapPoints(initial_selection_rect, new_selection_rect, saved_points_); + setRect(QRectF(mapFromScene(mapped_points.at(0)), mapFromScene(mapped_points.at(1)))); +} + +/** + @brief PartPlcTable::mouseReleaseEvent +*/ +void PartPlcTable::mouseReleaseEvent(QGraphicsSceneMouseEvent *event) +{ + CustomElementGraphicPart::mouseReleaseEvent(event); +} + +/** + @brief PartPlcTable::itemChange + @param change + @param value + @return +*/ +QVariant PartPlcTable::itemChange(GraphicsItemChange change, const QVariant &value) +{ + if (change == ItemPositionHasChanged) + { + adjustHandlerPos(); + } + else if (change == ItemSceneChange) + { + setSelected(false); + } + else if (change == ItemSceneHasChanged) + { + if (ElementScene *es = elementScene()) { + connect(es, &ElementScene::elementInfoChanged, + this, [this]() { + QSizeF table_size = calculateTableSize(); + if (m_rect.size() != table_size) { + QPointF top_left = m_rect.topLeft(); + setRect(QRectF(top_left, table_size)); + } + update(); + }); + } + } + + return QGraphicsItem::itemChange(change, value); +} + +/** + @brief PartPlcTable::sceneEventFilter + @param watched + @param event + @return +*/ +bool PartPlcTable::sceneEventFilter(QGraphicsItem *watched, QEvent *event) +{ + if(watched->type() == QetGraphicsHandlerItem::Type) + { + QetGraphicsHandlerItem *qghi = qgraphicsitem_cast(watched); + + if(m_handler_vector.contains(qghi)) + { + m_vector_index = m_handler_vector.indexOf(qghi); + if (m_vector_index != -1) + { + if(event->type() == QEvent::GraphicsSceneMousePress) + { + handlerMousePressEvent(qghi, static_cast(event)); + return true; + } + else if(event->type() == QEvent::GraphicsSceneMouseMove) + { + handlerMouseMoveEvent(qghi, static_cast(event)); + return true; + } + else if (event->type() == QEvent::GraphicsSceneMouseRelease) + { + handlerMouseReleaseEvent(qghi, static_cast(event)); + return true; + } + } + } + } + + return false; +} + +/** + @brief PartPlcTable::switchResizeMode +*/ +void PartPlcTable::switchResizeMode() +{ + if (m_resize_mode == 1) + { + m_resize_mode = 2; + for (QetGraphicsHandlerItem *qghi : m_handler_vector) + qghi->setColor(Qt::darkGreen); + } + else + { + m_resize_mode = 1; + qDeleteAll(m_handler_vector); + m_handler_vector.clear(); + addHandler(); + for (QetGraphicsHandlerItem *qghi : m_handler_vector) { + qghi->setColor(Qt::blue); + } + } +} + +/** + @brief PartPlcTable::adjustHandlerPos +*/ +void PartPlcTable::adjustHandlerPos() +{ + if (m_handler_vector.isEmpty()) + return; + + QVector points_vector = QetGraphicsHandlerUtility::pointsForRect(m_rect); + + if (m_handler_vector.size() == points_vector.size()) + { + points_vector = mapToScene(points_vector); + for (int i = 0 ; i < points_vector.size() ; ++i) + m_handler_vector.at(i)->setPos(points_vector.at(i)); + } + else + { + qDeleteAll(m_handler_vector); + m_handler_vector.clear(); + addHandler(); + } +} + +void PartPlcTable::handlerMousePressEvent(QetGraphicsHandlerItem *qghi, QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(qghi) + Q_UNUSED(event) + + m_old_rect = m_rect; +} + +void PartPlcTable::handlerMouseMoveEvent(QetGraphicsHandlerItem *qghi, QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(qghi) + + QPointF new_pos = event->scenePos(); + if (event->modifiers() != Qt::ControlModifier) + new_pos = elementScene()->snapToGrid(event->scenePos()); + new_pos = mapFromScene(new_pos); + + setRect(QetGraphicsHandlerUtility::rectForPosAtIndex(m_rect, new_pos, m_vector_index)); + adjustHandlerPos(); +} + +void PartPlcTable::handlerMouseReleaseEvent(QetGraphicsHandlerItem *qghi, QGraphicsSceneMouseEvent *event) +{ + Q_UNUSED(qghi) + Q_UNUSED(event) + + QUndoCommand *undo = new QUndoCommand("Modifier une table PLC"); + if (m_old_rect != m_rect) { + QPropertyUndoCommand *u = new QPropertyUndoCommand(this, "rect", QVariant(m_old_rect.normalized()), QVariant(m_rect.normalized()), undo); + u->setAnimated(true, false); + } + + elementScene()->undoStack().push(undo); + m_vector_index = -1; +} + +/** + @brief PartPlcTable::addHandler + Do not add resize handlers - table size is data-driven. + Move is handled by the base class QGraphicsItem drag behavior. +*/ +void PartPlcTable::addHandler() +{ + // No resize handlers - size comes from PLC data +} + +/** + @brief PartPlcTable::removeHandler + Remove the handlers of this item +*/ +void PartPlcTable::removeHandler() +{ + if (!m_handler_vector.isEmpty()) + { + qDeleteAll(m_handler_vector); + m_handler_vector.clear(); + } +} diff --git a/sources/editor/graphicspart/partplctable.h b/sources/editor/graphicspart/partplctable.h new file mode 100644 index 000000000..483c49dd2 --- /dev/null +++ b/sources/editor/graphicspart/partplctable.h @@ -0,0 +1,89 @@ +/* + 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 . +*/ +#ifndef PARTPLCTABLE_H +#define PARTPLCTABLE_H + +#include "customelementgraphicpart.h" +#include "../../QetGraphicsItemModeler/qetgraphicshandleritem.h" + +#include + +/** + @brief The PartPlcTable class + This class represents a PLC I/O table preview in the element editor. + It shows the user where the PLC table will appear at runtime, so they + can position other element parts (rectangles, terminals, etc.) around it. + The actual PLC data is read from ElementScene::elementData(). +*/ +class PartPlcTable : public CustomElementGraphicPart +{ + Q_OBJECT + + Q_PROPERTY(QRectF rect READ rect WRITE setRect) + + public: + PartPlcTable(QETElementEditor *editor, QGraphicsItem *parent = nullptr); + ~PartPlcTable() override; + + enum { Type = UserType + 1120 }; + int type () const override { return Type; } + void paint (QPainter *, const QStyleOptionGraphicsItem *, QWidget * = nullptr) override; + QString name () const override { return QObject::tr("table PLC", "element part name"); } + + QString xmlName () const override { return QString("plc_table"); } + const QDomElement toXml (QDomDocument &) const override; + void fromXml (const QDomElement &) override; + + QRectF rect() const; + void setRect(const QRectF &rect); + + QRectF sceneGeometricRect() const override; + QPainterPath shape () const override; + QPainterPath shadowShape() const override; + QRectF boundingRect() const override; + bool isUseless() const override; + + void startUserTransformation(const QRectF &) override; + void handleUserTransformation(const QRectF &, const QRectF &) override; + + void addHandler() override; + void removeHandler() override; + + protected: + void mouseReleaseEvent(QGraphicsSceneMouseEvent *event) override; + QVariant itemChange(GraphicsItemChange change, const QVariant &value) override; + bool sceneEventFilter(QGraphicsItem *watched, QEvent *event) override; + + private: + void switchResizeMode(); + void adjustHandlerPos(); + void handlerMousePressEvent (QetGraphicsHandlerItem *qghi, QGraphicsSceneMouseEvent *event); + void handlerMouseMoveEvent (QetGraphicsHandlerItem *qghi, QGraphicsSceneMouseEvent *event); + void handlerMouseReleaseEvent (QetGraphicsHandlerItem *qghi, QGraphicsSceneMouseEvent *event); + QSizeF calculateTableSize() const; + + private: + QRectF m_rect, + m_old_rect; + QList saved_points_; + int m_resize_mode = 1, + m_vector_index = -1; + QVector m_handler_vector; +}; + +#endif // PARTPLCTABLE_H diff --git a/sources/editor/ui/dynamictextfieldeditor.cpp b/sources/editor/ui/dynamictextfieldeditor.cpp index 84f4db772..fa29093af 100644 --- a/sources/editor/ui/dynamictextfieldeditor.cpp +++ b/sources/editor/ui/dynamictextfieldeditor.cpp @@ -197,6 +197,10 @@ void DynamicTextFieldEditor::setUpConnections() m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::textWidthChanged, this, [=](){this -> updateForm();}); m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::compositeTextChanged,this, [=](){this -> updateForm();}); m_connection_list << connect(m_text_field.data(), &PartDynamicTextField::keepVisualRotationChanged, this, [=](){this -> updateForm();}); + + // Refresh info combo when element data changes (e.g. type switched to PLC-Slave) + m_connection_list << connect(elementEditor()->elementScene(), &ElementScene::elementInfoChanged, + this, &DynamicTextFieldEditor::fillInfoComboBox); } void DynamicTextFieldEditor::disconnectConnections() @@ -218,13 +222,34 @@ void DynamicTextFieldEditor::fillInfoComboBox() ui -> m_elmt_info_cb -> clear(); QStringList strl; - auto type = elementEditor()->elementScene()->elementData().m_type; + auto ed = elementEditor()->elementScene()->elementData(); + auto type = ed.m_type; if((type & ElementData::AllReport) || (type == ElementData::ConductorDefinition)) { strl = QETInformation::folioReportInfoKeys(); } else { strl = QETInformation::elementInfoKeys(); + + bool is_plc_slave = (type == ElementData::Slave + && ed.m_slave_type == ElementData::PLCSlave); + + if (is_plc_slave) { + QStringList plc_keys = { + QETInformation::ELMT_PLC_TYPE, + QETInformation::ELMT_PLC_ADDRESS, + QETInformation::ELMT_PLC_FUNCTION, + QETInformation::ELMT_PLC_COMMENT, + QETInformation::ELMT_PLC_CROSSREF + }; + strl = plc_keys + strl; + } else { + strl.removeAll(QETInformation::ELMT_PLC_TYPE); + strl.removeAll(QETInformation::ELMT_PLC_ADDRESS); + strl.removeAll(QETInformation::ELMT_PLC_FUNCTION); + strl.removeAll(QETInformation::ELMT_PLC_COMMENT); + strl.removeAll(QETInformation::ELMT_PLC_CROSSREF); + } } for (int i=0; i #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /** @brief The EditorDelegate class @@ -91,9 +104,16 @@ void ElementPropertiesEditorWidget::upDateInterface() if (m_data.m_type == ElementData::Slave) { - ui->m_state_cb->setCurrentIndex( - ui->m_state_cb->findData( - m_data.m_slave_state)); + // If PLC Slave, select "Esclave PLC" in state combo (not in type combo) + if (m_data.m_slave_type == ElementData::PLCSlave) { + ui->m_state_cb->setCurrentIndex( + ui->m_state_cb->findData( + ElementData::PLCSlave)); + } else { + ui->m_state_cb->setCurrentIndex( + ui->m_state_cb->findData( + m_data.m_slave_state)); + } ui->m_type_cb->setCurrentIndex ( ui->m_type_cb->findData( m_data.m_slave_type)); @@ -120,6 +140,26 @@ void ElementPropertiesEditorWidget::upDateInterface() if (m_data.m_slave_contact_groups_enabled) { populateSlaveGroupsTable(); } + + // PLC configuration + if (m_data.m_master_type == ElementData::PLC) { + if (!m_plc_gb) { + createPlcConfigWidgets(); + } + m_plc_gb->setVisible(true); + ui->max_slaves_checkbox->setVisible(false); + ui->max_slaves_spinbox->setVisible(false); + ui->m_slave_groups_checkbox->setVisible(false); + ui->m_slave_groups_table->setVisible(false); + populatePlcTable(); + } else { + if (m_plc_gb) + m_plc_gb->setVisible(false); + ui->max_slaves_checkbox->setVisible(true); + ui->max_slaves_spinbox->setVisible(true); + ui->m_slave_groups_checkbox->setVisible(true); + ui->m_slave_groups_table->setVisible(true); + } } else if (m_data.m_type == ElementData::Terminal) { ui->m_terminal_type_cb->setCurrentIndex( ui->m_terminal_type_cb->findData( @@ -152,6 +192,7 @@ void ElementPropertiesEditorWidget::setUpInterface() ui->m_state_cb->addItem(tr("Normalement fermé"), ElementData::NC); ui->m_state_cb->addItem(tr("Inverseur"), ElementData::SW); ui->m_state_cb->addItem(tr("Other"), ElementData::Other); + ui->m_state_cb->addItem(tr("Esclave PLC"), ElementData::PLCSlave); ui->m_type_cb->addItem(tr("Simple"), ElementData::SSimple); ui->m_type_cb->addItem(tr("Puissance"), ElementData::Power); ui->m_type_cb->addItem(tr("Temporisé travail"), ElementData::DelayOn); @@ -162,6 +203,8 @@ void ElementPropertiesEditorWidget::setUpInterface() ui->m_master_type_cb->addItem(tr("Bobine"), ElementData::Coil); ui->m_master_type_cb->addItem(tr("Organe de protection"), ElementData::Protection); ui->m_master_type_cb->addItem(tr("Commutateur / bouton"), ElementData::Commutator); + ui->m_master_type_cb->addItem(tr("Module PLC"), ElementData::PLC); + ui->m_master_type_cb->setMinimumWidth(150); //Terminal option ui->m_terminal_type_cb->addItem(tr("Générique"), ElementData::TTGeneric); @@ -186,6 +229,38 @@ void ElementPropertiesEditorWidget::setUpInterface() } }); + // Connect master type combo box to show/hide PLC configuration + connect(ui->m_master_type_cb, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) { + auto master_type = ui->m_master_type_cb->itemData(index).value(); + if (master_type == ElementData::PLC) { + if (!m_plc_gb) { + createPlcConfigWidgets(); + } + m_plc_gb->setVisible(true); + ui->max_slaves_checkbox->setVisible(false); + ui->max_slaves_spinbox->setVisible(false); + ui->m_slave_groups_checkbox->setVisible(false); + ui->m_slave_groups_table->setVisible(false); + populatePlcTable(); + } else { + if (m_plc_gb) + m_plc_gb->setVisible(false); + ui->max_slaves_checkbox->setVisible(true); + ui->max_slaves_spinbox->setVisible(true); + ui->m_slave_groups_checkbox->setVisible(true); + ui->m_slave_groups_table->setVisible(true); + } + }); + + // When "Esclave PLC" is selected in state combo, disable type combo + connect(ui->m_state_cb, QOverload::of(&QComboBox::currentIndexChanged), [this](int index) { + bool is_plc = (ui->m_state_cb->itemData(index) == ElementData::PLCSlave); + ui->m_type_cb->setEnabled(!is_plc); + if (is_plc) { + ui->m_type_cb->setCurrentIndex(0); + } + }); + populateTree(); } @@ -231,6 +306,30 @@ void ElementPropertiesEditorWidget::updateTree() void ElementPropertiesEditorWidget::populateTree() { const auto keys = QETInformation::elementEditorElementInfoKeys(); + + // For PLC Slave: add PLC-specific info keys at the top + if (m_data.m_type == ElementData::Slave + && m_data.m_slave_type == ElementData::PLCSlave) + { + QStringList plc_keys = { + QETInformation::ELMT_PLC_TYPE, + QETInformation::ELMT_PLC_ADDRESS, + QETInformation::ELMT_PLC_FUNCTION, + QETInformation::ELMT_PLC_COMMENT, + QETInformation::ELMT_PLC_CROSSREF + }; + for (const QString &key : plc_keys) + { + QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_tree); + qtwi->setFlags(Qt::ItemIsEnabled | Qt::ItemIsEditable); + qtwi->setData(0, Qt::DisplayRole, + QETInformation::translatedInfoKey(key)); + qtwi->setData(0, Qt::UserRole, key); + qtwi->setText(1, m_data.m_informations.value(key).toString()); + qtwi->setForeground(0, QColor(0, 100, 180)); + } + } + for(const QString& key : keys) { QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_tree); @@ -254,21 +353,31 @@ void ElementPropertiesEditorWidget::on_m_buttonBox_accepted() if (m_data.m_type == ElementData::Slave) { - m_data.m_slave_state = ui->m_state_cb->currentData().value(); - m_data.m_slave_type = ui->m_type_cb->currentData().value(); + auto state_val = ui->m_state_cb->currentData(); + if (state_val == ElementData::PLCSlave) { + m_data.m_slave_type = ElementData::PLCSlave; + m_data.m_slave_state = ElementData::Other; + } else { + m_data.m_slave_state = state_val.value(); + m_data.m_slave_type = ui->m_type_cb->currentData().value(); + } m_data.m_contact_count = ui->m_number_ctc->value(); } else if (m_data.m_type == ElementData::Master) { m_data.m_master_type = ui->m_master_type_cb->currentData().value(); //If the checkbox is checked, save the number; otherwise, -1 (infinity) - if (ui->max_slaves_checkbox->isChecked()) { + if (ui->max_slaves_checkbox->isVisible() && ui->max_slaves_checkbox->isChecked()) { m_data.m_max_slaves = ui->max_slaves_spinbox->value(); } else { m_data.m_max_slaves = -1; } - readSlaveGroupsFromTable(); + if (m_data.m_master_type == ElementData::PLC) { + readPlcTable(); + } else { + readSlaveGroupsFromTable(); + } } else if (m_data.m_type == ElementData::Terminal) { @@ -532,3 +641,601 @@ void ElementPropertiesEditorWidget::readSlaveGroupsFromTable() m_data.m_slave_contact_groups.append(group); } } + +/** + * @brief ElementPropertiesEditorWidget::createPlcConfigWidgets + * Create the PLC configuration widgets programmatically and add them to m_master_gb + */ +void ElementPropertiesEditorWidget::createPlcConfigWidgets() +{ + // Create PLC group box + m_plc_gb = new QGroupBox(tr("Configuration PLC"), ui->m_master_gb); + auto *plc_layout = new QVBoxLayout(m_plc_gb); + + // Toolbar + auto *toolbar = new QHBoxLayout(); + auto *add_btn = new QPushButton(tr("+"), m_plc_gb); + auto *remove_btn = new QPushButton(tr("-"), m_plc_gb); + + add_btn->setMaximumWidth(30); + remove_btn->setMaximumWidth(30); + + toolbar->addStretch(); + toolbar->addWidget(add_btn); + toolbar->addWidget(remove_btn); + plc_layout->addLayout(toolbar); + + // Tables side by side: IO table (left) + Terminal table (right) + auto *tables_splitter = new QSplitter(Qt::Horizontal, m_plc_gb); + + // IO Table + m_plc_table = new QTableWidget(tables_splitter); + 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::Interactive); + m_plc_table->horizontalHeader()->setSectionsMovable(true); + m_plc_table->horizontalHeader()->setSectionsClickable(true); + m_plc_table->horizontalHeader()->resizeSection(0, 120); + m_plc_table->horizontalHeader()->resizeSection(1, 100); + m_plc_table->horizontalHeader()->resizeSection(2, 150); + m_plc_table->horizontalHeader()->resizeSection(3, 150); + m_plc_table->horizontalHeader()->resizeSection(4, 100); + m_plc_table->setSelectionBehavior(QAbstractItemView::SelectRows); + m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection); + m_plc_table->setMinimumHeight(200); + tables_splitter->addWidget(m_plc_table); + + // Terminal table (per IO: Nb + T1-T4) + m_plc_terminal_table = new QTableWidget(tables_splitter); + m_plc_terminal_table->setColumnCount(2); + m_plc_terminal_table->setHorizontalHeaderLabels({ + tr("Nb."), tr("T1") + }); + m_plc_terminal_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive); + m_plc_terminal_table->horizontalHeader()->resizeSection(0, 50); + m_plc_terminal_table->horizontalHeader()->resizeSection(1, 80); + m_plc_terminal_table->setSelectionBehavior(QAbstractItemView::SelectRows); + m_plc_terminal_table->setSelectionMode(QAbstractItemView::ExtendedSelection); + m_plc_terminal_table->setMinimumHeight(200); + tables_splitter->addWidget(m_plc_terminal_table); + + tables_splitter->setStretchFactor(0, 3); + tables_splitter->setStretchFactor(1, 1); + tables_splitter->setSizes({500, 150}); + + plc_layout->addWidget(tables_splitter); + + // Font settings + auto *font_layout = new QHBoxLayout(); + + m_plc_header_font_btn = new QPushButton(tr("Police des en-têtes"), m_plc_gb); + m_plc_header_font_btn->setToolTip(tr("Configurer la police des en-têtes de colonnes")); + connect(m_plc_header_font_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcSelectHeaderFont); + font_layout->addWidget(m_plc_header_font_btn); + + m_plc_cell_font_btn = new QPushButton(tr("Police du texte"), m_plc_gb); + m_plc_cell_font_btn->setToolTip(tr("Configurer la police du texte dans les cellules")); + connect(m_plc_cell_font_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcSelectCellFont); + font_layout->addWidget(m_plc_cell_font_btn); + + m_plc_show_headers_cb = new QCheckBox(tr("Afficher les en-têtes sur la feuille"), m_plc_gb); + m_plc_show_headers_cb->setToolTip(tr("Afficher ou masquer les en-têtes de colonnes du tableau PLC sur la feuille")); + m_plc_show_headers_cb->setChecked(true); + font_layout->addWidget(m_plc_show_headers_cb); + + font_layout->addStretch(); + plc_layout->addLayout(font_layout); + + // Display settings + auto *settings_layout = new QGridLayout(); + + for (int i = 0; i < 4; ++i) { + m_plc_break_checkboxes[i] = new QCheckBox( + tr("Saut %1 après:").arg(i + 1), m_plc_gb); + settings_layout->addWidget(m_plc_break_checkboxes[i], i, 0); + + m_plc_break_spinboxes[i] = new QSpinBox(m_plc_gb); + m_plc_break_spinboxes[i]->setMinimum(0); + m_plc_break_spinboxes[i]->setMaximum(128); + m_plc_break_spinboxes[i]->setSpecialValueText(tr("Aucun")); + m_plc_break_spinboxes[i]->setEnabled(false); + settings_layout->addWidget(m_plc_break_spinboxes[i], i, 1); + + connect(m_plc_break_checkboxes[i], &QCheckBox::toggled, + m_plc_break_spinboxes[i], &QSpinBox::setEnabled); + } + + settings_layout->addWidget(new QLabel(tr("H. ligne:"), m_plc_gb), 4, 0); + m_plc_row_height_spinbox = new QSpinBox(m_plc_gb); + m_plc_row_height_spinbox->setMinimum(4); + m_plc_row_height_spinbox->setMaximum(30); + m_plc_row_height_spinbox->setValue(8); + m_plc_row_height_spinbox->setSuffix(tr(" mm")); + settings_layout->addWidget(m_plc_row_height_spinbox, 4, 1); + + settings_layout->setColumnStretch(2, 1); + plc_layout->addLayout(settings_layout); + + // Column name, visibility and width + auto *col_layout = new QHBoxLayout(); + QStringList col_names = {tr("Type"), tr("Adresse"), tr("Fonction"), + tr("Commentaire"), tr("Réf.")}; + + for (int i = 0; i < 5; ++i) { + auto *col_widget = new QVBoxLayout(); + + auto *le = new QLineEdit(m_plc_gb); + le->setPlaceholderText(col_names.at(i)); + le->setToolTip(tr("Nom personnalisé de la colonne (vide = par défaut)")); + m_plc_col_name_edits.append(le); + col_widget->addWidget(le); + + auto *cb = new QCheckBox(tr("Visible"), m_plc_gb); + cb->setChecked(true); + m_plc_col_visibility_checkboxes.append(cb); + col_widget->addWidget(cb); + + auto *sb = new QSpinBox(m_plc_gb); + sb->setMinimum(10); + sb->setMaximum(200); + sb->setValue(40); + sb->setSuffix(tr(" mm")); + m_plc_col_width_spinboxes.append(sb); + col_widget->addWidget(sb); + + col_layout->addLayout(col_widget); + } + plc_layout->addLayout(col_layout); + + // Add to master group box + ui->m_master_gb->layout()->addWidget(m_plc_gb); + + // Connect signals + connect(add_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcAddRow); + connect(remove_btn, &QPushButton::clicked, this, &ElementPropertiesEditorWidget::plcRemoveRow); +} + +/** + * @brief ElementPropertiesEditorWidget::populatePlcTable + * Fill the PLC table from m_data + */ +void ElementPropertiesEditorWidget::populatePlcTable() +{ + if (!m_plc_table) + return; + + m_plc_table->setRowCount(0); + if (m_plc_terminal_table) + m_plc_terminal_table->setRowCount(0); + + const auto &plc_data = m_data.m_plc_master_data; + m_plc_table->setRowCount(plc_data.ios.size()); + if (m_plc_terminal_table) + m_plc_terminal_table->setRowCount(plc_data.ios.size()); + + for (int row = 0; row < plc_data.ios.size(); ++row) { + const auto &io = plc_data.ios.at(row); + + // --- IO Table --- + 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); + } + type_cb->setCurrentIndex(static_cast(io.type)); + m_plc_table->setCellWidget(row, 0, type_cb); + + m_plc_table->setItem(row, 1, new QTableWidgetItem(io.address)); + m_plc_table->setItem(row, 2, new QTableWidgetItem(io.functionText)); + m_plc_table->setItem(row, 3, new QTableWidgetItem(io.comment)); + + auto *crossref_item = new QTableWidgetItem(io.crossRef); + crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable); + m_plc_table->setItem(row, 4, crossref_item); + + // --- Terminal Table --- + if (m_plc_terminal_table) { + int tc = qMax(1, io.terminalCount); + + // Ensure enough columns + while (m_plc_terminal_table->columnCount() < tc + 1) + m_plc_terminal_table->insertColumn(m_plc_terminal_table->columnCount()); + + auto *tc_sb = new QSpinBox(m_plc_terminal_table); + tc_sb->setMinimum(1); + tc_sb->setMaximum(4); + tc_sb->setValue(tc); + m_plc_terminal_table->setCellWidget(row, 0, tc_sb); + connect(tc_sb, QOverload::of(&QSpinBox::valueChanged), + this, [this, row](int val) { plcTerminalCountChanged(row, val); }); + + for (int i = 0; i < m_plc_terminal_table->columnCount() - 1; ++i) { + QString val = (i < io.terminals.size()) ? io.terminals.at(i) : QString(); + auto *item = new QTableWidgetItem(val); + if (i >= tc) { + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + item->setBackground(QColor(230, 230, 230)); + } + m_plc_terminal_table->setItem(row, i + 1, item); + } + } + } + + // Load display settings + for (int i = 0; i < 4; ++i) { + int val = plc_data.breakPositions.value(i, 0); + bool enabled = (val > 0); + m_plc_break_checkboxes[i]->setChecked(enabled); + m_plc_break_spinboxes[i]->setValue(enabled ? val : 0); + } + m_plc_row_height_spinbox->setValue(static_cast(plc_data.rowHeight)); + + // Load fonts + m_plc_header_font = plc_data.headerFont; + m_plc_cell_font = plc_data.cellFont; + if (m_plc_header_font.family().isEmpty()) { + m_plc_header_font = QFont(m_plc_table->font()); + m_plc_header_font.setBold(true); + m_plc_header_font.setPointSize(8); + } + if (m_plc_cell_font.family().isEmpty()) { + m_plc_cell_font = QFont(m_plc_table->font()); + m_plc_cell_font.setPointSize(8); + } + m_plc_header_font_btn->setText(tr("Police des en-têtes: %1 %2pt") + .arg(m_plc_header_font.family()).arg(m_plc_header_font.pointSize())); + m_plc_cell_font_btn->setText(tr("Police du texte: %1 %2pt") + .arg(m_plc_cell_font.family()).arg(m_plc_cell_font.pointSize())); + m_plc_show_headers_cb->setChecked(plc_data.showHeaders); + + // UI columns 0-4 map to data model indices 0,1,2,3,4 + const int ui_to_data[] = {0, 1, 2, 3, 4}; + + for (int i = 0; i < 5; ++i) { + int di = ui_to_data[i]; + m_plc_col_visibility_checkboxes.at(i)->setChecked( + plc_data.colVisible.value(di, true)); + m_plc_col_width_spinboxes.at(i)->setValue( + static_cast(plc_data.colWidths.value(di, 40))); + + if (i < plc_data.columnNames.size()) + m_plc_col_name_edits.at(i)->setText(plc_data.columnNames.at(i)); + else + m_plc_col_name_edits.at(i)->clear(); + } + + // Restore column visual order (translate data model index → table col index) + QHeaderView *hdr = m_plc_table->horizontalHeader(); + if (!plc_data.columnOrder.isEmpty()) { + const int data_to_table[] = {0, 1, 2, 3, 4}; + for (int visual = 0; visual < plc_data.columnOrder.size(); ++visual) { + int data_idx = plc_data.columnOrder.at(visual); + if (data_idx < 0 || data_idx > 4) continue; + int logical = data_to_table[data_idx]; + if (logical < 0 || logical >= 5) continue; + if (hdr->visualIndex(logical) != visual) + hdr->moveSection(hdr->visualIndex(logical), visual); + } + } +} + +/** + * @brief ElementPropertiesEditorWidget::readPlcTable + * Read the PLC data from the table back into m_data + */ +void ElementPropertiesEditorWidget::readPlcTable() +{ + if (!m_plc_table) + return; + + ElementData::PlcMasterData plc_data; + + for (int row = 0; row < m_plc_table->rowCount(); ++row) { + ElementData::PlcIO io; + + auto *type_cb = qobject_cast(m_plc_table->cellWidget(row, 0)); + if (type_cb) + io.type = static_cast(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(); + + // Read terminal data from terminal table + if (m_plc_terminal_table && row < m_plc_terminal_table->rowCount()) { + auto *tc_sb = qobject_cast(m_plc_terminal_table->cellWidget(row, 0)); + if (tc_sb) { + io.terminalCount = tc_sb->value(); + for (int i = 0; i < io.terminalCount && i < 4; ++i) { + auto *term_item = m_plc_terminal_table->item(row, i + 1); + io.terminals.append(term_item ? term_item->text() : QString()); + } + } + } + + plc_data.ios.append(io); + } + + plc_data.rowHeight = m_plc_row_height_spinbox->value(); + + // Save break positions + for (int i = 0; i < 4; ++i) { + if (m_plc_break_checkboxes[i]->isChecked()) + plc_data.breakPositions.append(m_plc_break_spinboxes[i]->value()); + else + plc_data.breakPositions.append(0); + } + + // UI columns 0-4 map to data model indices 0,1,2,3,4 + const int ui_to_data[] = {0, 1, 2, 3, 4}; + + for (int i = 0; i < 5; ++i) { + int di = ui_to_data[i]; + plc_data.colVisible[di] = m_plc_col_visibility_checkboxes.at(i)->isChecked(); + plc_data.colWidths[di] = m_plc_col_width_spinboxes.at(i)->value(); + + QString name = m_plc_col_name_edits.at(i)->text().trimmed(); + if (!name.isEmpty()) + plc_data.columnNames.append(name); + else + plc_data.columnNames.append(QString()); + } + + // Save current column visual order (translate table col index → data model index) + QHeaderView *hdr = m_plc_table->horizontalHeader(); + const int table_to_data[] = {0, 1, 2, 3, 4}; + for (int visual = 0; visual < 5; ++visual) { + int table_col = hdr->logicalIndex(visual); + plc_data.columnOrder.append(table_to_data[table_col]); + } + + plc_data.headerFont = m_plc_header_font; + plc_data.cellFont = m_plc_cell_font; + plc_data.showHeaders = m_plc_show_headers_cb->isChecked(); + + m_data.setPlcMasterData(plc_data); +} + +/** + * @brief ElementPropertiesEditorWidget::plcAddRow + * Add a new empty row to the PLC IO table + */ +void ElementPropertiesEditorWidget::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); + + m_plc_table->setItem(row, 1, new QTableWidgetItem()); + m_plc_table->setItem(row, 2, new QTableWidgetItem()); + m_plc_table->setItem(row, 3, new QTableWidgetItem()); + + auto *crossref_item = new QTableWidgetItem(); + crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable); + m_plc_table->setItem(row, 4, crossref_item); + + // Terminal table row + if (m_plc_terminal_table) { + m_plc_terminal_table->insertRow(row); + + // Ensure enough columns (at least Nb + T1) + while (m_plc_terminal_table->columnCount() < 2) + m_plc_terminal_table->insertColumn(m_plc_terminal_table->columnCount()); + + auto *tc_sb = new QSpinBox(m_plc_terminal_table); + tc_sb->setMinimum(1); + tc_sb->setMaximum(4); + tc_sb->setValue(1); + m_plc_terminal_table->setCellWidget(row, 0, tc_sb); + connect(tc_sb, QOverload::of(&QSpinBox::valueChanged), + this, [this, row](int val) { plcTerminalCountChanged(row, val); }); + + // T1 editable, rest gray + auto *t1_item = new QTableWidgetItem(); + m_plc_terminal_table->setItem(row, 1, t1_item); + + for (int i = 2; i < m_plc_terminal_table->columnCount(); ++i) { + auto *item = new QTableWidgetItem(); + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + item->setBackground(QColor(230, 230, 230)); + m_plc_terminal_table->setItem(row, i, item); + } + } +} + +/** + * @brief ElementPropertiesEditorWidget::plcTerminalCountChanged + * Enable/disable T1-T4 cells and grow columns based on terminal count spinner for a row + */ +void ElementPropertiesEditorWidget::plcTerminalCountChanged(int row, int count) +{ + Q_UNUSED(row) + if (!m_plc_terminal_table) + return; + + // Find max terminal count across all rows + int max_tc = 1; + for (int r = 0; r < m_plc_terminal_table->rowCount(); ++r) { + auto *sb = qobject_cast(m_plc_terminal_table->cellWidget(r, 0)); + if (sb && sb->value() > max_tc) + max_tc = sb->value(); + } + + // Ensure enough columns + int needed = max_tc + 1; // +1 for Nb column + while (m_plc_terminal_table->columnCount() < needed) + m_plc_terminal_table->insertColumn(m_plc_terminal_table->columnCount()); + + // Rebuild headers + QStringList th; + th << tr("Nb."); + for (int i = 1; i < m_plc_terminal_table->columnCount(); ++i) + th << tr("T%1").arg(i); + m_plc_terminal_table->setHorizontalHeaderLabels(th); + + // Enable/disable cells per row + for (int r = 0; r < m_plc_terminal_table->rowCount(); ++r) { + auto *sb = qobject_cast(m_plc_terminal_table->cellWidget(r, 0)); + int tc = sb ? sb->value() : 1; + for (int c = 1; c < m_plc_terminal_table->columnCount(); ++c) { + auto *item = m_plc_terminal_table->item(r, c); + if (!item) { + item = new QTableWidgetItem(); + m_plc_terminal_table->setItem(r, c, item); + } + if (c <= tc) { + item->setFlags(item->flags() | Qt::ItemIsEditable); + item->setBackground(Qt::NoBrush); + } else { + item->setFlags(item->flags() & ~Qt::ItemIsEditable); + item->setText(QString()); + item->setBackground(QColor(230, 230, 230)); + } + } + } +} + +/** + * @brief ElementPropertiesEditorWidget::plcRemoveRow + * Remove selected rows from the PLC IO table + */ +void ElementPropertiesEditorWidget::plcRemoveRow() +{ + if (!m_plc_table) + return; + + QModelIndexList selected = m_plc_table->selectionModel()->selectedRows(); + if (selected.isEmpty()) + return; + + // Remove from bottom to top + 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()); + if (m_plc_terminal_table && idx.row() < m_plc_terminal_table->rowCount()) + m_plc_terminal_table->removeRow(idx.row()); + } +} + +/** + * @brief ElementPropertiesEditorWidget::plcPasteFromClipboard + * Paste IO data from clipboard (tab-separated, e.g. from Excel) + */ +void ElementPropertiesEditorWidget::plcPasteFromClipboard() +{ + if (!m_plc_table) + return; + + QString clipboard_text = QApplication::clipboard()->text(); + if (clipboard_text.isEmpty()) + return; + + QStringList lines = clipboard_text.split('\n', Qt::SkipEmptyParts); + + int start_row = m_plc_table->rowCount(); + m_plc_table->setRowCount(start_row + lines.size()); + + for (int i = 0; i < lines.size(); ++i) { + QStringList cells = lines.at(i).split('\t'); + int row = start_row + i; + + // 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); + } + + // Try to match type from clipboard + if (!cells.isEmpty()) { + QString type_str = cells.at(0).trimmed(); + int type_idx = -1; + for (int t = 0; t < plc_types.size(); ++t) { + if (plc_types.at(t).compare(type_str, Qt::CaseInsensitive) == 0) { + type_idx = t; + break; + } + } + if (type_idx >= 0) + type_cb->setCurrentIndex(type_idx); + } + m_plc_table->setCellWidget(row, 0, type_cb); + + // Address + m_plc_table->setItem(row, 1, new QTableWidgetItem( + cells.size() > 1 ? cells.at(1).trimmed() : QString())); + + // Function text + m_plc_table->setItem(row, 2, new QTableWidgetItem( + cells.size() > 2 ? cells.at(2).trimmed() : QString())); + + // Comment + m_plc_table->setItem(row, 3, new QTableWidgetItem( + cells.size() > 3 ? cells.at(3).trimmed() : QString())); + + // CrossRef (read-only) + auto *crossref_item = new QTableWidgetItem( + cells.size() > 4 ? cells.at(4).trimmed() : QString()); + crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable); + m_plc_table->setItem(row, 4, crossref_item); + } +} + +/** + * @brief ElementPropertiesEditorWidget::plcSelectHeaderFont + * Open font dialog to select column header font + */ +void ElementPropertiesEditorWidget::plcSelectHeaderFont() +{ + bool ok; + QFont font = QFontDialog::getFont(&ok, m_plc_header_font, this, + tr("Police des en-têtes de colonnes")); + if (ok) { + m_plc_header_font = font; + m_plc_header_font_btn->setText(tr("Police des en-têtes: %1 %2pt") + .arg(font.family()).arg(font.pointSize())); + } +} + +/** + * @brief ElementPropertiesEditorWidget::plcSelectCellFont + * Open font dialog to select cell text font + */ +void ElementPropertiesEditorWidget::plcSelectCellFont() +{ + bool ok; + QFont font = QFontDialog::getFont(&ok, m_plc_cell_font, this, + tr("Police du texte des cellules")); + if (ok) { + m_plc_cell_font = font; + m_plc_cell_font_btn->setText(tr("Police du texte: %1 %2pt") + .arg(font.family()).arg(font.pointSize())); + } +} diff --git a/sources/editor/ui/elementpropertieseditorwidget.h b/sources/editor/ui/elementpropertieseditorwidget.h index 46f0b1e51..b283a061e 100644 --- a/sources/editor/ui/elementpropertieseditorwidget.h +++ b/sources/editor/ui/elementpropertieseditorwidget.h @@ -24,6 +24,13 @@ #include #include +class QTableWidget; +class QSpinBox; +class QCheckBox; +class QGroupBox; +class QPushButton; +class QLineEdit; + namespace Ui { class ElementPropertiesEditorWidget; } @@ -51,6 +58,9 @@ class ElementPropertiesEditorWidget : public QDialog void populateTree(); void populateSlaveGroupsTable(); void readSlaveGroupsFromTable(); + void createPlcConfigWidgets(); + void populatePlcTable(); + void readPlcTable(); //SLOTS private slots: @@ -58,11 +68,33 @@ class ElementPropertiesEditorWidget : public QDialog void on_m_base_type_cb_currentIndexChanged(int index); void on_m_slave_groups_checkbox_toggled(bool checked); void on_max_slaves_checkbox_toggled(bool checked); + void plcAddRow(); + void plcRemoveRow(); + void plcPasteFromClipboard(); + void plcTerminalCountChanged(int row, int count); + void plcSelectHeaderFont(); + void plcSelectCellFont(); //ATTRIBUTES private: Ui::ElementPropertiesEditorWidget *ui; ElementData m_data; + + // PLC configuration widgets (created programmatically) + QGroupBox *m_plc_gb = nullptr; + QTableWidget *m_plc_table = nullptr; + QTableWidget *m_plc_terminal_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; + QPushButton *m_plc_header_font_btn = nullptr; + QPushButton *m_plc_cell_font_btn = nullptr; + QCheckBox *m_plc_show_headers_cb = nullptr; + QFont m_plc_header_font; + QFont m_plc_cell_font; + QList m_plc_col_visibility_checkboxes; + QList m_plc_col_width_spinboxes; + QList m_plc_col_name_edits; }; #endif // ELEMENTPROPERTIESEDITORWIDGET_H diff --git a/sources/editor/ui/elementpropertieseditorwidget.ui b/sources/editor/ui/elementpropertieseditorwidget.ui index ed2ae55c0..904faa2d2 100644 --- a/sources/editor/ui/elementpropertieseditorwidget.ui +++ b/sources/editor/ui/elementpropertieseditorwidget.ui @@ -93,17 +93,17 @@ Élément maître - - - - - Type concret - - - - - - + + + + + Type concret + + + + + + diff --git a/sources/factory/elementpicturefactory.cpp b/sources/factory/elementpicturefactory.cpp index 1f8af820b..764ea7a8c 100644 --- a/sources/factory/elementpicturefactory.cpp +++ b/sources/factory/elementpicturefactory.cpp @@ -19,6 +19,7 @@ #include "../ElementsCollection/elementslocation.h" #include "../editor/graphicspart/partline.h" +#include "../properties/elementdata.h" #include "../qetapp.h" #include "../qetversion.h" @@ -30,6 +31,7 @@ #include #include #include +#include ElementPictureFactory* ElementPictureFactory::m_factory = nullptr; @@ -206,7 +208,25 @@ bool ElementPictureFactory::build(const ElementsLocation &location, tmp.setCosmetic(true); low_painter.setPen(tmp); - //scroll of the Children of the Definition: Parts of the Drawing + //scroll of the Children of the Definition: Parts of the Drawing + // Extract PLC master data for rendering plc_table parts + QDomElement plc_master_data; + for (QDomNode node = dom.firstChild() ; !node.isNull() ; node = node.nextSibling()) + { + QDomElement elmts = node.toElement(); + if (elmts.isNull()) continue; + if (elmts.tagName() == "kindInformations") { + for (QDomNode ki = elmts.firstChild(); !ki.isNull(); ki = ki.nextSibling()) { + QDomElement ki_el = ki.toElement(); + if (!ki_el.isNull() && ki_el.tagName() == "plcMasterData") { + plc_master_data = ki_el; + break; + } + } + break; + } + } + for (QDomNode node = dom.firstChild() ; !node.isNull() ; node = node.nextSibling()) { QDomElement elmts = node.toElement(); @@ -223,9 +243,17 @@ bool ElementPictureFactory::build(const ElementsLocation &location, if (qde.isNull()) { continue; } - parseElement(qde, painter, primitives_); - primitives fake_prim; - parseElement(qde, low_painter, fake_prim); + if (qde.tagName() == "plc_table") { + // Skip - PLC table is rendered at runtime by + // Element::drawPlcTable() from live ElementData. + // Rendering into the cached QPicture here causes + // intermittent crashes when drawPicture() replays + // complex font/text operations. + } else { + parseElement(qde, painter, primitives_); + primitives fake_prim; + parseElement(qde, low_painter, fake_prim); + } } } } @@ -1088,3 +1116,217 @@ void ElementPictureFactory::setPainterStyle(const QDomElement &dom, QPainter &pa painter.setPen(pen); painter.setBrush(brush); } + +void ElementPictureFactory::parsePlcTable(const QDomElement &dom, const QDomElement &plc_data, QPainter &painter) const +{ + qreal pos_x = dom.attribute("x", "0").toDouble(); + qreal pos_y = dom.attribute("y", "0").toDouble(); + + if (plc_data.isNull()) return; + + const int COL_COUNT = 5; + const int COL_TYPE = 0; + const int COL_ADDRESS = 1; + const int COL_FUNCTION = 2; + const int COL_COMMENT = 3; + const int COL_CROSSREF = 4; + + qreal rowHeight = plc_data.attribute("rowHeight", "8.0").toDouble(); + + // Parse showHeaders + bool showHeaders = true; + auto xml_sh = plc_data.firstChildElement("showHeaders"); + if (!xml_sh.isNull()) + showHeaders = (xml_sh.text().trimmed() != "0"); + + qreal header_h = showHeaders ? (rowHeight + 2.0) : 0; + + // Parse column widths + QMap col_widths; + auto xml_cw = plc_data.firstChildElement("columnWidths"); + for (auto xml_col = xml_cw.firstChildElement("column"); !xml_col.isNull(); + xml_col = xml_col.nextSiblingElement("column")) { + int idx = xml_col.attribute("index").toInt(); + qreal w = xml_col.attribute("width").toDouble(); + if (w > 0) col_widths[idx] = w; + } + if (col_widths.isEmpty()) { + col_widths[0] = 35; col_widths[1] = 25; col_widths[2] = 50; + col_widths[3] = 40; col_widths[4] = 30; + } + + // Parse column visibility + QMap col_visible; + auto xml_cv = plc_data.firstChildElement("columnVisibility"); + for (auto xml_c = xml_cv.firstChildElement("column"); !xml_c.isNull(); + xml_c = xml_c.nextSiblingElement("column")) { + int idx = xml_c.attribute("index").toInt(); + QString vis = xml_c.attribute("visible", "true"); + col_visible[idx] = (vis == "true"); + } + + // Parse column order + QList column_order; + auto xml_ord = plc_data.firstChildElement("columnOrder"); + if (!xml_ord.isNull()) { + QStringList parts = xml_ord.text().split(",", Qt::SkipEmptyParts); + for (const QString &s : parts) { + int idx = s.trimmed().toInt(); + if (idx >= 0 && idx < COL_COUNT) + column_order.append(idx); + } + } + + // Parse custom column names + QMap column_names; + auto xml_names = plc_data.firstChildElement("columnNames"); + for (auto xml_n = xml_names.firstChildElement("column"); !xml_n.isNull(); + xml_n = xml_n.nextSiblingElement("column")) { + int idx = xml_n.attribute("index").toInt(); + QString name = xml_n.text().trimmed(); + if (!name.isEmpty()) + column_names[idx] = name; + } + + // Parse fonts + QFont header_font; + header_font.setPointSize(7); + header_font.setBold(true); + auto xml_hfont = plc_data.firstChildElement("headerFont"); + if (!xml_hfont.isNull()) { + if (!xml_hfont.attribute("family").isEmpty()) + header_font.setFamily(xml_hfont.attribute("family")); + if (xml_hfont.attribute("size").toInt() > 0) + header_font.setPointSize(xml_hfont.attribute("size").toInt()); + header_font.setBold(xml_hfont.attribute("bold") == "true"); + } + + QFont cell_font; + cell_font.setPointSize(7); + auto xml_cfont = plc_data.firstChildElement("cellFont"); + if (!xml_cfont.isNull()) { + if (!xml_cfont.attribute("family").isEmpty()) + cell_font.setFamily(xml_cfont.attribute("family")); + if (xml_cfont.attribute("size").toInt() > 0) + cell_font.setPointSize(xml_cfont.attribute("size").toInt()); + cell_font.setBold(xml_cfont.attribute("bold") == "true"); + } + + // Parse IOs + QVector> ios; + auto xml_ios = plc_data.firstChildElement("plcIOs"); + for (auto xml_io = xml_ios.firstChildElement("plcIO"); !xml_io.isNull(); + xml_io = xml_io.nextSiblingElement("plcIO")) { + QMap io_data; + io_data[COL_TYPE] = ElementData::translatedPlcIOType( + ElementData::plcIOTypeFromString(xml_io.attribute("type"))); + io_data[COL_ADDRESS] = xml_io.attribute("address"); + io_data[COL_FUNCTION] = xml_io.attribute("functionText"); + io_data[COL_COMMENT] = xml_io.attribute("comment"); + io_data[COL_CROSSREF] = xml_io.attribute("crossRef"); + ios.append(io_data); + } + + // Build visible columns (respect column order) + QList visible_cols; + if (!column_order.isEmpty()) { + for (int logical : column_order) { + if (logical >= 0 && logical < COL_COUNT + && col_visible.value(logical, true) + && !visible_cols.contains(logical)) + visible_cols.append(logical); + } + for (int i = 0; i < COL_COUNT; ++i) { + if (col_visible.value(i, true) && !visible_cols.contains(i)) + visible_cols.append(i); + } + } else { + for (int i = 0; i < COL_COUNT; ++i) { + if (col_visible.value(i, true)) + visible_cols.append(i); + } + } + if (visible_cols.isEmpty()) + visible_cols << COL_TYPE << COL_ADDRESS << COL_FUNCTION; + + // Build header labels + QMap headers; + headers[COL_TYPE] = QObject::tr("Type"); + headers[COL_ADDRESS] = QObject::tr("Adresse"); + headers[COL_FUNCTION] = QObject::tr("Fonction"); + headers[COL_COMMENT] = QObject::tr("Commentaire"); + headers[COL_CROSSREF] = QObject::tr("Réf. croisée"); + for (auto it = column_names.constBegin(); it != column_names.constEnd(); ++it) + headers[it.key()] = it.value(); + + // Parse break positions + QList breaks; + auto xml_breaks = plc_data.firstChildElement("breakPositions"); + for (auto xml_bp = xml_breaks.firstChildElement("break"); !xml_bp.isNull(); + xml_bp = xml_bp.nextSiblingElement("break")) { + int bp = xml_bp.text().toInt(); + if (bp > 0 && bp < ios.size() && !breaks.contains(bp)) + breaks.append(bp); + } + std::sort(breaks.begin(), breaks.end()); + + QList block_starts; + block_starts.append(0); + for (int bp : breaks) + block_starts.append(bp); + int block_count = block_starts.size(); + + qreal total_width = 0; + for (int col : visible_cols) + total_width += col_widths.value(col, 30); + + QPen border_pen(Qt::black, 0.5); + + int total_ios = ios.size(); + + for (int block = 0; block < block_count; ++block) { + qreal bx = pos_x + block * (total_width + 3); + qreal cx = bx; + + // Draw headers + if (showHeaders) { + painter.fillRect(QRectF(cx, pos_y, total_width, header_h), QColor(220, 220, 220)); + painter.setPen(border_pen); + painter.setBrush(Qt::NoBrush); + painter.setFont(header_font); + for (int col : visible_cols) { + QRectF hr(cx, pos_y, col_widths.value(col, 30), header_h); + painter.drawRect(hr); + QRectF text_rect = hr.adjusted(1, 0, -1, 0); + painter.drawText(text_rect, Qt::AlignCenter, headers.value(col, QString())); + cx += col_widths.value(col, 30); + } + } + + // Draw IO rows + painter.setFont(cell_font); + int start_idx = block_starts.at(block); + int end_idx = (block + 1 < block_starts.size()) ? block_starts.at(block + 1) : total_ios; + + for (int row = 0; row < (end_idx - start_idx); ++row) { + int io_idx = start_idx + row; + if (io_idx >= ios.size()) break; + const auto &io = ios.at(io_idx); + qreal ry = pos_y + header_h + row * rowHeight; + cx = bx; + + for (int col : visible_cols) { + QRectF cr(cx, ry, col_widths.value(col, 30), rowHeight); + painter.setPen(border_pen); + painter.setBrush(Qt::NoBrush); + painter.drawRect(cr); + + QString cell_text = io.value(col, QString()); + QRectF text_rect = cr.adjusted(1, 0, -1, 0); + painter.drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, cell_text); + + cx += col_widths.value(col, 30); + } + } + } +} diff --git a/sources/factory/elementpicturefactory.h b/sources/factory/elementpicturefactory.h index f63b162ee..63536a38a 100644 --- a/sources/factory/elementpicturefactory.h +++ b/sources/factory/elementpicturefactory.h @@ -101,6 +101,7 @@ class ElementPictureFactory void parseArc (const QDomElement &dom, QPainter &painter, primitives &prim) const; void parsePolygon(const QDomElement &dom, QPainter &painter, primitives &prim) const; void parseText (const QDomElement &dom, QPainter &painter, primitives &prim) const; + void parsePlcTable(const QDomElement &dom, const QDomElement &plc_data, QPainter &painter) const; void setPainterStyle(const QDomElement &dom, QPainter &painter) const; QHash m_pictures_H; diff --git a/sources/properties/elementdata.cpp b/sources/properties/elementdata.cpp index 53e653f4d..becb7a79e 100644 --- a/sources/properties/elementdata.cpp +++ b/sources/properties/elementdata.cpp @@ -112,6 +112,115 @@ QDomElement ElementData::kindInfoToXml(QDomDocument &document) } returned_elmt.appendChild(xml_groups); } + + // Save PLC master data if type is PLC + if (m_master_type == ElementData::PLC) { + auto xml_plc = document.createElement(QStringLiteral("plcMasterData")); + xml_plc.setAttribute(QStringLiteral("rowHeight"), + QString::number(m_plc_master_data.rowHeight, 'f', 2)); + + // Save break positions + { + auto xml_breaks = document.createElement(QStringLiteral("breakPositions")); + for (int bp : m_plc_master_data.breakPositions) { + auto xml_bp = document.createElement(QStringLiteral("break")); + xml_bp.appendChild(document.createTextNode(QString::number(bp))); + xml_breaks.appendChild(xml_bp); + } + xml_plc.appendChild(xml_breaks); + } + + // Save column widths + auto xml_col_widths = document.createElement(QStringLiteral("columnWidths")); + for (auto it = m_plc_master_data.colWidths.constBegin(); + it != m_plc_master_data.colWidths.constEnd(); ++it) { + auto xml_col = document.createElement(QStringLiteral("column")); + xml_col.setAttribute(QStringLiteral("index"), it.key()); + xml_col.setAttribute(QStringLiteral("width"), QString::number(it.value(), 'f', 2)); + xml_col_widths.appendChild(xml_col); + } + xml_plc.appendChild(xml_col_widths); + + // Save column visibility + auto xml_col_vis = document.createElement(QStringLiteral("columnVisibility")); + for (auto it = m_plc_master_data.colVisible.constBegin(); + it != m_plc_master_data.colVisible.constEnd(); ++it) { + auto xml_col = document.createElement(QStringLiteral("column")); + xml_col.setAttribute(QStringLiteral("index"), it.key()); + xml_col.setAttribute(QStringLiteral("visible"), it.value() ? "true" : "false"); + xml_col_vis.appendChild(xml_col); + } + xml_plc.appendChild(xml_col_vis); + + // Save fonts + if (!m_plc_master_data.headerFont.family().isEmpty()) { + auto xml_hfont = document.createElement(QStringLiteral("headerFont")); + xml_hfont.setAttribute(QStringLiteral("family"), m_plc_master_data.headerFont.family()); + xml_hfont.setAttribute(QStringLiteral("size"), m_plc_master_data.headerFont.pointSize()); + xml_hfont.setAttribute(QStringLiteral("bold"), m_plc_master_data.headerFont.bold() ? "true" : "false"); + xml_plc.appendChild(xml_hfont); + } + if (!m_plc_master_data.cellFont.family().isEmpty()) { + auto xml_cfont = document.createElement(QStringLiteral("cellFont")); + xml_cfont.setAttribute(QStringLiteral("family"), m_plc_master_data.cellFont.family()); + xml_cfont.setAttribute(QStringLiteral("size"), m_plc_master_data.cellFont.pointSize()); + xml_cfont.setAttribute(QStringLiteral("bold"), m_plc_master_data.cellFont.bold() ? "true" : "false"); + xml_plc.appendChild(xml_cfont); + } + + // Save custom column names + if (!m_plc_master_data.columnNames.isEmpty()) { + auto xml_names = document.createElement(QStringLiteral("columnNames")); + for (int i = 0; i < m_plc_master_data.columnNames.size(); ++i) { + auto xml_name = document.createElement(QStringLiteral("column")); + xml_name.setAttribute(QStringLiteral("index"), i); + xml_name.appendChild(document.createTextNode(m_plc_master_data.columnNames.at(i))); + xml_names.appendChild(xml_name); + } + xml_plc.appendChild(xml_names); + } + + // Save column order + if (!m_plc_master_data.columnOrder.isEmpty()) { + auto xml_order = document.createElement(QStringLiteral("columnOrder")); + QString order_str; + for (int i = 0; i < m_plc_master_data.columnOrder.size(); ++i) { + if (i > 0) order_str += QStringLiteral(","); + order_str += QString::number(m_plc_master_data.columnOrder.at(i)); + } + xml_order.appendChild(document.createTextNode(order_str)); + xml_plc.appendChild(xml_order); + } + + // Save showHeaders + { + auto xml_sh = document.createElement(QStringLiteral("showHeaders")); + xml_sh.appendChild(document.createTextNode( + m_plc_master_data.showHeaders ? QStringLiteral("1") : QStringLiteral("0"))); + xml_plc.appendChild(xml_sh); + } + + // Save IO entries + auto xml_ios = document.createElement(QStringLiteral("plcIOs")); + for (const auto &io : m_plc_master_data.ios) { + auto xml_io = document.createElement(QStringLiteral("plcIO")); + xml_io.setAttribute(QStringLiteral("type"), plcIOTypeToString(io.type)); + xml_io.setAttribute(QStringLiteral("address"), io.address); + xml_io.setAttribute(QStringLiteral("functionText"), io.functionText); + xml_io.setAttribute(QStringLiteral("comment"), io.comment); + xml_io.setAttribute(QStringLiteral("crossRef"), io.crossRef); + xml_io.setAttribute(QStringLiteral("terminalCount"), io.terminalCount); + for (const auto &t : io.terminals) { + auto xml_term = document.createElement(QStringLiteral("terminal")); + xml_term.appendChild(document.createTextNode(t)); + xml_io.appendChild(xml_term); + } + xml_ios.appendChild(xml_io); + } + xml_plc.appendChild(xml_ios); + + returned_elmt.appendChild(xml_plc); + } } else if (m_type == ElementData::Slave) { @@ -270,6 +379,11 @@ bool ElementData::operator==(const ElementData &data) const if (data.m_slave_contact_groups != m_slave_contact_groups) { return false; } + if (data.m_master_type == ElementData::PLC) { + if (data.m_plc_master_data != m_plc_master_data) { + return false; + } + } } else if (data.m_type == ElementData::Slave) { if (data.m_slave_state != m_slave_state || @@ -410,6 +524,8 @@ QString ElementData::masterTypeToString(ElementData::MasterType type) return QStringLiteral("protection"); case ElementData::Commutator: return QStringLiteral("commutator"); + case ElementData::PLC: + return QStringLiteral("plc"); } return QStringLiteral("coil"); } @@ -422,6 +538,8 @@ ElementData::MasterType ElementData::masterTypeFromString(const QString &string) return ElementData::Protection; } else if (string == QLatin1String("commutator")) { return ElementData::Commutator; + } else if (string == QLatin1String("plc")) { + return ElementData::PLC; } qDebug() << "ElementData::masterTypeFromString : string " @@ -443,6 +561,8 @@ QString ElementData::slaveTypeToString(ElementData::SlaveType type) return QStringLiteral("delayOff"); case ElementData::delayOnOff: return QStringLiteral("delayOnOff"); + case ElementData::PLCSlave: + return QStringLiteral("plc"); } return QStringLiteral("simple"); } @@ -459,6 +579,8 @@ ElementData::SlaveType ElementData::slaveTypeFromString(const QString &string) return ElementData::DelayOff; } else if (string == QLatin1String("delayOnOff")) { return ElementData::delayOnOff; + } else if (string == QLatin1String("plc")) { + return ElementData::PLCSlave; } qDebug() << "ElementData::slaveTypeFromSting : string " @@ -658,6 +780,102 @@ void ElementData::kindInfoFromXml(const QDomElement &xml_element) m_slave_contact_groups.append(group); } } + + // Parse PLC master data + auto xml_plc = xml_kind.firstChildElement(QStringLiteral("plcMasterData")); + if (!xml_plc.isNull()) { + m_plc_master_data.rowHeight = xml_plc.attribute( + QStringLiteral("rowHeight"), QStringLiteral("8.00")).toDouble(); + + // Parse break positions (new format: child elements) + auto xml_breaks = xml_plc.firstChildElement(QStringLiteral("breakPositions")); + if (!xml_breaks.isNull()) { + for (const auto &xml_bp : QETXML::findInDomElement(xml_breaks, QStringLiteral("break"))) { + m_plc_master_data.breakPositions.append(xml_bp.text().toInt()); + } + } else { + // Backward compat: old format was breakAfter attribute + int old_break = xml_plc.attribute( + QStringLiteral("breakAfter"), QStringLiteral("0")).toInt(); + m_plc_master_data.breakPositions.append(old_break > 0 ? old_break : 0); + } + + // Parse column widths + auto xml_col_widths = xml_plc.firstChildElement(QStringLiteral("columnWidths")); + for (const auto &xml_col : QETXML::findInDomElement(xml_col_widths, QStringLiteral("column"))) { + int index = xml_col.attribute(QStringLiteral("index")).toInt(); + qreal width = xml_col.attribute(QStringLiteral("width")).toDouble(); + m_plc_master_data.colWidths[index] = width; + } + + // Parse column visibility + auto xml_col_vis = xml_plc.firstChildElement(QStringLiteral("columnVisibility")); + for (const auto &xml_col : QETXML::findInDomElement(xml_col_vis, QStringLiteral("column"))) { + int index = xml_col.attribute(QStringLiteral("index")).toInt(); + bool visible = xml_col.attribute(QStringLiteral("visible")) == QLatin1String("true"); + m_plc_master_data.colVisible[index] = visible; + } + + // Parse fonts + auto xml_hfont = xml_plc.firstChildElement(QStringLiteral("headerFont")); + if (!xml_hfont.isNull()) { + m_plc_master_data.headerFont.setFamily(xml_hfont.attribute(QStringLiteral("family"))); + m_plc_master_data.headerFont.setPointSize(xml_hfont.attribute(QStringLiteral("size"), QStringLiteral("8")).toInt()); + m_plc_master_data.headerFont.setBold(xml_hfont.attribute(QStringLiteral("bold")) == QLatin1String("true")); + } + auto xml_cfont = xml_plc.firstChildElement(QStringLiteral("cellFont")); + if (!xml_cfont.isNull()) { + m_plc_master_data.cellFont.setFamily(xml_cfont.attribute(QStringLiteral("family"))); + m_plc_master_data.cellFont.setPointSize(xml_cfont.attribute(QStringLiteral("size"), QStringLiteral("8")).toInt()); + m_plc_master_data.cellFont.setBold(xml_cfont.attribute(QStringLiteral("bold")) == QLatin1String("true")); + } + + // Parse custom column names + auto xml_names = xml_plc.firstChildElement(QStringLiteral("columnNames")); + if (!xml_names.isNull()) { + for (const auto &xml_col : QETXML::findInDomElement(xml_names, QStringLiteral("column"))) { + int index = xml_col.attribute(QStringLiteral("index")).toInt(); + while (m_plc_master_data.columnNames.size() <= index) + m_plc_master_data.columnNames.append(QString()); + m_plc_master_data.columnNames[index] = xml_col.text(); + } + } + + // Parse column order + auto xml_order = xml_plc.firstChildElement(QStringLiteral("columnOrder")); + if (!xml_order.isNull()) { + QStringList parts = xml_order.text().split(','); + for (const auto &p : parts) { + bool ok; + int val = p.trimmed().toInt(&ok); + if (ok) + m_plc_master_data.columnOrder.append(val); + } + } + + // Parse showHeaders + auto xml_sh = xml_plc.firstChildElement(QStringLiteral("showHeaders")); + if (!xml_sh.isNull()) + m_plc_master_data.showHeaders = (xml_sh.text().trimmed() != QStringLiteral("0")); + + // Parse IO entries + auto xml_ios = xml_plc.firstChildElement(QStringLiteral("plcIOs")); + for (const auto &xml_io : QETXML::findInDomElement(xml_ios, QStringLiteral("plcIO"))) { + PlcIO io; + io.type = plcIOTypeFromString( + xml_io.attribute(QStringLiteral("type"), QStringLiteral("entree_digitale"))); + io.address = xml_io.attribute(QStringLiteral("address")); + io.functionText = xml_io.attribute(QStringLiteral("functionText")); + io.comment = xml_io.attribute(QStringLiteral("comment")); + io.crossRef = xml_io.attribute(QStringLiteral("crossRef")); + io.terminalCount = xml_io.attribute( + QStringLiteral("terminalCount"), QStringLiteral("0")).toInt(); + for (const auto &xml_term : QETXML::findInDomElement(xml_io, QStringLiteral("terminal"))) { + io.terminals.append(xml_term.text()); + } + m_plc_master_data.ios.append(io); + } + } } } @@ -697,3 +915,90 @@ ElementData::SlaveType ElementData::slaveContactGroupSubtypeFromString(const QSt { return slaveTypeFromString(string); } + +/** + * @brief ElementData::plcIOTypeToString + * Convert a PlcIOType to string for XML storage. + */ +QString ElementData::plcIOTypeToString(PlcIOType type) +{ + switch (type) { + case EntreeDigitale: + return QStringLiteral("entree_digitale"); + case SortieDigitale: + return QStringLiteral("sortie_digitale"); + case EntreeAnalogique: + return QStringLiteral("entree_analogique"); + case SortieAnalogique: + return QStringLiteral("sortie_analogique"); + case EntreeUniverselle: + return QStringLiteral("entree_universelle"); + case SortieUniverselle: + return QStringLiteral("sortie_universelle"); + } + return QStringLiteral("entree_digitale"); +} + +/** + * @brief ElementData::plcIOTypeFromString + * Convert a string from XML to PlcIOType. + */ +ElementData::PlcIOType ElementData::plcIOTypeFromString(const QString &string) +{ + if (string == QLatin1String("entree_digitale")) { + return ElementData::EntreeDigitale; + } else if (string == QLatin1String("sortie_digitale")) { + return ElementData::SortieDigitale; + } else if (string == QLatin1String("entree_analogique")) { + return ElementData::EntreeAnalogique; + } else if (string == QLatin1String("sortie_analogique")) { + return ElementData::SortieAnalogique; + } else if (string == QLatin1String("entree_universelle")) { + return ElementData::EntreeUniverselle; + } else if (string == QLatin1String("sortie_universelle")) { + return ElementData::SortieUniverselle; + } + + qDebug() << "ElementData::plcIOTypeFromString : string " + << string + << " don't exist, return failsafe value 'entree_digitale'"; + return ElementData::EntreeDigitale; +} + +/** + * @brief ElementData::translatedPlcIOType + * Return translated string for PlcIOType. + */ +QString ElementData::translatedPlcIOType(PlcIOType type) +{ + switch (type) { + case EntreeDigitale: + return QObject::tr("Entrée digitale"); + case SortieDigitale: + return QObject::tr("Sortie digitale"); + case EntreeAnalogique: + return QObject::tr("Entrée analogique"); + case SortieAnalogique: + return QObject::tr("Sortie analogique"); + case EntreeUniverselle: + return QObject::tr("Entrée universelle"); + case SortieUniverselle: + return QObject::tr("Sortie universelle"); + } + return QObject::tr("Entrée digitale"); +} + +/** + * @brief ElementData::plcIOTypeList + * Return list of all translated PLC IO type strings (for combo boxes). + */ +QStringList ElementData::plcIOTypeList() +{ + return QStringList() + << translatedPlcIOType(EntreeDigitale) + << translatedPlcIOType(SortieDigitale) + << translatedPlcIOType(EntreeAnalogique) + << translatedPlcIOType(SortieAnalogique) + << translatedPlcIOType(EntreeUniverselle) + << translatedPlcIOType(SortieUniverselle); +} diff --git a/sources/properties/elementdata.h b/sources/properties/elementdata.h index 4fa264ab4..6a66f387a 100644 --- a/sources/properties/elementdata.h +++ b/sources/properties/elementdata.h @@ -24,6 +24,7 @@ #include #include +#include /** * @brief The ElementData class @@ -52,7 +53,8 @@ class ElementData : public PropertiesInterface enum MasterType { Coil, Protection, - Commutator + Commutator, + PLC }; Q_ENUM(MasterType) @@ -61,7 +63,8 @@ class ElementData : public PropertiesInterface Power, DelayOn, DelayOff, - delayOnOff + delayOnOff, + PLCSlave }; Q_ENUM(SlaveType) @@ -73,6 +76,82 @@ class ElementData : public PropertiesInterface }; Q_ENUM(SlaveState) + enum PlcIOType { + EntreeDigitale, + SortieDigitale, + EntreeAnalogique, + SortieAnalogique, + EntreeUniverselle, + SortieUniverselle + }; + Q_ENUM(PlcIOType) + + /** + * @brief The PlcIO struct + * Represents a single IO entry in a PLC master element. + */ + struct PlcIO { + PlcIOType type = EntreeDigitale; + QString address; ///< e.g. "1.0", "1.1" + QString functionText; ///< e.g. "Motor K1" + QString comment; ///< Optional comment + QString crossRef; ///< Auto-filled: slave reference + int terminalCount = 1; ///< Number of terminals for this IO (1-4) + QStringList terminals; ///< Terminal values T1, T2, ... (size = terminalCount) + + bool operator==(const PlcIO &other) const { + return type == other.type + && address == other.address + && functionText == other.functionText + && comment == other.comment + && crossRef == other.crossRef + && terminalCount == other.terminalCount + && terminals == other.terminals; + } + bool operator!=(const PlcIO &other) const { + return !(*this == other); + } + }; + + /** + * @brief The PlcMasterData struct + * Stores all PLC master configuration: IO table, display settings, column visibility. + * All data is stored in the .elmt file for easy sharing between users. + */ + struct PlcMasterData { + QVector ios; ///< All IO entries + QList breakPositions; ///< Up to 4 break positions (IO index after which to break, 0 = disabled) + QMap colWidths; ///< Column widths (key: column index) + qreal rowHeight = 8.0; ///< Row height in mm + QMap colVisible; ///< Column visibility (key: column index) + QFont headerFont; ///< Font for column headers + QFont cellFont; ///< Font for cell content + QStringList columnNames; ///< Custom column header names + QList columnOrder; ///< Column display order (logical indices) + bool showHeaders = true; ///< Show column headers on sheet + + bool operator==(const PlcMasterData &other) const { + return ios == other.ios + && breakPositions == other.breakPositions + && colWidths == other.colWidths + && qFuzzyCompare(rowHeight, other.rowHeight) + && colVisible == other.colVisible + && headerFont == other.headerFont + && cellFont == other.cellFont + && columnNames == other.columnNames + && columnOrder == other.columnOrder + && showHeaders == other.showHeaders; + } + bool operator!=(const PlcMasterData &other) const { + return !(*this == other); + } + }; + + static QString plcIOTypeToString(PlcIOType type); + static PlcIOType plcIOTypeFromString(const QString &string); + static QString translatedPlcIOType(PlcIOType type); + static QStringList plcIOTypeList(); + enum TerminalType { TTGeneric, TTFuse, @@ -163,6 +242,9 @@ class ElementData : public PropertiesInterface static QString slaveContactGroupSubtypeToString(ElementData::SlaveType type); static ElementData::SlaveType slaveContactGroupSubtypeFromString(const QString &string); + PlcMasterData plcMasterData() const { return m_plc_master_data; } + void setPlcMasterData(const PlcMasterData &data) { m_plc_master_data = data; } + // must be public, because this class is a private member // of Element/ element editor and they must access this data ElementData::Type m_type = ElementData::Simple; @@ -171,6 +253,7 @@ class ElementData : public PropertiesInterface int m_max_slaves{-1}; bool m_slave_contact_groups_enabled{false}; ///< Whether slave contact groups table is active QVector m_slave_contact_groups; ///< Contact groups defined by master + PlcMasterData m_plc_master_data; ///< PLC master IO table and display settings ElementData::SlaveType m_slave_type = ElementData::SSimple; ElementData::SlaveState m_slave_state = ElementData::NO; diff --git a/sources/properties/xrefproperties.cpp b/sources/properties/xrefproperties.cpp index 63fcf68ac..be0213c2b 100644 --- a/sources/properties/xrefproperties.cpp +++ b/sources/properties/xrefproperties.cpp @@ -176,7 +176,7 @@ QHash XRefProperties::defaultProperties() { QHash hash; QStringList keys; - keys << "coil" << "protection" << "commutator"; + keys << "coil" << "protection" << "commutator" << "plc"; QSettings settings; diff --git a/sources/qetgraphicsitem/crossrefitem.cpp b/sources/qetgraphicsitem/crossrefitem.cpp index 8bc328e16..4f00b6b53 100644 --- a/sources/qetgraphicsitem/crossrefitem.cpp +++ b/sources/qetgraphicsitem/crossrefitem.cpp @@ -28,6 +28,7 @@ #include "elementtextitemgroup.h" #include "qgraphicsitemutility.h" #include "terminal.h" +#include "../properties/elementdata.h" //define the height of the header. static int header = 5; @@ -235,8 +236,11 @@ void CrossRefItem::updateLabel() qp.setPen(pen_); qp.setFont(QETApp::diagramTextsFont(5)); - //Draw cross or contact, only if master element is linked. - if (! m_element->linkedElements().isEmpty()) + // PLC table is drawn by Element::drawPlcTable(), not by CrossRefItem + if (m_element->elementData().m_master_type == ElementData::PLC) + return; + //Draw cross or contact, only if master element is linked. + else if (! m_element->linkedElements().isEmpty()) { m_update_map = true; XRefProperties::DisplayHas dh = m_properties.displayHas(); @@ -325,6 +329,11 @@ void CrossRefItem::paint( // caused a use-after-free crash (QRegion::begin, Qt5Gui+0x49af60) // confirmed by analysis of 19+ coredumps. // m_update_map=false: draw functions do not overwrite m_hovered_contacts_map. + + // PLC table is drawn by Element::drawPlcTable(), not by CrossRefItem + if (m_element->elementData().m_master_type == ElementData::PLC) + return; + if (m_element->linkedElements().isEmpty()) return; QPen pen_; @@ -1336,3 +1345,211 @@ QList CrossRefItem::NCElements() const return nc_list; } + +/** + * @brief CrossRefItem::drawAsPlcTable + * Draw the PLC IO table for PLC master elements. + * The table shows columns: Type | Address | Function | Comment | CrossRef + * Columns and rows are configurable via PlcMasterData. + * @param painter painter to use + */ +void CrossRefItem::drawAsPlcTable(QPainter &painter) +{ + // Get PLC data from the element + ElementData ed = m_element->elementData(); + if (ed.m_master_type != ElementData::PLC) + return; + + ElementData::PlcMasterData plc_data = ed.plcMasterData(); + if (plc_data.ios.isEmpty()) + return; + + // Define column indices + const int COL_TYPE = 0; + const int COL_ADDRESS = 1; + const int COL_FUNCTION = 2; + const int COL_COMMENT = 3; + const int COL_CROSSREF = 4; + const int COL_COUNT = 5; + + // Column headers (French) + QMap headers; + headers[COL_TYPE] = QObject::tr("Type"); + headers[COL_ADDRESS] = QObject::tr("Adresse"); + headers[COL_FUNCTION] = QObject::tr("Fonction"); + headers[COL_COMMENT] = QObject::tr("Commentaire"); + headers[COL_CROSSREF] = QObject::tr("Réf. croisée"); + + // Build list of visible columns + QList visible_cols; + for (int i = 0; i < COL_COUNT; ++i) { + if (plc_data.colVisible.value(i, true)) + visible_cols.append(i); + } + if (visible_cols.isEmpty()) + return; + + // Default column widths if not set (in scene units, roughly mm * 2.835) + QMap col_widths; + for (int col : visible_cols) { + if (plc_data.colWidths.contains(col) && plc_data.colWidths[col] > 0) + col_widths[col] = plc_data.colWidths[col]; + else { + switch (col) { + case COL_TYPE: col_widths[col] = 35; break; + case COL_ADDRESS: col_widths[col] = 25; break; + case COL_FUNCTION: col_widths[col] = 50; break; + case COL_COMMENT: col_widths[col] = 40; break; + case COL_CROSSREF: col_widths[col] = 30; break; + default: col_widths[col] = 30; break; + } + } + } + + qreal row_h = plc_data.rowHeight > 0 ? plc_data.rowHeight : 8.0; + qreal header_h = row_h + 2.0; + + // Calculate total width + qreal total_width = 0; + for (int col : visible_cols) + total_width += col_widths[col]; + + // Calculate total height: header + all IO rows + int total_ios = plc_data.ios.size(); + + // Collect active break positions (sorted) + QList breaks; + for (int bp : plc_data.breakPositions) { + if (bp > 0 && bp < total_ios && !breaks.contains(bp)) + breaks.append(bp); + } + std::sort(breaks.begin(), breaks.end()); + + // Build block boundaries + QList block_starts; + block_starts.append(0); + for (int bp : breaks) + block_starts.append(bp); + + qreal total_height; + int block_count = block_starts.size(); + + if (block_count > 1) { + int max_rows = 0; + for (int b = 0; b < block_count; ++b) { + int start = block_starts.at(b); + int end = (b + 1 < block_starts.size()) ? block_starts.at(b + 1) : total_ios; + max_rows = qMax(max_rows, end - start); + } + total_height = header_h + max_rows * row_h; + total_width = total_width * block_count + (block_count - 1) * 3; + } else { + total_height = header_h + total_ios * row_h; + } + + // Draw background rectangle + QRectF bg_rect(0, 0, total_width, total_height); + painter.fillRect(bg_rect, Qt::white); + QPen border_pen(Qt::black, 0.5); + painter.setPen(border_pen); + painter.drawRect(bg_rect); + + // Draw header row + qreal x_offset = 0; + + for (int block = 0; block < block_count; ++block) { + qreal block_x = block * (col_widths.value(visible_cols.first(), 30) * visible_cols.size() + 3); + qreal cx = block_x; + + // Draw column headers + QFont header_font = painter.font(); + header_font.setBold(true); + painter.setFont(header_font); + + for (int col : visible_cols) { + QRectF header_rect(cx, 0, col_widths[col], header_h); + painter.fillRect(header_rect, QColor(220, 220, 220)); + painter.drawRect(header_rect); + painter.drawText(header_rect, Qt::AlignCenter, headers[col]); + cx += col_widths[col]; + } + + painter.setFont(painter.font()); + + // Draw IO rows for this block + int start_idx = block_starts.at(block); + int end_idx = (block + 1 < block_starts.size()) ? block_starts.at(block + 1) : total_ios; + + for (int row = 0; row < (end_idx - start_idx); ++row) { + int io_idx = start_idx + row; + const ElementData::PlcIO &io = plc_data.ios.at(io_idx); + + qreal ry = header_h + row * row_h; + cx = block_x; + + // Get linked slave element for this IO (via group index) + QString cross_ref_text = io.crossRef; + for (Element *slave : m_element->linkedElements()) { + if (m_element->groupIndexForElement(slave) == io_idx) { + cross_ref_text = elementPositionText(slave); + break; + } + } + + for (int col : visible_cols) { + QRectF cell_rect(cx, ry, col_widths[col], row_h); + painter.drawRect(cell_rect); + + QString cell_text; + switch (col) { + case COL_TYPE: + cell_text = ElementData::translatedPlcIOType(io.type); + break; + case COL_ADDRESS: + cell_text = io.address; + break; + case COL_FUNCTION: + cell_text = io.functionText; + break; + case COL_COMMENT: + cell_text = io.comment; + break; + case COL_CROSSREF: + cell_text = cross_ref_text; + break; + } + + QRectF text_rect = cell_rect.adjusted(1, 0, -1, 0); + painter.drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, cell_text); + + // If function text doesn't fit, try to show as much as possible + if (col == COL_FUNCTION && !io.functionText.isEmpty()) { + QFontMetrics fm(painter.font()); + QString elided = fm.elidedText(io.functionText, Qt::ElideRight, text_rect.width()); + // Clear and redraw with elided text + painter.save(); + painter.setBrush(Qt::white); + painter.drawRect(text_rect); + painter.restore(); + painter.drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, elided); + } + + // Store hover map for cross-reference cells + if (col == COL_CROSSREF && m_update_map && !cross_ref_text.isEmpty()) { + for (Element *slave : m_element->linkedElements()) { + if (m_element->groupIndexForElement(slave) == io_idx) { + m_hovered_contacts_map.insert(slave, cell_rect); + break; + } + } + } + + cx += col_widths[col]; + } + } + } + + prepareGeometryChange(); + m_bounding_rect = bg_rect; + m_shape_path.addRect(bg_rect); +} diff --git a/sources/qetgraphicsitem/crossrefitem.h b/sources/qetgraphicsitem/crossrefitem.h index 57e1fb22b..6c11374fd 100644 --- a/sources/qetgraphicsitem/crossrefitem.h +++ b/sources/qetgraphicsitem/crossrefitem.h @@ -106,6 +106,7 @@ class CrossRefItem : public QGraphicsObject void setUpCrossBoundingRect(QPainter &painter); void drawAsCross(QPainter &painter); void drawAsContacts(QPainter &painter); + void drawAsPlcTable(QPainter &painter); QRectF drawContact(QPainter &painter, int flags, Element *elmt, int pole_index = 0); void fillCrossRef(QPainter &painter); void AddExtraInfo(QPainter &painter, const QString&); diff --git a/sources/qetgraphicsitem/dynamicelementtextitem.cpp b/sources/qetgraphicsitem/dynamicelementtextitem.cpp index de47ca808..194296d60 100644 --- a/sources/qetgraphicsitem/dynamicelementtextitem.cpp +++ b/sources/qetgraphicsitem/dynamicelementtextitem.cpp @@ -280,6 +280,10 @@ Element *DynamicElementTextItem::elementUseForInfo() const return elmt; case Element::Slave: { + //PLC slave stores its own PLC variables in elementInformations + //(populated from master on link). Return self so %{plc_*} vars resolve. + if (elmt->elementData().m_slave_type == ElementData::PLCSlave) + return elmt; if(elmt->linkedElements().isEmpty()) return nullptr; else @@ -1347,18 +1351,20 @@ void DynamicElementTextItem::updateXref() } } } - else if (m_parent_element.data()->linkType() == Element::Slave) + else if (m_parent_element.data()->linkType() == Element::Slave && + !m_parent_element.data()->linkedElements().isEmpty()) { - if(m_master_element && !parentGroup() && + Element *master_elmt = m_parent_element.data()->linkedElements().first(); + if(master_elmt && !parentGroup() && ( (m_text_from == DynamicElementTextItem::ElementInfo && m_info_name == "label") || (m_text_from == DynamicElementTextItem::CompositeText && m_composite_text.contains("%{label}")) ) ) { - XRefProperties xrp = diagram()->project()->defaultXRefProperties(m_master_element.data()->kindInformations()["type"].toString()); + XRefProperties xrp = diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString()); QString xref_label = xrp.slaveLabel(); - xref_label = autonum::AssignVariables::formulaToLabel(xref_label, m_master_element.data()->rSequenceStruct(), m_master_element.data()->diagram(), m_master_element.data()); + xref_label = autonum::AssignVariables::formulaToLabel(xref_label, master_elmt->rSequenceStruct(), master_elmt->diagram(), master_elmt); if(!m_slave_Xref_item) { @@ -1367,9 +1373,9 @@ void DynamicElementTextItem::updateXref() m_slave_Xref_item->setDefaultTextColor(Qt::black); m_slave_Xref_item->installSceneEventFilter(this); - m_update_slave_Xref_connection << connect(m_master_element.data(), &Element::xChanged, this, &DynamicElementTextItem::updateXref); - m_update_slave_Xref_connection << connect(m_master_element.data(), &Element::yChanged, this, &DynamicElementTextItem::updateXref); - m_update_slave_Xref_connection << connect(m_master_element.data(), &Element::elementInfoChange, this, &DynamicElementTextItem::updateXref); + m_update_slave_Xref_connection << connect(master_elmt, &Element::xChanged, this, &DynamicElementTextItem::updateXref); + m_update_slave_Xref_connection << connect(master_elmt, &Element::yChanged, this, &DynamicElementTextItem::updateXref); + m_update_slave_Xref_connection << connect(master_elmt, &Element::elementInfoChange, this, &DynamicElementTextItem::updateXref); m_update_slave_Xref_connection << connect(diagram(), &Diagram::diagramInformationChanged, this, &DynamicElementTextItem::updateXref); m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::projectDiagramsOrderChanged, this, &DynamicElementTextItem::updateXref); m_update_slave_Xref_connection << connect(diagram()->project(), &QETProject::diagramRemoved, this, &DynamicElementTextItem::updateXref); @@ -1439,9 +1445,12 @@ void DynamicElementTextItem::setPlainText(const QString &text) } else if (m_slave_Xref_item) { - - XRefProperties xrp = diagram()->project()->defaultXRefProperties(m_master_element.data()->kindInformations()["type"].toString()); - setXref_item(xrp.getXrefPos()); + Element *master_elmt = m_parent_element.data()->linkedElements().isEmpty() + ? nullptr : m_parent_element.data()->linkedElements().first(); + if (master_elmt) { + XRefProperties xrp = diagram()->project()->defaultXRefProperties(master_elmt->kindInformations()["type"].toString()); + setXref_item(xrp.getXrefPos()); + } } } diff --git a/sources/qetgraphicsitem/element.cpp b/sources/qetgraphicsitem/element.cpp index fd723da7d..e8eb71e66 100644 --- a/sources/qetgraphicsitem/element.cpp +++ b/sources/qetgraphicsitem/element.cpp @@ -18,6 +18,7 @@ #include "element.h" #include "../qetproject.h" #include "../PropertiesEditor/propertieseditordialog.h" +#include "../autoNum/assignvariables.h" #include "../autoNum/numerotationcontextcommands.h" #include "../diagram.h" #include "../diagramcommands.h" @@ -26,6 +27,8 @@ #include "../elementprovider.h" #include "../factory/elementpicturefactory.h" #include "../properties/terminaldata.h" +#include "../properties/xrefproperties.h" +#include "../qetinformation.h" #include "../qetgraphicsitem/conductor.h" #include "../qetgraphicsitem/terminal.h" #include "../ui/elementpropertieswidget.h" @@ -229,13 +232,25 @@ void Element::paint( painter->setBrush(brush); if (options && options->levelOfDetailFromTransform(painter->worldTransform()) < 0.5) { - painter->drawPicture(0, 0, m_low_zoom_picture); + if (!m_low_zoom_picture.isNull()) + painter->drawPicture(0, 0, m_low_zoom_picture); } else { - painter->drawPicture(0, 0, m_picture); + if (!m_picture.isNull()) + painter->drawPicture(0, 0, m_picture); } painter->restore(); //Restore the QPainter after use drawPicture + // Draw PLC table from in-memory data (overwrites the stale cached picture) + if (scene() && painter->isActive() && + m_data.m_type == ElementData::Master && + m_data.m_master_type == ElementData::PLC) + { + const auto &plc_data = m_data.plcMasterData(); + if (!plc_data.ios.isEmpty()) + drawPlcTable(painter); + } + //Draw the selection rectangle if ( isSelected() || m_mouse_over ) { QGIUtility::drawBoundingRectSelection(this, painter); @@ -486,6 +501,13 @@ bool Element::buildFromXml(const QDomElement &xml_def_elmt, int *state) if (qde.isNull()) continue; + // Store plc_table positions for runtime rendering + if (qde.tagName() == QLatin1String("plc_table")) { + qreal x = qde.attribute("x", "0").toDouble(); + qreal y = qde.attribute("y", "0").toDouble(); + m_plc_table_positions.append(QPointF(x, y)); + } + if (parseElement(qde)) { ++ parsed_elements_count; } @@ -502,8 +524,8 @@ bool Element::buildFromXml(const QDomElement &xml_def_elmt, int *state) ElementPictureFactory *epf = ElementPictureFactory::instance(); epf->getPictures(m_location, - const_cast(m_picture), - const_cast(m_low_zoom_picture)); + m_picture, + m_low_zoom_picture); if(!m_picture.isNull()) ++ parsed_elements_count; @@ -1394,6 +1416,8 @@ ElementData Element::elementData() const void Element::setElementData(ElementData data) { auto old_info = m_data.m_informations; + auto old_plc = m_data.m_type == ElementData::Master && m_data.m_master_type == ElementData::PLC + ? m_data.plcMasterData() : ElementData::PlcMasterData(); m_data = data; if (old_info != m_data.m_informations) { @@ -1403,6 +1427,42 @@ void Element::setElementData(ElementData data) } emit elementInfoChange(old_info, m_data.m_informations); } + + // Propagate PLC master data changes to linked slaves + if (m_data.m_type == ElementData::Master && m_data.m_master_type == ElementData::PLC) + { + const auto &new_plc = m_data.plcMasterData(); + bool plc_changed = (old_plc.ios != new_plc.ios); + if (plc_changed && !m_group_index_map.isEmpty()) + { + for (auto it = m_group_index_map.constBegin(); it != m_group_index_map.constEnd(); ++it) + { + Element *slave = it.key(); + int io_idx = it.value(); + if (!slave || io_idx < 0 || io_idx >= new_plc.ios.size()) + continue; + const auto &io = new_plc.ios.at(io_idx); + DiagramContext ctx = slave->elementInformations(); + ctx.addValue(QETInformation::ELMT_PLC_TYPE, + ElementData::translatedPlcIOType(io.type)); + ctx.addValue(QETInformation::ELMT_PLC_ADDRESS, io.address); + ctx.addValue(QETInformation::ELMT_PLC_FUNCTION, io.functionText); + ctx.addValue(QETInformation::ELMT_PLC_COMMENT, io.comment); + ctx.addValue(QETInformation::ELMT_PLC_CROSSREF, + [&]() -> QString { + if (!diagram() || !diagram()->project()) + return QString(); + XRefProperties xrp = diagram()->project() + ->defaultXRefProperties("plc"); + autonum::sequentialNumbers seq; + return autonum::AssignVariables::formulaToLabel( + xrp.slaveLabel(), seq, diagram(), this); + }()); + ctx.addValue(QETInformation::ELMT_LABEL, actualLabel()); + slave->setElementInformations(ctx); + } + } + } } /** @@ -1638,3 +1698,213 @@ void Element::updateConductorTexts() } } } + +/** + @brief Element::drawPlcTable + Draw the PLC table directly from in-memory ElementData. + This overlays the stale picture from the .elmt file with current data. +*/ +void Element::drawPlcTable(QPainter *painter) +{ + if (!painter || !painter->isActive() || !scene()) + return; + + if (m_data.m_type != ElementData::Master || + m_data.m_master_type != ElementData::PLC) + return; + + const auto &plc_data = m_data.plcMasterData(); + if (plc_data.ios.isEmpty()) + return; + + // Use plc_table positions from the .elmt file, or default to (0,0) + QList positions = m_plc_table_positions; + if (positions.isEmpty()) + positions.append(QPointF(0, 0)); + + const int COL_COUNT = 5; + const int COL_TYPE = 0; + const int COL_ADDRESS = 1; + const int COL_FUNCTION = 2; + const int COL_COMMENT = 3; + const int COL_CROSSREF = 4; + + // Build header labels + QMap headers; + headers[COL_TYPE] = QObject::tr("Type"); + headers[COL_ADDRESS] = QObject::tr("Adresse"); + headers[COL_FUNCTION] = QObject::tr("Fonction"); + headers[COL_COMMENT] = QObject::tr("Commentaire"); + headers[COL_CROSSREF] = QObject::tr("Réf. croisée"); + + // Override with custom column names + if (!plc_data.columnNames.isEmpty()) { + QList all_cols; + all_cols << COL_TYPE << COL_ADDRESS << COL_FUNCTION << COL_COMMENT << COL_CROSSREF; + for (int i = 0; i < qMin(plc_data.columnNames.size(), all_cols.size()); ++i) { + if (!plc_data.columnNames.at(i).isEmpty()) + headers[all_cols.at(i)] = plc_data.columnNames.at(i); + } + } + + // Build visible columns + QList visible_cols; + if (!plc_data.columnOrder.isEmpty()) { + for (int logical : plc_data.columnOrder) { + if (logical >= 0 && logical < COL_COUNT + && plc_data.colVisible.value(logical, true) + && !visible_cols.contains(logical)) + visible_cols.append(logical); + } + for (int i = 0; i < COL_COUNT; ++i) { + if (plc_data.colVisible.value(i, true) && !visible_cols.contains(i)) + visible_cols.append(i); + } + } else { + for (int i = 0; i < COL_COUNT; ++i) { + if (plc_data.colVisible.value(i, true)) + visible_cols.append(i); + } + } + if (visible_cols.isEmpty()) + return; + + // Column widths + QMap col_widths; + for (int col : visible_cols) { + if (plc_data.colWidths.contains(col) && plc_data.colWidths[col] > 0) + col_widths[col] = plc_data.colWidths[col]; + else { + switch (col) { + case COL_TYPE: col_widths[col] = 35; break; + case COL_ADDRESS: col_widths[col] = 25; break; + case COL_FUNCTION: col_widths[col] = 50; break; + case COL_COMMENT: col_widths[col] = 40; break; + case COL_CROSSREF: col_widths[col] = 30; break; + default: col_widths[col] = 30; break; + } + } + } + + qreal row_h = plc_data.rowHeight > 0 ? plc_data.rowHeight : 8.0; + qreal header_h = plc_data.showHeaders ? (row_h + 2.0) : 0; + int total_ios = plc_data.ios.size(); + + // Parse break positions + QList breaks; + for (int bp : plc_data.breakPositions) { + if (bp > 0 && bp < total_ios && !breaks.contains(bp)) + breaks.append(bp); + } + std::sort(breaks.begin(), breaks.end()); + + QList block_starts; + block_starts.append(0); + for (int bp : breaks) + block_starts.append(bp); + int block_count = block_starts.size(); + + qreal total_width = 0; + for (int col : visible_cols) + total_width += col_widths[col]; + + QPen border_pen(Qt::black, 0.5); + + // Fonts + QFont header_font = plc_data.headerFont; + if (header_font.family().isEmpty()) { + header_font = painter->font(); + header_font.setBold(true); + } + QFont cell_font = plc_data.cellFont; + if (cell_font.family().isEmpty()) { + cell_font = painter->font(); + } + + for (const QPointF &pos : positions) { + for (int block = 0; block < block_count; ++block) { + qreal bx = pos.x() + block * (total_width + 3); + qreal cx = bx; + + // Draw header background + if (plc_data.showHeaders) { + painter->save(); + painter->setPen(Qt::NoPen); + painter->setBrush(QColor(220, 220, 220)); + painter->drawRect(QRectF(cx, pos.y(), total_width, header_h)); + painter->restore(); + + painter->setFont(header_font); + painter->setPen(border_pen); + painter->setBrush(Qt::NoBrush); + + for (int col : visible_cols) { + QRectF hr(cx, pos.y(), col_widths[col], header_h); + painter->drawRect(hr); + QRectF text_rect = hr.adjusted(1, 0, -1, 0); + painter->drawText(text_rect, Qt::AlignCenter, headers.value(col, QString())); + cx += col_widths[col]; + } + } + + // Draw IO rows + painter->setFont(cell_font); + int start_idx = block_starts.at(block); + int end_idx = (block + 1 < block_starts.size()) + ? block_starts.at(block + 1) : total_ios; + + for (int row = 0; row < (end_idx - start_idx); ++row) { + int io_idx = start_idx + row; + if (io_idx >= plc_data.ios.size()) break; + const auto &io = plc_data.ios.at(io_idx); + qreal ry = pos.y() + header_h + row * row_h; + cx = bx; + + for (int col : visible_cols) { + QRectF cr(cx, ry, col_widths[col], row_h); + painter->setPen(border_pen); + painter->setBrush(Qt::NoBrush); + painter->drawRect(cr); + + QString cell_text; + switch (col) { + case COL_TYPE: cell_text = ElementData::translatedPlcIOType(io.type); break; + case COL_ADDRESS: cell_text = io.address; break; + case COL_FUNCTION: cell_text = io.functionText; break; + case COL_COMMENT: cell_text = io.comment; break; + case COL_CROSSREF: + { + cell_text = io.crossRef; + for (auto it = m_group_index_map.constBegin(); + it != m_group_index_map.constEnd(); ++it) { + if (it.value() == io_idx) { + Element *slave = it.key(); + if (slave && slave->diagram() && diagram() + && diagram()->project()) + { + XRefProperties xrp = diagram()->project() + ->defaultXRefProperties("plc"); + autonum::sequentialNumbers seq; + cell_text = autonum::AssignVariables + ::formulaToLabel( + xrp.masterLabel(), + seq, + slave->diagram(), + slave); + } + break; + } + } + break; + } + } + + QRectF text_rect = cr.adjusted(1, 0, -1, 0); + painter->drawText(text_rect, Qt::AlignLeft | Qt::AlignVCenter, cell_text); + + cx += col_widths[col]; + } + } + } + } +} diff --git a/sources/qetgraphicsitem/element.h b/sources/qetgraphicsitem/element.h index 0dc3f7c8f..fce4f4878 100644 --- a/sources/qetgraphicsitem/element.h +++ b/sources/qetgraphicsitem/element.h @@ -256,11 +256,14 @@ class Element : public QetGraphicsItem bool m_freeze_label = false; QString m_F_str; - ElementsLocation m_location; - QList m_terminals; - const QPicture m_picture; - const QPicture m_low_zoom_picture; - ElementData m_data; + ElementsLocation m_location; + QList m_terminals; + QPicture m_picture; + QPicture m_low_zoom_picture; + ElementData m_data; + QList m_plc_table_positions; // Positions of plc_table parts in the element definition + + void drawPlcTable(QPainter *painter); private: bool m_must_highlight = false; diff --git a/sources/qetgraphicsitem/masterelement.cpp b/sources/qetgraphicsitem/masterelement.cpp index 5d573ca12..28d03085e 100644 --- a/sources/qetgraphicsitem/masterelement.cpp +++ b/sources/qetgraphicsitem/masterelement.cpp @@ -18,8 +18,10 @@ #include "masterelement.h" #include "../qetproject.h" #include "../diagram.h" +#include "../qetinformation.h" #include "crossrefitem.h" #include "dynamicelementtextitem.h" +#include "../properties/elementdata.h" #include @@ -61,9 +63,14 @@ void MasterElement::linkToElement(Element *elmt) elmt->linkToElement(this); XRefProperties xrp = diagram()->project()->defaultXRefProperties(kindInformations()["type"].toString()); - if (!m_Xref_item && xrp.snapTo() == XRefProperties::Bottom) + if (!m_Xref_item && (xrp.snapTo() == XRefProperties::Bottom || + m_data.m_master_type == ElementData::PLC)) m_Xref_item = new CrossRefItem(this); //create cross ref item if not yet - + + // For PLC masters, connect slave position changes to trigger master repaint + if (m_data.m_master_type == ElementData::PLC) + connectSlavePositionUpdates(elmt); + emit linkedElementChanged(); aboutDeleteXref(); } @@ -94,10 +101,28 @@ void MasterElement::unlinkElement(Element *elmt) //Ensure elmt is linked to this element if (connected_elements.contains(elmt)) { + // Clear PLC variables on slave before unlinking + if (m_data.m_master_type == ElementData::PLC) + { + DiagramContext ctx = elmt->elementInformations(); + ctx.remove(QETInformation::ELMT_PLC_TYPE); + ctx.remove(QETInformation::ELMT_PLC_ADDRESS); + ctx.remove(QETInformation::ELMT_PLC_FUNCTION); + ctx.remove(QETInformation::ELMT_PLC_COMMENT); + ctx.remove(QETInformation::ELMT_PLC_CROSSREF); + ctx.remove(QETInformation::ELMT_LABEL); + elmt->setElementInformations(ctx); + + setGroupIndexForElement(elmt, -1); + } + connected_elements.removeOne(elmt); elmt -> unlinkElement (this); elmt -> setHighlighted (false); + // Disconnect slave position updates for PLC masters + disconnectSlavePositionUpdates(elmt); + aboutDeleteXref(); emit linkedElementChanged(); } @@ -141,7 +166,7 @@ void MasterElement::xrefPropertiesChanged() { if(!diagram()) return; - + XRefProperties xrp = diagram()->project()->defaultXRefProperties(kindInformations()["type"].toString()); if(xrp.snapTo() == XRefProperties::Bottom) { @@ -150,6 +175,12 @@ void MasterElement::xrefPropertiesChanged() if(!m_Xref_item) m_Xref_item = new CrossRefItem(this); } + // Always create Xref for PLC master elements (they show IO table even without linked slaves) + else if (m_data.m_master_type == ElementData::PLC) + { + if(!m_Xref_item) + m_Xref_item = new CrossRefItem(this); + } aboutDeleteXref(); } @@ -167,7 +198,11 @@ void MasterElement::aboutDeleteXref() { if(!m_Xref_item) return; - + + // Never delete Xref for PLC master elements - they always show the IO table + if (m_data.m_master_type == ElementData::PLC) + return; + XRefProperties xrp = diagram()->project()->defaultXRefProperties(kindInformations()["type"].toString()); if (xrp.snapTo() != XRefProperties::Bottom && m_Xref_item) { @@ -175,7 +210,7 @@ void MasterElement::aboutDeleteXref() m_Xref_item = nullptr; return; } - + if (m_Xref_item->boundingRect().isNull()) { delete m_Xref_item; @@ -207,3 +242,36 @@ bool MasterElement::isFull() const // Return true if current connected elements reached or exceeded the limit return connected_elements.size() >= max_slaves; } + +/** + @brief MasterElement::connectSlavePositionUpdates + Connect slave xChanged/yChanged to master update() so the PLC table + repaints in real-time when a slave moves on the diagram. + @param slave the slave element +*/ +void MasterElement::connectSlavePositionUpdates(Element *slave) +{ + if (!slave) + return; + m_slave_x_conn[slave] = connect(slave, &QGraphicsObject::xChanged, + [this]() { update(); }); + m_slave_y_conn[slave] = connect(slave, &QGraphicsObject::yChanged, + [this]() { update(); }); +} + +/** + @brief MasterElement::disconnectSlavePositionUpdates + Disconnect slave position signals from master. + @param slave the slave element +*/ +void MasterElement::disconnectSlavePositionUpdates(Element *slave) +{ + if (!slave) + return; + if (m_slave_x_conn.contains(slave)) { + disconnect(m_slave_x_conn.take(slave)); + } + if (m_slave_y_conn.contains(slave)) { + disconnect(m_slave_y_conn.take(slave)); + } +} diff --git a/sources/qetgraphicsitem/masterelement.h b/sources/qetgraphicsitem/masterelement.h index 89927161a..5fb1fb029 100644 --- a/sources/qetgraphicsitem/masterelement.h +++ b/sources/qetgraphicsitem/masterelement.h @@ -19,6 +19,8 @@ #define MASTERELEMENT_H #include "element.h" +#include +#include class CrossRefItem; @@ -55,10 +57,14 @@ class MasterElement : public Element private: void xrefPropertiesChanged(); void aboutDeleteXref (); + void connectSlavePositionUpdates(Element *slave); + void disconnectSlavePositionUpdates(Element *slave); private: CrossRefItem *m_Xref_item = nullptr; bool m_first_scene_change = true; + QHash m_slave_x_conn; + QHash m_slave_y_conn; }; #endif // MASTERELEMENT_H diff --git a/sources/qetinformation.cpp b/sources/qetinformation.cpp index ced8fa3c1..de28925ef 100644 --- a/sources/qetinformation.cpp +++ b/sources/qetinformation.cpp @@ -190,6 +190,11 @@ QStringList QETInformation::elementInfoKeys() ELMT_SUPPLIER_AUX4, ELMT_QUANTITY_AUX4, ELMT_UNITY_AUX4, + ELMT_PLC_TYPE, + ELMT_PLC_ADDRESS, + ELMT_PLC_FUNCTION, + ELMT_PLC_COMMENT, + ELMT_PLC_CROSSREF, "exclude_from_bom" }; return list; } @@ -306,6 +311,11 @@ QString QETInformation::translatedInfoKey(const QString &info) else if (info == ELMT_SUPPLIER_AUX4) return QObject::tr("Fournisseur auxiliaire 4"); else if (info == ELMT_QUANTITY_AUX4) return QObject::tr("Quantité auxiliaire 4"); else if (info == ELMT_UNITY_AUX4) return QObject::tr("Unité auxiliaire 4"); + else if (info == ELMT_PLC_TYPE) return QObject::tr("Type PLC"); + else if (info == ELMT_PLC_ADDRESS) return QObject::tr("Adresse PLC"); + else if (info == ELMT_PLC_FUNCTION) return QObject::tr("Fonction PLC"); + else if (info == ELMT_PLC_COMMENT) return QObject::tr("Commentaire PLC"); + else if (info == ELMT_PLC_CROSSREF) return QObject::tr("Réf. croisée PLC"); else return QString(); } diff --git a/sources/qetinformation.h b/sources/qetinformation.h index e2a855fe1..2dd566ffd 100644 --- a/sources/qetinformation.h +++ b/sources/qetinformation.h @@ -124,6 +124,13 @@ namespace QETInformation /** Default information related to QElectroTech **/ static QString QET_VERSION = "version"; + /** PLC-specific information for slave elements **/ + static QString ELMT_PLC_TYPE = "plc_type"; + static QString ELMT_PLC_ADDRESS = "plc_address"; + static QString ELMT_PLC_FUNCTION = "plc_function"; + static QString ELMT_PLC_COMMENT = "plc_comment"; + static QString ELMT_PLC_CROSSREF = "plc_crossref"; + diff --git a/sources/ui/elementpropertieswidget.cpp b/sources/ui/elementpropertieswidget.cpp index 10cc84ea7..870984bdd 100644 --- a/sources/ui/elementpropertieswidget.cpp +++ b/sources/ui/elementpropertieswidget.cpp @@ -28,6 +28,7 @@ #include "elementinfowidget.h" #include "linksingleelementwidget.h" #include "masterpropertieswidget.h" +#include "plclinkwidget.h" #include #include @@ -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); diff --git a/sources/ui/linksingleelementwidget.cpp b/sources/ui/linksingleelementwidget.cpp index 8f3f962be..528a20e8a 100644 --- a/sources/ui/linksingleelementwidget.cpp +++ b/sources/ui/linksingleelementwidget.cpp @@ -29,6 +29,7 @@ #include "../ui_linksingleelementwidget.h" #include +#include /** @@ -393,17 +394,24 @@ QVector > 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(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(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 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 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 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; } - + } /** diff --git a/sources/ui/masterpropertieswidget.cpp b/sources/ui/masterpropertieswidget.cpp index 8c56f5757..25aede52a 100644 --- a/sources/ui/masterpropertieswidget.cpp +++ b/sources/ui/masterpropertieswidget.cpp @@ -24,9 +24,21 @@ #include "../qetgraphicsitem/element.h" #include "../undocommand/linkelementcommand.h" #include "ui_masterpropertieswidget.h" +#include "../properties/elementdata.h" #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include /** @@ -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 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 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(ui->m_free_tree_widget), + static_cast(ui->m_link_tree_widget), + static_cast(ui->label), + static_cast(ui->label_2), + static_cast(ui->link_button), + static_cast(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(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(ui->m_free_tree_widget), + static_cast(ui->m_link_tree_widget), + static_cast(ui->label), + static_cast(ui->label_2), + static_cast(ui->link_button), + static_cast(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 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 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(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::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(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::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::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::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(m_plc_table->cellWidget(row, 0)); + if (type_cb) + io.type = static_cast(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)); +} diff --git a/sources/ui/masterpropertieswidget.h b/sources/ui/masterpropertieswidget.h index a66f49835..9c34d15ab 100644 --- a/sources/ui/masterpropertieswidget.h +++ b/sources/ui/masterpropertieswidget.h @@ -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 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 m_plc_col_visibility_checkboxes; + QList m_plc_col_width_spinboxes; + bool m_plc_updating = false; // Guard against recursive updates }; #endif // MASTERPROPERTIESWIDGET_H diff --git a/sources/ui/plclinkwidget.cpp b/sources/ui/plclinkwidget.cpp new file mode 100644 index 000000000..add80441d --- /dev/null +++ b/sources/ui/plclinkwidget.cpp @@ -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 . +*/ +#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); +} diff --git a/sources/ui/plclinkwidget.h b/sources/ui/plclinkwidget.h new file mode 100644 index 000000000..62ca2243c --- /dev/null +++ b/sources/ui/plclinkwidget.h @@ -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 . +*/ +#ifndef PLCLINKWIDGET_H +#define PLCLINKWIDGET_H + +#include "abstractelementpropertieseditorwidget.h" + +#include +#include +#include + +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 master; + int ioIndex = -1; + }; + QHash m_io_entry_hash; + + Element *m_element_to_link = nullptr; + int m_pending_io_index = -1; +}; + +#endif // PLCLINKWIDGET_H diff --git a/sources/ui/xrefpropertieswidget.cpp b/sources/ui/xrefpropertieswidget.cpp index 60d81b860..94c03a72a 100644 --- a/sources/ui/xrefpropertieswidget.cpp +++ b/sources/ui/xrefpropertieswidget.cpp @@ -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"); diff --git a/sources/undocommand/linkelementcommand.cpp b/sources/undocommand/linkelementcommand.cpp index 799835d3a..924d11c56 100644 --- a/sources/undocommand/linkelementcommand.cpp +++ b/sources/undocommand/linkelementcommand.cpp @@ -19,10 +19,35 @@ #include "../conductorautonumerotation.h" #include "../diagram.h" +#include "../diagramposition.h" +#include "../qetproject.h" #include "../qetgraphicsitem/conductor.h" #include "../qetgraphicsitem/element.h" #include "../qetgraphicsitem/terminal.h" #include "../ui/potentialselectordialog.h" +#include "../qetinformation.h" +#include "../properties/elementdata.h" +#include "../properties/xrefproperties.h" +#include "../autoNum/assignvariables.h" +#include "../autoNum/numerotationcontextcommands.h" + +/** + @brief Get the cross-ref text for a slave element using XRefProperties formula + @param master the PLC master element + @param slave the slave element + @return formatted cross-ref string (e.g. "(1-C4)") +*/ +static QString plcCrossRefText(Element *master, Element *slave) +{ + if (!master || !slave || !master->diagram() + || !master->diagram()->project()) + return QString(); + XRefProperties xrp = master->diagram()->project() + ->defaultXRefProperties("plc"); + autonum::sequentialNumbers seq; + return autonum::AssignVariables::formulaToLabel( + xrp.slaveLabel(), seq, master->diagram(), master); +} /** @brief LinkElementCommand::LinkElementCommand @@ -99,6 +124,14 @@ bool LinkElementCommand::isLinkable(Element *element_a, Element *element_b, bool { //Type isn't good if (element_b->linkType() != Element::Slave) return false; + + // PLC master can only link to PLC slave, and vice versa + { + const bool master_is_plc = (element_a->elementData().m_master_type == ElementData::PLC); + const bool slave_is_plc = (element_b->elementData().m_slave_type == ElementData::PLCSlave); + if (master_is_plc != slave_is_plc) return false; + } + //element_b is free if (element_b->isFree()) return true; //element_b isn't free but already linked to element_a and already_linked is true @@ -111,6 +144,14 @@ bool LinkElementCommand::isLinkable(Element *element_a, Element *element_b, bool { //Type isn't good if (element_b->linkType() != Element::Master) return false; + + // PLC master can only link to PLC slave, and vice versa + { + const bool slave_is_plc = (element_a->elementData().m_slave_type == ElementData::PLCSlave); + const bool master_is_plc = (element_b->elementData().m_master_type == ElementData::PLC); + if (slave_is_plc != master_is_plc) return false; + } + //Element_a is free if (element_a->isFree()) return true; //element_a isn't free but already linked to element_b and already_linked is true; @@ -195,6 +236,16 @@ void LinkElementCommand::undo() t->setMasterLabelIndex(0); } + // Clear PLC variables on slave when unlinking from PLC master + DiagramContext ctx = m_element->elementInformations(); + ctx.remove(QETInformation::ELMT_PLC_TYPE); + ctx.remove(QETInformation::ELMT_PLC_ADDRESS); + ctx.remove(QETInformation::ELMT_PLC_FUNCTION); + ctx.remove(QETInformation::ELMT_PLC_COMMENT); + ctx.remove(QETInformation::ELMT_PLC_CROSSREF); + ctx.remove(QETInformation::ELMT_LABEL); + m_element->setElementInformations(ctx); + foreach(Element *elmt, m_element->linkedElements()) { if (elmt->linkType() == Element::Master) @@ -220,6 +271,19 @@ void LinkElementCommand::undo() t->setMasterLabelIndex(0); } + // Clear PLC variables on slave when unlinking from PLC master + if (m_element->elementData().m_master_type == ElementData::PLC) + { + DiagramContext ctx = slave->elementInformations(); + ctx.remove(QETInformation::ELMT_PLC_TYPE); + ctx.remove(QETInformation::ELMT_PLC_ADDRESS); + ctx.remove(QETInformation::ELMT_PLC_FUNCTION); + ctx.remove(QETInformation::ELMT_PLC_COMMENT); + ctx.remove(QETInformation::ELMT_PLC_CROSSREF); + ctx.remove(QETInformation::ELMT_LABEL); + slave->setElementInformations(ctx); + } + m_element->setGroupIndexForElement(slave, -1); } } @@ -361,6 +425,28 @@ void LinkElementCommand::makeLink(const QList &element_list) } } } + + // Populate PLC variables on the slave if master is PLC type + if (elmt->elementData().m_master_type == ElementData::PLC) + { + const auto &plc_data = elmt->elementData().plcMasterData(); + if (group_idx < plc_data.ios.size()) + { + const auto &io = plc_data.ios.at(group_idx); + DiagramContext ctx = m_element->elementInformations(); + ctx.addValue(QETInformation::ELMT_PLC_TYPE, + ElementData::translatedPlcIOType(io.type)); + ctx.addValue(QETInformation::ELMT_PLC_ADDRESS, io.address); + ctx.addValue(QETInformation::ELMT_PLC_FUNCTION, io.functionText); + ctx.addValue(QETInformation::ELMT_PLC_COMMENT, io.comment); + ctx.addValue(QETInformation::ELMT_PLC_CROSSREF, + plcCrossRefText(elmt, m_element)); + ctx.addValue(QETInformation::ELMT_LABEL, + elmt->actualLabel()); + m_element->setElementInformations(ctx); + } + } + break; } } @@ -397,6 +483,27 @@ void LinkElementCommand::makeLink(const QList &element_list) } } } + + // Populate PLC variables on the slave if master is PLC type + if (m_element->elementData().m_master_type == ElementData::PLC) + { + const auto &plc_data = m_element->elementData().plcMasterData(); + if (group_idx < plc_data.ios.size()) + { + const auto &io = plc_data.ios.at(group_idx); + DiagramContext ctx = slave->elementInformations(); + ctx.addValue(QETInformation::ELMT_PLC_TYPE, + ElementData::translatedPlcIOType(io.type)); + ctx.addValue(QETInformation::ELMT_PLC_ADDRESS, io.address); + ctx.addValue(QETInformation::ELMT_PLC_FUNCTION, io.functionText); + ctx.addValue(QETInformation::ELMT_PLC_COMMENT, io.comment); + ctx.addValue(QETInformation::ELMT_PLC_CROSSREF, + plcCrossRefText(m_element, slave)); + ctx.addValue(QETInformation::ELMT_LABEL, + m_element->actualLabel()); + slave->setElementInformations(ctx); + } + } } } } @@ -415,6 +522,33 @@ void LinkElementCommand::makeLink(const QList &element_list) { foreach(Element *elmt, to_unlink) { + // Clear PLC variables on the slave before unlinking + // The slave is either elmt (if m_element is master) or m_element (if m_element is slave) + Element *slave = nullptr; + if (m_element->elementData().m_master_type == ElementData::PLC) + slave = elmt; + else if (m_element->linkType() == Element::Slave) + { + for (Element *linked : m_element->linkedElements()) + { + if (linked->elementData().m_master_type == ElementData::PLC) + { + slave = m_element; + break; + } + } + } + if (slave) + { + DiagramContext ctx = slave->elementInformations(); + ctx.remove(QETInformation::ELMT_PLC_TYPE); + ctx.remove(QETInformation::ELMT_PLC_ADDRESS); + ctx.remove(QETInformation::ELMT_PLC_FUNCTION); + ctx.remove(QETInformation::ELMT_PLC_COMMENT); + ctx.remove(QETInformation::ELMT_PLC_CROSSREF); + ctx.remove(QETInformation::ELMT_LABEL); + slave->setElementInformations(ctx); + } m_element->unlinkElement(elmt); } } From 3db298996aec20f7ec6a589595ed6dc4093cc6ea Mon Sep 17 00:00:00 2001 From: Kellermorph Date: Wed, 29 Jul 2026 16:40:17 +0200 Subject: [PATCH 2/2] Fix --- sources/properties/elementdata.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sources/properties/elementdata.cpp b/sources/properties/elementdata.cpp index becb7a79e..6bab5ce3d 100644 --- a/sources/properties/elementdata.cpp +++ b/sources/properties/elementdata.cpp @@ -835,6 +835,8 @@ void ElementData::kindInfoFromXml(const QDomElement &xml_element) if (!xml_names.isNull()) { for (const auto &xml_col : QETXML::findInDomElement(xml_names, QStringLiteral("column"))) { int index = xml_col.attribute(QStringLiteral("index")).toInt(); + if (index < 0 || index >= 5) + continue; while (m_plc_master_data.columnNames.size() <= index) m_plc_master_data.columnNames.append(QString()); m_plc_master_data.columnNames[index] = xml_col.text(); @@ -846,6 +848,8 @@ void ElementData::kindInfoFromXml(const QDomElement &xml_element) if (!xml_order.isNull()) { QStringList parts = xml_order.text().split(','); for (const auto &p : parts) { + if (m_plc_master_data.columnOrder.size() >= 5) + break; bool ok; int val = p.trimmed().toInt(&ok); if (ok) @@ -861,6 +865,8 @@ void ElementData::kindInfoFromXml(const QDomElement &xml_element) // Parse IO entries auto xml_ios = xml_plc.firstChildElement(QStringLiteral("plcIOs")); for (const auto &xml_io : QETXML::findInDomElement(xml_ios, QStringLiteral("plcIO"))) { + if (m_plc_master_data.ios.size() >= 128) + break; PlcIO io; io.type = plcIOTypeFromString( xml_io.attribute(QStringLiteral("type"), QStringLiteral("entree_digitale")));