Merge branch 'master' into qt6_cmake_joshua

This commit is contained in:
joshua
2026-07-31 21:15:21 +02:00
parent f16cf7dac8
commit e6d29cf345
297 changed files with 82691 additions and 27463 deletions
+63
View File
@@ -0,0 +1,63 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "backupdialog.h"
#include <QLabel>
#include <QPushButton>
#include <QVBoxLayout>
#include <QHBoxLayout>
/**
@brief BackupDialog::BackupDialog
@param parent parent widget
*/
BackupDialog::BackupDialog(QWidget *parent) :
QDialog(parent)
{
setWindowTitle(tr("Créer une copie de sauvegarde ?", "window title"));
setFixedSize(450, 100);
auto main_layout = new QVBoxLayout(this);
auto label = new QLabel(
tr("Souhaitez-vous créer une copie de sauvegarde ?",
"dialog message"));
label->setWordWrap(true);
main_layout->addWidget(label);
main_layout->addStretch();
auto button_layout = new QHBoxLayout();
button_layout->addStretch();
auto yes_button = new QPushButton(tr("Oui", "yes button"));
auto no_button = new QPushButton(tr("Non", "no button"));
button_layout->addWidget(yes_button);
button_layout->addWidget(no_button);
main_layout->addLayout(button_layout);
connect(yes_button, &QPushButton::clicked, this, &QDialog::accept);
connect(no_button, &QPushButton::clicked, this, &QDialog::reject);
}
/**
@brief BackupDialog::~BackupDialog
*/
BackupDialog::~BackupDialog() = default;
+32
View File
@@ -0,0 +1,32 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef BACKUPDIALOG_H
#define BACKUPDIALOG_H
#include <QDialog>
class BackupDialog : public QDialog
{
Q_OBJECT
public:
explicit BackupDialog(QWidget *parent = nullptr);
~BackupDialog() override;
};
#endif // BACKUPDIALOG_H
+51 -7
View File
@@ -30,7 +30,7 @@
#include "../reportpropertiewidget.h"
#include "../titleblockpropertieswidget.h"
#include "../xrefpropertieswidget.h"
#include "guidespropertieswidget.h"
#include <QFont>
#include <QFontDialog>
#include <QSizePolicy>
@@ -73,6 +73,33 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
rpw = new ReportPropertieWidget(ReportProperties::defaultProperties());
// default properties of xref
xrefpw = new XRefPropertiesWidget(XRefProperties::defaultProperties(), this);
// default guides properties
m_gpw = new GuidesPropertiesWidget(this);
QSettings settings;
QList<Diagram::Guide> loaded_guides;
if (m_project) {
for (const auto &pg : m_project->defaultGuides()) {
Diagram::Guide g;
g.orientation = static_cast<Diagram::Guide::Orientation>(pg.orientation);
g.position = pg.position;
g.color = pg.color;
loaded_guides.append(g);
}
} else {
QSettings settings;
int size = settings.beginReadArray(QStringLiteral("diagrameditor/defaultguides"));
for (int i = 0; i < size; ++i) {
settings.setArrayIndex(i);
Diagram::Guide g;
g.orientation = static_cast<Diagram::Guide::Orientation>(settings.value(QStringLiteral("orientation"), 0).toInt());
g.position = settings.value(QStringLiteral("position"), 0.0).toReal();
g.color = QColor(settings.value(QStringLiteral("color"), QStringLiteral("#ff0000")).toString());
loaded_guides.append(g);
}
settings.endArray();
}
m_gpw->setGuides(loaded_guides);
//If there is a project, we edit his properties
if (m_project) {
@@ -98,6 +125,7 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
tab_widget -> addTab (m_cpw, tr("Conducteur"));
tab_widget -> addTab (rpw, tr("Reports de folio"));
tab_widget -> addTab (xrefpw, tr("Références croisées"));
tab_widget -> addTab (m_gpw, tr("Guides"));
QVBoxLayout *vlayout1 = new QVBoxLayout();
vlayout1->addWidget(tab_widget);
@@ -155,6 +183,17 @@ void NewDiagramPage::applyConf()
modified_project = true;
}
QList<GuideProperties> proj_guides;
for (const auto &g : m_gpw->guides()) {
GuideProperties pg;
pg.orientation = static_cast<int>(g.orientation);
pg.position = g.position;
pg.color = g.color;
proj_guides.append(pg);
}
m_project->setDefaultGuides(proj_guides);
modified_project = true;
if (modified_project) {
m_project -> setModified(modified_project);
}
@@ -176,13 +215,18 @@ void NewDiagramPage::applyConf()
// default xref properties
QHash <QString, XRefProperties> hash_xrp = xrefpw -> properties();
foreach (QString key, hash_xrp.keys()) {
XRefProperties xrp = hash_xrp[key];
QString str("diagrameditor/defaultxref");
xrp.toSettings(settings, str += key);
}
}
// Global in QSettings speichern
QList<Diagram::Guide> current_guides = m_gpw->guides();
settings.beginWriteArray(QStringLiteral("diagrameditor/defaultguides"));
for (int i = 0; i < current_guides.size(); ++i) {
settings.setArrayIndex(i);
settings.setValue(QStringLiteral("orientation"), static_cast<int>(current_guides[i].orientation));
settings.setValue(QStringLiteral("position"), current_guides[i].position);
settings.setValue(QStringLiteral("color"), current_guides[i].color.name());
}
settings.endArray();
}
}
/**
+2
View File
@@ -29,6 +29,7 @@ class TitleBlockPropertiesWidget;
class ExportPropertiesWidget;
class ReportPropertieWidget;
class XRefPropertiesWidget;
class GuidesPropertiesWidget;
class QETProject;
class TitleBlockProperties;
@@ -69,6 +70,7 @@ public slots:
ConductorPropertiesWidget *m_cpw; ///< Widget to edit default conductor properties
ReportPropertieWidget *rpw; ///< Widget to edit default report label
XRefPropertiesWidget *xrefpw; ///< Widget to edit default xref properties
GuidesPropertiesWidget *m_gpw; ///< Widget to edit guides
TitleBlockProperties savedTbp; ///< Used to save current TBP and retrieve later
};
@@ -21,11 +21,12 @@
#include "../../qeticons.h"
#include "ui_generalconfigurationpage.h"
#include "../../utils/qetsettings.h"
#include "../../utils/qetutils.h"
#include "../../qetmessagebox.h"
#include <QFileDialog>
#include <QFontDialog>
#include <QSettings>
#include <sources/ui/configpage/ui_generalconfigurationpage.h>
/**
@brief GeneralConfigurationPage::GeneralConfigurationPage
@@ -62,6 +63,9 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
ui->m_hdpi_round_policy_cb->setCurrentIndex(4);
break;
}
ui->grid_startup_cb->setChecked(settings.value("diagrameditor/grid_display_startup", true).toBool());
ui->guides_startup_cb->setChecked(settings.value("diagrameditor/guides_display_startup", false).toBool());
ui->DiagramEditor_xGrid_sb->setValue(settings.value("diagrameditor/Xgrid", 10).toInt());
ui->DiagramEditor_yGrid_sb->setValue(settings.value("diagrameditor/Ygrid", 10).toInt());
ui->DiagramEditor_xKeyGrid_sb->setValue(settings.value("diagrameditor/key_Xgrid", 10).toInt());
@@ -95,7 +99,7 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
if (settings.contains("diagrameditor/dynamic_text_font"))
{
QFont font;
font.fromString(settings.value("diagrameditor/dynamic_text_font").toString());
QETUtils::fontFromString(font, settings.value("diagrameditor/dynamic_text_font").toString());
QString fontInfos = font.family() + " " +
QString::number(font.pointSize()) + " (" +
@@ -108,7 +112,7 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
if (settings.contains("diagrameditor/independent_text_font"))
{
QFont font;
font.fromString(settings.value("diagrameditor/independent_text_font").toString());
QETUtils::fontFromString(font, settings.value("diagrameditor/independent_text_font").toString());
QString fontInfos = font.family() + " " +
QString::number(font.pointSize()) + " (" +
@@ -234,6 +238,9 @@ void GeneralConfigurationPage::applyConf()
settings.setValue("diagrameditor/highlight-integrated-elements", ui->m_highlight_integrated_elements->isChecked());
settings.setValue("diagrameditor/zoom-out-beyond-of-folio", ui->m_zoom_out_beyond_folio->isChecked());
settings.setValue("diagrameditor/autosave-interval", ui->m_autosave_sb->value());
settings.setValue("diagrameditor/grid_display_startup", ui->grid_startup_cb->isChecked());
settings.setValue("diagrameditor/guides_display_startup", ui->guides_startup_cb->isChecked());
//Grid step and key navigation
settings.setValue("diagrameditor/Xgrid", ui->DiagramEditor_xGrid_sb->value());
settings.setValue("diagrameditor/Ygrid", ui->DiagramEditor_yGrid_sb->value());
@@ -445,11 +452,11 @@ void GeneralConfigurationPage::on_m_dyn_text_font_pb_clicked()
bool ok;
QSettings settings;
QFont curFont;
curFont.fromString(settings.value("diagrameditor/dynamic_text_font", "Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular").toString());
QETUtils::fontFromString(curFont, settings.value("diagrameditor/dynamic_text_font", "Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular").toString());
QFont font = QFontDialog::getFont(&ok, curFont, this);
if (ok)
{
settings.setValue("diagrameditor/dynamic_text_font", font.toString());
settings.setValue("diagrameditor/dynamic_text_font", QETUtils::fontToString(font));
QString fontInfos = font.family() + " " +
QString::number(font.pointSize()) + " (" +
font.styleName() + ")";
@@ -549,11 +556,11 @@ void GeneralConfigurationPage::on_m_indi_text_font_pb_clicked()
bool ok;
QSettings settings;
QFont curFont;
curFont.fromString(settings.value("diagrameditor/independent_text_font", "Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular").toString());
QETUtils::fontFromString(curFont, settings.value("diagrameditor/independent_text_font", "Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular").toString());
QFont font = QFontDialog::getFont(&ok, curFont, this);
if (ok)
{
settings.setValue("diagrameditor/independent_text_font", font.toString());
settings.setValue("diagrameditor/independent_text_font", QETUtils::fontToString(font));
QString fontInfos = font.family() + " " +
QString::number(font.pointSize()) + " (" +
font.styleName() + ")";
@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>955</width>
<height>556</height>
<height>570</height>
</rect>
</property>
<property name="windowTitle">
@@ -17,7 +17,7 @@
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>2</number>
<number>0</number>
</property>
<widget class="QWidget" name="tab_3">
<attribute name="title">
@@ -59,6 +59,27 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="grid_startup_cb">
<property name="text">
<string>Afficher la grille par défaut (appliqué au prochain lancement)</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="guides_startup_cb">
<property name="text">
<string>Afficher les guides par défaut (appliqué au prochain lancement)</string>
</property>
</widget>
</item>
<item>
<widget class="Line" name="line_5">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="m_use_windows_mode_rb">
<property name="text">
@@ -350,7 +371,7 @@
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_11">
<widget class="QLabel" name="label_23">
<property name="text">
<string>Répertoire des Macros utilisateur</string>
</property>
@@ -911,30 +932,10 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
<string>Affichage Grille</string>
</property>
<layout class="QGridLayout" name="gridLayout_5">
<item row="0" column="1">
<spacer name="horizontalSpacer_10">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>555</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="4">
<widget class="QLabel" name="label_18">
<item row="0" column="2">
<widget class="QLabel" name="label_19">
<property name="text">
<string>max:</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="Label_Diagram_Grid_PointSize">
<property name="text">
<string>Taille des points de la grille de Diagram-Editor : 1 - 5</string>
<string>min:</string>
</property>
</widget>
</item>
@@ -954,10 +955,39 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QLabel" name="label_19">
<item row="0" column="1">
<spacer name="horizontalSpacer_10">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>555</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="3">
<widget class="QSpinBox" name="ElementEditor_Grid_PointSize_min_sb">
<property name="minimumSize">
<size>
<width>60</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>5</number>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="Label_Diagram_Grid_PointSize">
<property name="text">
<string>min:</string>
<string>Taille des points de la grille de Diagram-Editor : 1 - 5</string>
</property>
</widget>
</item>
@@ -977,13 +1007,6 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_20">
<property name="text">
<string>Taille des points de la grille de l'éditeur d'éléments : 1 - 5</string>
</property>
</widget>
</item>
<item row="1" column="5">
<widget class="QSpinBox" name="ElementEditor_Grid_PointSize_max_sb">
<property name="minimumSize">
@@ -1007,19 +1030,10 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
</property>
</widget>
</item>
<item row="1" column="3">
<widget class="QSpinBox" name="ElementEditor_Grid_PointSize_min_sb">
<property name="minimumSize">
<size>
<width>60</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>5</number>
<item row="0" column="4">
<widget class="QLabel" name="label_18">
<property name="text">
<string>max:</string>
</property>
</widget>
</item>
@@ -1030,6 +1044,13 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_20">
<property name="text">
<string>Taille des points de la grille de l'éditeur d'éléments : 1 - 5</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -1110,7 +1131,6 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
</layout>
</widget>
<tabstops>
<tabstop>tabWidget</tabstop>
<tabstop>m_use_system_color_cb</tabstop>
<tabstop>m_use_gesture_trackpad</tabstop>
<tabstop>m_zoom_out_beyond_folio</tabstop>
@@ -0,0 +1,113 @@
#include "guidespropertieswidget.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QPushButton>
#include <QTableWidget>
#include <QHeaderView>
#include <QComboBox>
#include <QDoubleSpinBox>
#include <QColorDialog>
GuidesPropertiesWidget::GuidesPropertiesWidget(QWidget *parent)
: QWidget(parent)
{
setupUi();
}
GuidesPropertiesWidget::~GuidesPropertiesWidget() {}
void GuidesPropertiesWidget::setupUi() {
QVBoxLayout *main_layout = new QVBoxLayout(this);
m_table = new QTableWidget(0, 3, this);
m_table->setHorizontalHeaderLabels({tr("Orientation"), tr("Position"), tr("Couleur")});
m_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
m_add_btn = new QPushButton(tr("Ajouter"), this);
m_remove_btn = new QPushButton(tr("Supprimer"), this);
QHBoxLayout *btn_layout = new QHBoxLayout();
btn_layout->addWidget(m_add_btn);
btn_layout->addWidget(m_remove_btn);
btn_layout->addStretch();
main_layout->addWidget(m_table);
main_layout->addLayout(btn_layout);
connect(m_add_btn, &QPushButton::clicked, this, &GuidesPropertiesWidget::addGuide);
connect(m_remove_btn, &QPushButton::clicked, this, &GuidesPropertiesWidget::removeGuide);
}
QList<Diagram::Guide> GuidesPropertiesWidget::guides() const {
QList<Diagram::Guide> list;
for (int row = 0; row < m_table->rowCount(); ++row) {
QComboBox *combo = qobject_cast<QComboBox*>(m_table->cellWidget(row, 0));
QDoubleSpinBox *spin = qobject_cast<QDoubleSpinBox*>(m_table->cellWidget(row, 1));
QPushButton *colorBtn = qobject_cast<QPushButton*>(m_table->cellWidget(row, 2));
if (combo && spin && colorBtn) {
Diagram::Guide g;
g.orientation = (combo->currentIndex() == 0) ? Diagram::Guide::Horizontal : Diagram::Guide::Vertical;
g.position = spin->value();
g.color = colorBtn->property("color").value<QColor>();
list.append(g);
}
}
return list;
}
void GuidesPropertiesWidget::setGuides(const QList<Diagram::Guide> &guides) {
m_table->setRowCount(0);
for (const auto &g : guides) {
addGuide();
int row = m_table->rowCount() - 1;
QComboBox *combo = qobject_cast<QComboBox*>(m_table->cellWidget(row, 0));
QDoubleSpinBox *spin = qobject_cast<QDoubleSpinBox*>(m_table->cellWidget(row, 1));
QPushButton *colorBtn = qobject_cast<QPushButton*>(m_table->cellWidget(row, 2));
if (combo && spin && colorBtn) {
combo->setCurrentIndex(g.orientation == Diagram::Guide::Horizontal ? 0 : 1);
spin->setValue(g.position);
colorBtn->setProperty("color", g.color);
colorBtn->setStyleSheet(QString("background-color: %1; color: white; font-weight: bold;").arg(g.color.name()));
}
}
}
void GuidesPropertiesWidget::addGuide() {
int row = m_table->rowCount();
m_table->insertRow(row);
QComboBox *combo = new QComboBox(this);
combo->addItems({tr("Horizontal"), tr("Vertical")});
m_table->setCellWidget(row, 0, combo);
QDoubleSpinBox *spin = new QDoubleSpinBox(this);
spin->setRange(-10000.0, 10000.0);
spin->setDecimals(2);
spin->setValue(100.0);
m_table->setCellWidget(row, 1, spin);
QPushButton *colorBtn = new QPushButton(tr("Couleur"), this);
QColor defaultColor = Qt::lightGray;
colorBtn->setProperty("color", defaultColor);
colorBtn->setStyleSheet(QString("background-color: %1; color: white; font-weight: bold;").arg(defaultColor.name()));
connect(colorBtn, &QPushButton::clicked, [this, colorBtn]() {
QColor c = QColorDialog::getColor(colorBtn->property("color").value<QColor>(), this);
if (c.isValid()) {
colorBtn->setProperty("color", c);
colorBtn->setStyleSheet(QString("background-color: %1; color: white; font-weight: bold;").arg(c.name()));
}
});
m_table->setCellWidget(row, 2, colorBtn);
}
void GuidesPropertiesWidget::removeGuide() {
int row = m_table->currentRow();
if (row >= 0) {
m_table->removeRow(row);
}
}
@@ -0,0 +1,32 @@
#ifndef GUIDESPROPERTIESWIDGET_H
#define GUIDESPROPERTIESWIDGET_H
#include <QWidget>
#include <QList>
#include "../../diagram.h"
class QTableWidget;
class QPushButton;
class GuidesPropertiesWidget : public QWidget {
Q_OBJECT
public:
explicit GuidesPropertiesWidget(QWidget *parent = nullptr);
~GuidesPropertiesWidget() override;
QList<Diagram::Guide> guides() const;
void setGuides(const QList<Diagram::Guide> &guides);
private slots:
void addGuide();
void removeGuide();
private:
void setupUi();
QTableWidget *m_table;
QPushButton *m_add_btn;
QPushButton *m_remove_btn;
};
#endif // GUIDESPROPERTIESWIDGET_H
+50 -3
View File
@@ -22,6 +22,7 @@
#include "../autoNum/ui/folioautonumbering.h"
#include "../autoNum/ui/formulaautonumberingw.h"
#include "../autoNum/ui/selectautonumw.h"
#include "../project/projectpropertieshandler.h"
#include "../qeticons.h"
#include "../qetproject.h"
#include "../borderpropertieswidget.h"
@@ -161,6 +162,13 @@ void ProjectMainConfigPage::applyProjectConf()
m_project -> setProjectProperties(new_properties);
modified_project = true;
}
ProjectUsageTracker &usage_tracker = m_project -> projectPropertiesHandler().usageTracker();
if (usage_tracker.isEnabled() != usage_enabled_cb_ -> isChecked()) {
usage_tracker.setEnabled(usage_enabled_cb_ -> isChecked());
modified_project = true;
}
if (modified_project) {
m_project -> setModified(true);
}
@@ -191,6 +199,12 @@ void ProjectMainConfigPage::initWidgets()
project_variables_label_ -> setWordWrap(true);
project_variables_ = new DiagramContextWidget();
project_variables_ -> setContext(DiagramContext());
usage_label_ = new QLabel(tr("Temps passé sur ce projet :", "label when configuring"));
usage_value_ = new QLabel();
usage_enabled_cb_ = new QCheckBox(tr("Suivre le temps passé sur ce projet (uniquement enregistré localement dans ce fichier)", "checkbox label"));
usage_reset_pb_ = new QPushButton(tr("Réinitialiser", "button label"));
connect(usage_reset_pb_, &QPushButton::clicked, this, &ProjectMainConfigPage::resetUsageTracker);
}
/**
@@ -207,6 +221,16 @@ void ProjectMainConfigPage::initLayout()
main_layout0 -> addSpacing(10);
main_layout0 -> addWidget(project_variables_label_);
main_layout0 -> addWidget(project_variables_);
main_layout0 -> addSpacing(10);
QHBoxLayout *usage_layout0 = new QHBoxLayout();
usage_layout0 -> addWidget(usage_label_);
usage_layout0 -> addWidget(usage_value_);
usage_layout0 -> addStretch();
usage_layout0 -> addWidget(usage_reset_pb_);
main_layout0 -> addLayout(usage_layout0);
main_layout0 -> addWidget(usage_enabled_cb_);
setLayout(main_layout0);
this -> setMinimumWidth(680);
@@ -219,6 +243,27 @@ void ProjectMainConfigPage::readValuesFromProject()
{
title_value_ -> setText(m_project -> title());
project_variables_ -> setContext(m_project -> projectProperties());
const ProjectUsageTracker &usage_tracker = m_project -> projectPropertiesHandler().usageTracker();
const qint64 total_seconds = usage_tracker.secondsSpent();
usage_value_ -> setText(tr("%1 h %2 min", "hours and minutes of time spent on a project")
.arg(total_seconds / 3600)
.arg((total_seconds % 3600) / 60));
usage_enabled_cb_ -> setChecked(usage_tracker.isEnabled());
}
/**
@brief ProjectMainConfigPage::resetUsageTracker
Reset the accumulated "time spent on this project" counter to zero and
refresh its displayed value. Applies immediately (not staged behind
OK/Cancel like the other fields on this page), since it isn't
destructive to any actual project content.
*/
void ProjectMainConfigPage::resetUsageTracker()
{
m_project -> projectPropertiesHandler().usageTracker().resetSecondsSpent();
m_project -> setModified(true);
usage_value_ -> setText(tr("%1 h %2 min", "hours and minutes of time spent on a project").arg(0).arg(0));
}
/**
@@ -229,6 +274,8 @@ void ProjectMainConfigPage::adjustReadOnly()
{
bool is_read_only = m_project -> isReadOnly();
title_value_ -> setReadOnly(is_read_only);
usage_enabled_cb_ -> setDisabled(is_read_only);
usage_reset_pb_ -> setDisabled(is_read_only);
}
//######################################################################################//
@@ -419,7 +466,7 @@ void ProjectAutoNumConfigPage::saveContextElement()
m_saw_element->contextComboBox()->addItem(tr("Sans nom"));
}
// If the text isn't yet to the autonum of the project, add this new item to the combo box.
else if ( !m_project -> elementAutoNum().keys().contains( m_saw_element->contextComboBox()->currentText()))
else if ( !m_project -> elementAutoNum().contains( m_saw_element->contextComboBox()->currentText()))
{
m_project->addElementAutoNum(m_saw_element->contextComboBox()->currentText(), m_saw_element->toNumContext());
m_project->setCurrrentElementAutonum(m_saw_element->contextComboBox()->currentText());
@@ -461,7 +508,7 @@ void ProjectAutoNumConfigPage::saveContextConductor()
m_saw_conductor->contextComboBox()-> addItem(tr("Sans nom"));
}
// If the text isn't yet to the autonum of the project, add this new item to the combo box.
else if ( !m_project -> conductorAutoNum().keys().contains( m_saw_conductor->contextComboBox()->currentText()))
else if ( !m_project -> conductorAutoNum().contains( m_saw_conductor->contextComboBox()->currentText()))
{
project()->addConductorAutoNum(m_saw_conductor->contextComboBox()->currentText(), m_saw_conductor->toNumContext());
project()->setCurrentConductorAutoNum(m_saw_conductor->contextComboBox()->currentText());
@@ -489,7 +536,7 @@ void ProjectAutoNumConfigPage::saveContextFolio()
m_saw_folio->contextComboBox() -> addItem(tr("Sans nom"));
}
// If the text isn't yet to the autonum of the project, add this new item to the combo box.
else if ( !m_project -> folioAutoNum().keys().contains( m_saw_folio->contextComboBox()->currentText())) {
else if ( !m_project -> folioAutoNum().contains( m_saw_folio->contextComboBox()->currentText())) {
project()->addFolioAutoNum(m_saw_folio->contextComboBox()->currentText(), m_saw_folio->toNumContext());
m_saw_folio->contextComboBox() -> addItem(m_saw_folio->contextComboBox()->currentText());
}
+10 -1
View File
@@ -21,6 +21,8 @@
class QLabel;
class QLineEdit;
class QCheckBox;
class QPushButton;
class QETProject;
class BorderPropertiesWidget;
class ConductorPropertiesWidget;
@@ -108,7 +110,10 @@ class ProjectMainConfigPage : public ProjectConfigPage {
void initLayout() override;
void readValuesFromProject() override;
void adjustReadOnly() override;
private slots:
void resetUsageTracker();
// attributes
protected:
QLabel *title_label_;
@@ -116,6 +121,10 @@ class ProjectMainConfigPage : public ProjectConfigPage {
QLabel *title_information_;
QLabel *project_variables_label_;
DiagramContextWidget *project_variables_;
QLabel *usage_label_;
QLabel *usage_value_;
QCheckBox *usage_enabled_cb_;
QPushButton *usage_reset_pb_;
};
class ProjectAutoNumConfigPage : public ProjectConfigPage {
+238
View File
@@ -0,0 +1,238 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "contactgroupselectiondialog.h"
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QTableWidget>
#include <QPushButton>
#include <QLabel>
#include <QHeaderView>
#include <QItemSelectionModel>
ContactGroupSelectionDialog::ContactGroupSelectionDialog(
const QVector<ElementData::SlaveContactGroup> &groups,
const QSet<int> &usedGroupIndices,
const ElementData &slaveData,
QWidget *parent)
: QDialog(parent)
{
m_used_indices = usedGroupIndices;
setWindowTitle(tr("Sélectionner un groupe de contacts"));
auto *main_layout = new QVBoxLayout(this);
auto *info_label = new QLabel(
tr("Sélectionnez le groupe de contacts à assigner à cet élément esclave :"));
main_layout->addWidget(info_label);
// Determine max terminal count for dynamic columns
int max_terminals = 0;
for (const auto &g : groups) {
if (g.terminalCount > max_terminals)
max_terminals = g.terminalCount;
}
// Build column headers: #, Type, Sous-type, Contacts, Bornes, T1, T2, ...
QStringList headers;
headers << tr("#")
<< tr("Type")
<< tr("Sous-type")
<< tr("Contacts")
<< tr("Bornes");
for (int t = 0; t < max_terminals; ++t) {
headers << tr("T%1").arg(t + 1);
}
m_table = new QTableWidget(groups.size(), headers.size(), this);
m_table->setHorizontalHeaderLabels(headers);
m_table->setSelectionBehavior(QTableWidget::SelectRows);
m_table->setSelectionMode(QTableWidget::SingleSelection);
m_table->setEditTriggers(QTableWidget::NoEditTriggers);
m_table->verticalHeader()->setVisible(false);
m_table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
m_table->horizontalHeader()->setStretchLastSection(false);
m_table->verticalHeader()->setDefaultSectionSize(24);
// Populate table rows
for (int row = 0; row < groups.size(); ++row) {
const auto &g = groups.at(row);
auto *num_item = new QTableWidgetItem(QString::number(row + 1));
num_item->setTextAlignment(Qt::AlignCenter);
m_table->setItem(row, 0, num_item);
m_table->setItem(row, 1, new QTableWidgetItem(typeToString(g.type)));
m_table->setItem(row, 2, new QTableWidgetItem(subtypeToString(g.subtype)));
auto *ctc_item = new QTableWidgetItem(QString::number(g.contactCount));
ctc_item->setTextAlignment(Qt::AlignCenter);
m_table->setItem(row, 3, ctc_item);
auto *term_item = new QTableWidgetItem(QString::number(g.terminalCount));
term_item->setTextAlignment(Qt::AlignCenter);
m_table->setItem(row, 4, term_item);
// Fill T1..TN label columns
for (int t = 0; t < max_terminals; ++t) {
int col = 5 + t;
QString label;
if (t < g.labels.size()) {
label = g.labels.at(t);
} else if (t < g.terminalCount) {
label = tr("T%1").arg(t + 1);
}
auto *label_item = new QTableWidgetItem(label);
label_item->setTextAlignment(Qt::AlignCenter);
m_table->setItem(row, col, label_item);
}
}
// Mark used group rows as disabled (grayed out)
QFont disabled_font;
disabled_font.setStrikeOut(true);
QColor disabled_color(Qt::gray);
for (int row = 0; row < groups.size(); ++row) {
bool disabled = false;
QString reason;
if (m_used_indices.contains(row)) {
disabled = true;
reason = tr("(déjà assigné)");
} else {
const auto &g = groups.at(row);
if (g.type != slaveData.m_slave_state) {
disabled = true;
reason = tr("(état ne correspond pas)");
} else if (g.subtype != slaveData.m_slave_type) {
disabled = true;
reason = tr("(sous-type ne correspond pas)");
} else if (g.contactCount != slaveData.m_contact_count) {
disabled = true;
reason = tr("(nombre de contacts ne correspond pas)");
}
}
if (disabled) {
m_disabled_rows.insert(row);
for (int col = 0; col < m_table->columnCount(); ++col) {
auto *item = m_table->item(row, col);
if (item) {
item->setForeground(disabled_color);
item->setFont(disabled_font);
item->setFlags(item->flags() & ~Qt::ItemIsSelectable);
item->setToolTip(reason);
}
}
// Show circled "?" as a widget in the first column
auto *num_item = m_table->item(row, 0);
if (num_item) {
num_item->setToolTip(reason);
auto *question_label = new QLabel(this);
question_label->setText("?");
question_label->setAlignment(Qt::AlignCenter);
question_label->setStyleSheet(
"QLabel {"
" color: #1a73e8;"
" font-weight: bold;"
" font-size: 8px;"
" border: 1.5px solid #1a73e8;"
" border-radius: 7px;"
" min-width: 12px; max-width: 12px;"
" min-height: 12px; max-height: 12px;"
"}");
question_label->setToolTip(reason);
m_table->setCellWidget(row, 0, question_label);
}
}
}
// Select first available (non-disabled) row
int first_available = 0;
for (int row = 0; row < groups.size(); ++row) {
if (!m_disabled_rows.contains(row)) {
first_available = row;
break;
}
}
m_table->selectRow(first_available);
main_layout->addWidget(m_table);
// Calculate width based on column count: base + ~60px per column
int table_width = 50 + headers.size() * 65;
int table_height = 60 + groups.size() * 26;
m_table->setMinimumWidth(table_width);
m_table->setMinimumHeight(table_height);
// Buttons
auto *button_layout = new QHBoxLayout();
button_layout->addStretch();
m_ok_button = new QPushButton(tr("OK"), this);
button_layout->addWidget(m_ok_button);
auto *cancel_button = new QPushButton(tr("Annuler"), this);
button_layout->addWidget(cancel_button);
main_layout->addLayout(button_layout);
// Connections
connect(m_ok_button, &QPushButton::clicked, this, [this]() {
auto *item = m_table->currentItem();
if (item && !m_disabled_rows.contains(item->row())) {
m_selected_index = item->row();
accept();
}
});
connect(cancel_button, &QPushButton::clicked, this, &QDialog::reject);
connect(m_table, &QTableWidget::cellDoubleClicked, this, [this](int row, int) {
if (!m_disabled_rows.contains(row)) {
m_selected_index = row;
accept();
}
});
}
int ContactGroupSelectionDialog::selectedIndex() const
{
return m_selected_index;
}
QString ContactGroupSelectionDialog::typeToString(ElementData::SlaveState type)
{
switch (type) {
case ElementData::NO: return tr("Normalement ouvert");
case ElementData::NC: return tr("Normalement fermé");
case ElementData::SW: return tr("Inverseur");
case ElementData::Other: return tr("Autre");
default: return tr("Inconnu");
}
}
QString ContactGroupSelectionDialog::subtypeToString(ElementData::SlaveType subtype)
{
switch (subtype) {
case ElementData::SSimple: return tr("Simple");
case ElementData::Power: return tr("Puissance");
case ElementData::DelayOn: return tr("Temporisé travail");
case ElementData::DelayOff: return tr("Temporisé repos");
case ElementData::delayOnOff: return tr("Temporisé travail & repos");
default: return tr("Inconnu");
}
}
+61
View File
@@ -0,0 +1,61 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef CONTACTGROUPSELECTIONDIALOG_H
#define CONTACTGROUPSELECTIONDIALOG_H
#include <QDialog>
#include <QVector>
#include <QSet>
#include "../properties/elementdata.h"
class QTableWidget;
class QTableWidgetItem;
class QPushButton;
/**
@brief The ContactGroupSelectionDialog class
A dialog that displays all slave contact groups defined by a master element
in a table format. The user can select one group to assign to a slave element.
Columns: #, Type, Subtype, Contacts, Terminals, T1, T2, ..., TN
*/
class ContactGroupSelectionDialog : public QDialog
{
Q_OBJECT
public:
ContactGroupSelectionDialog(
const QVector<ElementData::SlaveContactGroup> &groups,
const QSet<int> &usedGroupIndices,
const ElementData &slaveData,
QWidget *parent = nullptr);
int selectedIndex() const;
private:
QTableWidget *m_table = nullptr;
QPushButton *m_ok_button = nullptr;
int m_selected_index = -1;
QSet<int> m_used_indices;
QSet<int> m_disabled_rows;
static QString typeToString(ElementData::SlaveState type);
static QString subtypeToString(ElementData::SlaveType subtype);
};
#endif // CONTACTGROUPSELECTIONDIALOG_H
+9 -9
View File
@@ -130,7 +130,7 @@ QList<QStandardItem *> DynamicElementTextModel::itemsForText(
{
QList <QStandardItem *> qsi_list;
if(m_texts_list.keys().contains(deti))
if(m_texts_list.contains(deti))
return qsi_list;
QStandardItem *qsi = new QStandardItem(deti->toPlainText());
@@ -718,7 +718,7 @@ QUndoCommand *DynamicElementTextModel::undoForEditedGroup(
*/
void DynamicElementTextModel::addGroup(ElementTextItemGroup *group)
{
if(m_groups_list.keys().contains(group))
if(m_groups_list.contains(group))
return;
//Group
@@ -869,7 +869,7 @@ void DynamicElementTextModel::addGroup(ElementTextItemGroup *group)
*/
void DynamicElementTextModel::removeGroup(ElementTextItemGroup *group)
{
if(m_groups_list.keys().contains(group))
if(m_groups_list.contains(group))
{
QModelIndex group_index = m_groups_list.value(group)->index();
this->removeRow(group_index.row(), group_index.parent());
@@ -896,7 +896,7 @@ void DynamicElementTextModel::removeTextFromGroup(DynamicElementTextItem *deti,
{
Q_UNUSED(group)
if(m_texts_list.keys().contains(deti))
if(m_texts_list.contains(deti))
{
QStandardItem *text_item = m_texts_list.value(deti);
QModelIndex text_index = indexFromItem(text_item);
@@ -961,7 +961,7 @@ ElementTextItemGroup *DynamicElementTextModel::groupFromItem(
QModelIndex DynamicElementTextModel::indexFromGroup(
ElementTextItemGroup *group) const
{
if(m_groups_list.keys().contains(group))
if(m_groups_list.contains(group))
return m_groups_list.value(group)->index();
else
return QModelIndex();
@@ -1371,7 +1371,7 @@ void DynamicElementTextModel::setConnection(DynamicElementTextItem *deti, bool s
{
if(set)
{
if(m_hash_text_connect.keys().contains(deti))
if(m_hash_text_connect.contains(deti))
return;
QList<QMetaObject::Connection> connection_list;
@@ -1390,7 +1390,7 @@ void DynamicElementTextModel::setConnection(DynamicElementTextItem *deti, bool s
}
else
{
if(!m_hash_text_connect.keys().contains(deti))
if(!m_hash_text_connect.contains(deti))
return;
for (const QMetaObject::Connection& con : m_hash_text_connect.value(deti))
@@ -1412,7 +1412,7 @@ void DynamicElementTextModel::setConnection(ElementTextItemGroup *group, bool se
{
if(set)
{
if(m_hash_group_connect.keys().contains(group))
if(m_hash_group_connect.contains(group))
return;
QList<QMetaObject::Connection> connection_list;
@@ -1429,7 +1429,7 @@ void DynamicElementTextModel::setConnection(ElementTextItemGroup *group, bool se
}
else
{
if(!m_hash_group_connect.keys().contains(group))
if(!m_hash_group_connect.contains(group))
return;
for (const QMetaObject::Connection& con : m_hash_group_connect.value(group))
+58 -9
View File
@@ -16,7 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "elementinfowidget.h"
#include <QCheckBox>
#include "../diagram.h"
#include "../qetapp.h"
#include "../qetgraphicsitem/element.h"
@@ -161,6 +161,13 @@ void ElementInfoWidget::enableLiveEdit()
for (ElementInfoPartWidget *eipw : m_eipw_list)
connect(eipw, &ElementInfoPartWidget::textChanged, this, &ElementInfoWidget::apply);
connect(ui->m_auto_num_locked_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
if (m_potential_isolating_cb) {
connect(m_potential_isolating_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
}
if (m_exclude_from_bom_cb) {
connect(m_exclude_from_bom_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
}
}
/**
@@ -172,6 +179,13 @@ void ElementInfoWidget::disableLiveEdit()
for (ElementInfoPartWidget *eipw : m_eipw_list)
disconnect(eipw, &ElementInfoPartWidget::textChanged, this, &ElementInfoWidget::apply);
disconnect(ui->m_auto_num_locked_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
if (m_potential_isolating_cb) {
disconnect(m_potential_isolating_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
}
if (m_exclude_from_bom_cb) {
disconnect(m_exclude_from_bom_cb, &QCheckBox::clicked, this, &ElementInfoWidget::apply);
}
}
/**
@@ -193,16 +207,34 @@ void ElementInfoWidget::buildInterface()
ui->scroll_vlayout->addWidget(eipw);
m_eipw_list << eipw;
}
ui->scroll_vlayout->addStretch();
// Existing potential isolating checkbox
m_potential_isolating_cb = new QCheckBox(tr("Séparation de potentiel"), this);
m_potential_isolating_cb->setStyleSheet(QStringLiteral("margin: 5px; font-weight: bold;"));
// English: Initialize and style the BOM exclusion checkbox
m_exclude_from_bom_cb = new QCheckBox(tr("Exclure de la nomenclature"), this);
m_exclude_from_bom_cb->setStyleSheet(QStringLiteral("margin: 5px; font-weight: bold;"));
if (QVBoxLayout *mainLayout = qobject_cast<QVBoxLayout*>(this->layout())) {
mainLayout->insertWidget(1, m_potential_isolating_cb);
// English: Insert the new checkbox into the main vertical layout
mainLayout->insertWidget(2, m_exclude_from_bom_cb);
}
// English: BOM exclusion applies to all elements, so it's always visible
m_exclude_from_bom_cb->setVisible(true);
// Show checkbox only if the element is a terminal
if (m_element.data()->elementData().m_type == ElementData::Terminal) {
ui->m_auto_num_locked_cb->setVisible(true);
m_potential_isolating_cb->setVisible(true);
} else {
ui->m_auto_num_locked_cb->setVisible(false);
m_potential_isolating_cb->setVisible(false);
}
}
/**
@brief ElementInfoWidget::infoPartWidgetForKey
@param key
@@ -211,7 +243,7 @@ void ElementInfoWidget::buildInterface()
*/
ElementInfoPartWidget *ElementInfoWidget::infoPartWidgetForKey(const QString &key) const
{
for (const auto &eipw : qAsConst(m_eipw_list))
for (const auto &eipw : std::as_const(m_eipw_list))
{
if (eipw->key() == key)
return eipw;
@@ -243,6 +275,17 @@ void ElementInfoWidget::updateUi()
if (m_element->elementData().m_type == ElementData::Terminal) {
QString lock_value = element_info.value(QStringLiteral("auto_num_locked")).toString();
ui->m_auto_num_locked_cb->setChecked(lock_value == QLatin1String("true"));
// English: Load the potential isolating status from the element information mapping
if (m_potential_isolating_cb) {
QString isolating_value = element_info.value(QStringLiteral("potential_isolating")).toString();
m_potential_isolating_cb->setChecked(isolating_value == QLatin1String("true"));
}
}
// English: Load the BOM exclusion status from the element information mapping
if (m_exclude_from_bom_cb) {
QString exclude_bom_value = element_info.value(QStringLiteral("exclude_from_bom")).toString();
m_exclude_from_bom_cb->setChecked(exclude_bom_value == QLatin1String("true"));
}
if (m_live_edit) {
@@ -258,14 +301,13 @@ DiagramContext ElementInfoWidget::currentInfo() const
{
DiagramContext info_;
for (const auto &eipw : qAsConst(m_eipw_list))
for (const auto &eipw : std::as_const(m_eipw_list))
{
//add value only if they're something to store
//add value only if they're something to store
if (!eipw->text().isEmpty())
{
QString txt{eipw->text()};
//remove line feed and carriage return
//remove line feed and carriage return
txt.remove(QStringLiteral("\r"));
txt.remove(QStringLiteral("\n"));
info_.addValue(eipw->key(), txt);
@@ -275,10 +317,17 @@ DiagramContext ElementInfoWidget::currentInfo() const
// Save the auto numbering lock status
if (m_element->elementData().m_type == ElementData::Terminal) {
info_.addValue(QStringLiteral("auto_num_locked"), ui->m_auto_num_locked_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"));
if (m_potential_isolating_cb) {
info_.addValue(QStringLiteral("potential_isolating"), m_potential_isolating_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"));
}
}
if (m_exclude_from_bom_cb) {
info_.addValue(QStringLiteral("exclude_from_bom"), m_exclude_from_bom_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"));
}
return info_;
}
/**
@brief ElementInfoWidget::firstActivated
Slot activated when this widget is show.
+3
View File
@@ -27,6 +27,7 @@ class Element;
class QUndoCommand;
class ElementInfoPartWidget;
class ChangeElementInformationCommand;
class QCheckBox;
namespace Ui {
class ElementInfoWidget;
@@ -71,6 +72,8 @@ class ElementInfoWidget : public AbstractElementPropertiesEditorWidget
private:
Ui::ElementInfoWidget *ui;
QList <ElementInfoPartWidget *> m_eipw_list;
QCheckBox *m_potential_isolating_cb = nullptr;
QCheckBox *m_exclude_from_bom_cb = nullptr;
bool m_first_activation;
bool m_ui_builded = false;
};
+5 -1
View File
@@ -28,6 +28,7 @@
#include "elementinfowidget.h"
#include "linksingleelementwidget.h"
#include "masterpropertieswidget.h"
#include "plclinkwidget.h"
#include <QLabel>
#include <QUndoStack>
@@ -303,7 +304,10 @@ void ElementPropertiesWidget::updateUi()
m_list_editor << new ElementInfoWidget(m_element, this);
break;
case Element::Slave:
m_list_editor << new LinkSingleElementWidget(m_element, this);
if (m_element->elementData().m_slave_type == ElementData::PLCSlave)
m_list_editor << new PlcLinkWidget(m_element, this);
else
m_list_editor << new LinkSingleElementWidget(m_element, this);
break;
case Element::Terminale:
m_list_editor << new ElementInfoWidget(m_element, this);
+102 -10
View File
@@ -16,6 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "linksingleelementwidget.h"
#include "contactgroupselectiondialog.h"
#include "../qetgraphicsitem/masterelement.h"
#include "../qetgraphicsitem/conductor.h"
#include "../diagram.h"
@@ -24,10 +25,12 @@
#include "../elementprovider.h"
#include "../undocommand/linkelementcommand.h"
#include "../qetinformation.h"
#include "../qetproject.h"
#include "../ui_linksingleelementwidget.h"
#include <QTreeWidgetItem>
#include <QInputDialog>
/**
@brief LinkSingleElementWidget::LinkSingleElementWidget
@@ -175,6 +178,7 @@ void LinkSingleElementWidget::apply()
m_unlink = false;
m_element_to_link = nullptr;
m_pending_qtwi = nullptr;
m_pending_group_index = -1;
}
/**
@@ -188,8 +192,12 @@ QUndoCommand *LinkSingleElementWidget::associatedUndo() const
if (m_element_to_link || m_unlink)
{
if (m_element_to_link)
if (m_element_to_link) {
undo->setLink(m_element_to_link);
if (m_pending_group_index >= 0) {
undo->setGroupIndex(m_pending_group_index);
}
}
else if (m_unlink)
undo->unlinkAll();
@@ -386,17 +394,24 @@ QVector <QPointer<Element>> LinkSingleElementWidget::availableElements()
//If element is linked, remove is parent from the list
if(!m_element->isFree()) elmt_vector.removeAll(m_element->linkedElements().first());
// Filter out all master elements from the list
// Filter out incompatible elements: PLC and non-PLC must not mix
const bool element_is_plc = (m_element->elementData().m_type == ElementData::Slave &&
m_element->elementData().m_slave_type == ElementData::PLCSlave);
for (int i = elmt_vector.size() - 1; i >= 0; --i) {
Element *elmt = elmt_vector.at(i);
// If the item in the list is a master
if (elmt->linkType() == Element::Master) {
const bool master_is_plc = (elmt->elementData().m_master_type == ElementData::PLC);
// We convert the generic element pointer into a MasterElement pointer
MasterElement *master = static_cast<MasterElement*>(elmt);
// PLC slave can only link to PLC master, and vice versa
if (element_is_plc != master_is_plc) {
elmt_vector.removeAt(i);
continue;
}
// If the master is full, we'll remove it from the list!
MasterElement *master = static_cast<MasterElement*>(elmt);
if (master->isFull()) {
elmt_vector.removeAt(i);
}
@@ -529,9 +544,86 @@ 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;
//If linking a slave to a master with contact groups, show group selection dialog
if (m_element->linkType() == Element::Slave
&& m_element_to_link
&& m_element_to_link->linkType() == Element::Master)
{
// Check if this is a PLC master
if (m_element_to_link->elementData().m_master_type == ElementData::PLC)
{
// Show PLC IO selection dialog
const auto &plc_data = m_element_to_link->elementData().plcMasterData();
if (!plc_data.ios.isEmpty())
{
// Collect already-used IO indices from the master
QSet<int> used_indices;
for (Element *linked : m_element_to_link->linkedElements()) {
int idx = m_element_to_link->groupIndexForElement(linked);
if (idx >= 0) {
used_indices.insert(idx);
}
}
// Build selection dialog
QStringList items;
for (int i = 0; i < plc_data.ios.size(); ++i) {
const auto &io = plc_data.ios.at(i);
QString label = QString("[%1] %2 - %3")
.arg(i + 1)
.arg(io.address)
.arg(io.functionText);
if (used_indices.contains(i))
label += tr(" (déjà utilisé)");
items << label;
}
bool ok = false;
int selected = QInputDialog::getInt(
this,
tr("Sélectionner un IO PLC"),
tr("IO disponible:"),
0, 0, plc_data.ios.size() - 1, 1, &ok);
if (ok && selected >= 0) {
m_pending_group_index = selected;
} else {
m_element_to_link = nullptr;
return;
}
}
}
else
{
// Normal contact group selection for non-PLC masters
const auto &groups = m_element_to_link->elementData().m_slave_contact_groups;
if (!groups.isEmpty())
{
// Collect already-used group indices from the master
QSet<int> used_indices;
for (Element *linked : m_element_to_link->linkedElements()) {
int idx = m_element_to_link->groupIndexForElement(linked);
if (idx >= 0) {
used_indices.insert(idx);
}
}
ContactGroupSelectionDialog dlg(groups, used_indices,
m_element->elementData(), this);
if (dlg.exec() == QDialog::Accepted && dlg.selectedIndex() >= 0) {
m_pending_group_index = dlg.selectedIndex();
} else {
m_element_to_link = nullptr;
return;
}
}
}
}
if(m_live_edit)
{
apply();
@@ -552,7 +644,7 @@ void LinkSingleElementWidget::linkTriggered()
Qt::NoBrush));
}
}
for (int i=0 ; i<6 ; i++)
{
m_qtwi_at_context_menu->setBackground(i,
@@ -561,7 +653,7 @@ void LinkSingleElementWidget::linkTriggered()
}
m_pending_qtwi = m_qtwi_at_context_menu;
}
}
/**
+4
View File
@@ -22,6 +22,8 @@
#include "abstractelementpropertieseditorwidget.h"
#include <QHash>
#include <QDialog>
#include <QComboBox>
class QTreeWidgetItem;
class Element;
@@ -101,6 +103,8 @@ class LinkSingleElementWidget : public AbstractElementPropertiesEditorWidget
Element *m_showed_element = nullptr,
*m_element_to_link = nullptr;
int m_pending_group_index = -1;
QMenu *m_context_menu{nullptr};
QAction *m_link_action{nullptr},
*m_show_qtwi{nullptr},
+627 -94
View File
@@ -16,16 +16,30 @@
* along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "masterpropertieswidget.h"
#include "contactgroupselectiondialog.h"
#include "../qetproject.h"
#include "../diagram.h"
#include "../diagramposition.h"
#include "../elementprovider.h"
#include "../qetgraphicsitem/element.h"
#include "../undocommand/linkelementcommand.h"
#include "ui_masterpropertieswidget.h"
#include "../properties/elementdata.h"
#include <QListWidgetItem>
#include <QMessageBox>
#include <QTableWidget>
#include <QSpinBox>
#include <QCheckBox>
#include <QComboBox>
#include <QHeaderView>
#include <QMenu>
#include <QAction>
#include <QClipboard>
#include <QApplication>
#include <QShortcut>
#include <QPushButton>
/**
* @brief MasterPropertiesWidget::MasterPropertiesWidget
@@ -143,7 +157,7 @@ void MasterPropertiesWidget::setElement(Element *element)
disconnect(m_project, SIGNAL(diagramRemoved(QETProject*,Diagram*)),
this, SLOT(diagramWasdeletedFromProject()));
if(Q_LIKELY(element->diagram() && element->diagram()->project()))
if(Q_LIKELY(element->diagram() && element->diagram()->project()))
{
m_project = element->diagram()->project();
connect(m_project, SIGNAL(diagramRemoved(QETProject*,Diagram*)),
@@ -157,7 +171,7 @@ void MasterPropertiesWidget::setElement(Element *element)
disconnect(m_element.data(), &Element::linkedElementChanged,
this, &MasterPropertiesWidget::updateUi);
m_element = element;
m_element = element;
connect(m_element.data(), &Element::linkedElementChanged,
this, &MasterPropertiesWidget::updateUi);
@@ -176,6 +190,8 @@ void MasterPropertiesWidget::apply()
{
if (QUndoCommand *undo = associatedUndo())
m_element -> diagram() -> undoStack().push(undo);
m_pending_group_indices.clear();
}
/**
@@ -224,8 +240,23 @@ QUndoCommand* MasterPropertiesWidget::associatedUndo() const
if (to_link.isEmpty())
undo->unlinkAll();
else
{
undo->setLink(to_link);
//Pass group indices for newly linked slaves
if (!m_pending_group_indices.isEmpty())
{
QMap<Element*, int> indices;
for (Element *slave : to_link)
{
if (m_pending_group_indices.contains(slave))
indices[slave] = m_pending_group_indices.value(slave);
}
if (!indices.isEmpty())
undo->setGroupIndices(indices);
}
}
return undo;
}
@@ -241,98 +272,7 @@ bool MasterPropertiesWidget::setLiveEdit(bool live_edit)
return true;
}
/**
* @brief MasterPropertiesWidget::updateUi
* Build the interface of the widget
*/
void MasterPropertiesWidget::updateUi()
{
ui->m_free_tree_widget->clear();
ui->m_link_tree_widget->clear();
m_qtwi_hash.clear();
if (Q_UNLIKELY(!m_project))
return;
ElementProvider elmt_prov(m_project);
QSettings settings;
//Build the list of free available element
QList <QTreeWidgetItem *> items_list;
for(const auto &elmt : elmt_prov.freeElement(ElementData::Slave))
{
QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_free_tree_widget);
qtwi->setIcon(0, elmt->pixmap());
if(settings.value("genericpanel/folio", false).toBool())
{
autonum::sequentialNumbers seq;
QString F =autonum::AssignVariables::formulaToLabel(
elmt->diagram()->border_and_titleblock.folio(),
seq,
elmt->diagram(),
elmt);
qtwi->setText(1, F);
}
else
{
qtwi->setText(1, QString::number(
elmt->diagram()->folioIndex()
+ 1));
}
qtwi->setText(2, elmt->diagram()->title());
qtwi->setText(4, elmt->diagram()->convertPosition(
elmt->scenePos()).toString());
items_list.append(qtwi);
m_qtwi_hash.insert(qtwi, elmt);
}
ui->m_free_tree_widget->addTopLevelItems(items_list);
items_list.clear();
//Build the list of already linked element
const QList<Element *> link_list = m_element->linkedElements();
for(Element *elmt : link_list)
{
QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_link_tree_widget);
qtwi->setIcon(0, elmt->pixmap());
if(settings.value("genericpanel/folio", false).toBool())
{
autonum::sequentialNumbers seq;
QString F =autonum::AssignVariables::formulaToLabel(
elmt->diagram()->border_and_titleblock.folio(),
seq,
elmt->diagram(),
elmt);
qtwi->setText(1, F);
}
else
{
qtwi->setText(1, QString::number(
elmt->diagram()->folioIndex()
+ 1));
}
qtwi->setText(2, elmt->diagram()->title());
qtwi->setText(3, elmt->diagram()->convertPosition(
elmt->scenePos()).toString());
items_list.append(qtwi);
m_qtwi_hash.insert(qtwi, elmt);
}
if(items_list.count())
ui->m_link_tree_widget->addTopLevelItems(items_list);
QVariant v = settings.value("link-element-widget/master-state");
if(!v.isNull())
{
ui->m_free_tree_widget->header()->restoreState(v.toByteArray());
ui->m_link_tree_widget->header()->restoreState(v.toByteArray());
}
}
/**
* @brief MasterPropertiesWidget::headerCustomContextMenuRequested
@@ -375,6 +315,38 @@ void MasterPropertiesWidget::on_link_button_clicked()
QTreeWidgetItem *qtwi = ui->m_free_tree_widget->currentItem();
if (qtwi)
{
Element *slave_elmt = m_qtwi_hash.value(qtwi);
//If master has contact groups, show group selection dialog
const auto &groups = m_element->elementData().m_slave_contact_groups;
if (!groups.isEmpty() && slave_elmt)
{
// Collect already-used group indices from the master
QSet<int> used_indices;
for (Element *linked : m_element->linkedElements()) {
int idx = m_element->groupIndexForElement(linked);
if (idx >= 0) {
used_indices.insert(idx);
}
}
// Don't mark the current slave as used (it might be relinked)
if (slave_elmt->linkedElements().contains(m_element)) {
int current_idx = m_element->groupIndexForElement(slave_elmt);
if (current_idx >= 0) {
used_indices.remove(current_idx);
}
}
ContactGroupSelectionDialog dlg(groups, used_indices,
slave_elmt->elementData(), this);
if (dlg.exec() == QDialog::Accepted && dlg.selectedIndex() >= 0) {
m_pending_group_indices[slave_elmt] = dlg.selectedIndex();
} else {
return;
}
}
ui->m_free_tree_widget->takeTopLevelItem(
ui->m_free_tree_widget->indexOfTopLevelItem(qtwi));
ui->m_link_tree_widget->insertTopLevelItem(0, qtwi);
@@ -497,3 +469,564 @@ void MasterPropertiesWidget::customContextMenu(const QPoint &pos, int i)
m_context_menu->addAction(m_show_element);
m_context_menu->popup(point);
}
/**
* @brief MasterPropertiesWidget::updateUi
* Build the interface of the widget
*/
void MasterPropertiesWidget::updateUi()
{
ui->m_free_tree_widget->clear();
ui->m_link_tree_widget->clear();
m_qtwi_hash.clear();
// Check if this is a PLC master
bool is_plc = m_element &&
m_element->elementData().m_type == ElementData::Master &&
m_element->elementData().m_master_type == ElementData::PLC;
// Show/hide normal master UI and PLC UI
ui->m_free_tree_widget->setVisible(!is_plc);
ui->m_link_tree_widget->setVisible(!is_plc);
ui->label->setVisible(!is_plc);
ui->label_2->setVisible(!is_plc);
ui->link_button->setVisible(!is_plc);
ui->unlink_button->setVisible(!is_plc);
// In PLC mode, make hidden widgets take no space in the grid layout
if (is_plc) {
for (QWidget *w : {static_cast<QWidget*>(ui->m_free_tree_widget),
static_cast<QWidget*>(ui->m_link_tree_widget),
static_cast<QWidget*>(ui->label),
static_cast<QWidget*>(ui->label_2),
static_cast<QWidget*>(ui->link_button),
static_cast<QWidget*>(ui->unlink_button)}) {
w->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
w->setMinimumSize(0, 0);
w->setMaximumSize(0, 0);
}
for (int col = 0; col < ui->gridLayout->columnCount(); ++col)
ui->gridLayout->setColumnStretch(col, col < 2 ? 1 : 0);
}
if (is_plc) {
// Create PLC widget if not yet created
if (!m_plc_widget) {
m_plc_widget = new QWidget(ui->gridLayout->parentWidget());
auto *plc_layout = new QVBoxLayout(m_plc_widget);
plc_layout->setContentsMargins(0, 0, 0, 0);
// Table
m_plc_table = new QTableWidget(m_plc_widget);
m_plc_table->setColumnCount(5);
m_plc_table->setHorizontalHeaderLabels({
tr("Type"), tr("Adresse"), tr("Fonction"),
tr("Commentaire"), tr("Réf. croisée")
});
m_plc_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectRows);
m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_plc_table->setMinimumHeight(200);
plc_layout->addWidget(m_plc_table);
connect(m_plc_table, &QTableWidget::cellChanged, this, &MasterPropertiesWidget::plcIOCellChanged);
// Context menu for the PLC table
m_plc_table->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_plc_table, &QTableWidget::customContextMenuRequested,
this, &MasterPropertiesWidget::plcShowTableContextMenu);
// Ctrl+V shortcut for paste
auto *paste_shortcut = new QShortcut(QKeySequence::Paste, m_plc_table);
connect(paste_shortcut, &QShortcut::activated, this, &MasterPropertiesWidget::plcPasteFromClipboard);
}
ui->gridLayout->addWidget(m_plc_widget, 0, 0, 1, 5);
m_plc_widget->setVisible(true);
// Load PLC data into table (block signals to prevent cellChanged during population)
m_plc_updating = true;
m_plc_table->blockSignals(true);
// Clear existing rows
m_plc_table->setRowCount(0);
ElementData::PlcMasterData plc_data = m_element->elementData().plcMasterData();
m_plc_table->setRowCount(plc_data.ios.size());
for (int row = 0; row < plc_data.ios.size(); ++row) {
const ElementData::PlcIO &io = plc_data.ios.at(row);
// Type combo (read-only — type is defined in the editor)
auto *type_cb = new QComboBox(m_plc_table);
type_cb->setEnabled(false);
type_cb->setStyleSheet("QComboBox { background-color: #f0f0f0; }");
QStringList plc_types = ElementData::plcIOTypeList();
for (int t = 0; t < plc_types.size(); ++t) {
type_cb->addItem(plc_types.at(t), t);
}
type_cb->setCurrentIndex(static_cast<int>(io.type));
m_plc_table->setCellWidget(row, 0, type_cb);
// Address
auto *addr_item = new QTableWidgetItem(io.address);
m_plc_table->setItem(row, 1, addr_item);
// Function text
auto *func_item = new QTableWidgetItem(io.functionText);
m_plc_table->setItem(row, 2, func_item);
// Comment
auto *comment_item = new QTableWidgetItem(io.comment);
m_plc_table->setItem(row, 3, comment_item);
// CrossRef (read-only)
auto *crossref_item = new QTableWidgetItem(io.crossRef);
crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable);
m_plc_table->setItem(row, 4, crossref_item);
}
m_plc_table->blockSignals(false);
m_plc_updating = false;
} else {
// Hide PLC widget if it was shown before
if (m_plc_widget)
m_plc_widget->setVisible(false);
// Restore normal widget size policies
for (QWidget *w : {static_cast<QWidget*>(ui->m_free_tree_widget),
static_cast<QWidget*>(ui->m_link_tree_widget),
static_cast<QWidget*>(ui->label),
static_cast<QWidget*>(ui->label_2),
static_cast<QWidget*>(ui->link_button),
static_cast<QWidget*>(ui->unlink_button)}) {
w->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
w->setMinimumSize(0, 0);
w->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
}
for (int col = 0; col < ui->gridLayout->columnCount(); ++col)
ui->gridLayout->setColumnStretch(col, 0);
if (Q_UNLIKELY(!m_project))
return;
ElementProvider elmt_prov(m_project);
QSettings settings;
//Build the list of free available element
QList <QTreeWidgetItem *> items_list;
for(const auto &elmt : elmt_prov.freeElement(ElementData::Slave))
{
// Filter out PLC-Slave elements — they should only link to PLC-Master
if (elmt->elementData().m_slave_type == ElementData::PLCSlave)
continue;
QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_free_tree_widget);
qtwi->setIcon(0, elmt->pixmap());
if(settings.value("genericpanel/folio", false).toBool())
{
autonum::sequentialNumbers seq;
QString F =autonum::AssignVariables::formulaToLabel(
elmt->diagram()->border_and_titleblock.folio(),
seq,
elmt->diagram(),
elmt);
qtwi->setText(1, F);
}
else
{
qtwi->setText(1, QString::number(
elmt->diagram()->folioIndex()
+ 1));
}
qtwi->setText(2, elmt->diagram()->title());
qtwi->setText(4, elmt->diagram()->convertPosition(
elmt->scenePos()).toString());
items_list.append(qtwi);
m_qtwi_hash.insert(qtwi, elmt);
}
ui->m_free_tree_widget->addTopLevelItems(items_list);
items_list.clear();
//Build the list of already linked element
const QList<Element *> link_list = m_element->linkedElements();
for(Element *elmt : link_list)
{
QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_link_tree_widget);
qtwi->setIcon(0, elmt->pixmap());
if(settings.value("genericpanel/folio", false).toBool())
{
autonum::sequentialNumbers seq;
QString F =autonum::AssignVariables::formulaToLabel(
elmt->diagram()->border_and_titleblock.folio(),
seq,
elmt->diagram(),
elmt);
qtwi->setText(1, F);
}
else
{
qtwi->setText(1, QString::number(
elmt->diagram()->folioIndex()
+ 1));
}
qtwi->setText(2, elmt->diagram()->title());
qtwi->setText(3, elmt->diagram()->convertPosition(
elmt->scenePos()).toString());
items_list.append(qtwi);
m_qtwi_hash.insert(qtwi, elmt);
}
if(items_list.count())
ui->m_link_tree_widget->addTopLevelItems(items_list);
QVariant v = settings.value("link-element-widget/master-state");
if(!v.isNull())
{
ui->m_free_tree_widget->header()->restoreState(v.toByteArray());
ui->m_link_tree_widget->header()->restoreState(v.toByteArray());
}
}
}
// ============================================================================
// PLC IO Table methods
// ============================================================================
/**
* @brief MasterPropertiesWidget::plcPasteFromClipboard
* Paste IO data from clipboard.
*
* Two modes:
* 1. Single-column data (no tabs, e.g. copied vertically from A1:A6):
* Values are pasted vertically down the current column.
* 2. Multi-column data (tab-separated, e.g. copied from a row in Excel):
* Each line becomes a separate IO row.
*/
void MasterPropertiesWidget::plcPasteFromClipboard()
{
if (!m_plc_table || !m_element)
return;
QString clipboard_text = QApplication::clipboard()->text();
if (clipboard_text.isEmpty())
return;
QStringList lines = clipboard_text.split('\n', Qt::SkipEmptyParts);
if (lines.isEmpty())
return;
bool has_tabs = false;
for (const QString &line : lines) {
if (line.contains('\t')) {
has_tabs = true;
break;
}
}
m_plc_updating = true;
if (!has_tabs) {
// Vertical paste: values go down the same column, within existing rows only
int target_col = m_plc_table->currentColumn();
if (target_col < 0) target_col = 0;
int target_row = m_plc_table->currentRow();
if (target_row < 0) target_row = 0;
int max_rows = m_plc_table->rowCount();
for (int i = 0; i < lines.size(); ++i) {
int row = target_row + i;
if (row >= max_rows) break;
setCellFromValue(row, target_col, lines.at(i).trimmed());
}
} else {
// Horizontal paste: each line is a separate IO row, within existing rows only
int target_row = m_plc_table->currentRow();
if (target_row < 0) target_row = 0;
int max_rows = m_plc_table->rowCount();
for (int i = 0; i < lines.size(); ++i) {
int row = target_row + i;
if (row >= max_rows) break;
QStringList cells = lines.at(i).split('\t');
for (int c = 0; c < cells.size(); ++c) {
if (c > 6) break;
setCellFromValue(row, c, cells.at(c).trimmed());
}
}
}
m_plc_updating = false;
plcUpdateDisplaySettings();
}
/**
* @brief MasterPropertiesWidget::setCellFromValue
* Set a single table cell value, respecting the column widget type.
*/
void MasterPropertiesWidget::setCellFromValue(int row, int col, const QString &val)
{
if (!m_plc_table || row < 0 || col < 0 || col > 6)
return;
if (col == 0) {
// Type combo
auto *type_cb = qobject_cast<QComboBox*>(m_plc_table->cellWidget(row, col));
if (!type_cb) {
type_cb = new QComboBox(m_plc_table);
QStringList plc_types = ElementData::plcIOTypeList();
for (int t = 0; t < plc_types.size(); ++t)
type_cb->addItem(plc_types.at(t), t);
m_plc_table->setCellWidget(row, col, type_cb);
connect(type_cb, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, [this, row](int) { plcIOCellChanged(row, 0); });
}
if (!val.isEmpty()) {
QStringList plc_types = ElementData::plcIOTypeList();
for (int t = 0; t < plc_types.size(); ++t) {
if (plc_types.at(t).compare(val, Qt::CaseInsensitive) == 0) {
type_cb->setCurrentIndex(t);
return;
}
}
}
}
else if (col == 5) {
// Terminal count spinbox
auto *tc_sb = qobject_cast<QSpinBox*>(m_plc_table->cellWidget(row, col));
if (!tc_sb) {
tc_sb = new QSpinBox(m_plc_table);
tc_sb->setMinimum(1);
tc_sb->setMaximum(4);
m_plc_table->setCellWidget(row, col, tc_sb);
connect(tc_sb, QOverload<int>::of(&QSpinBox::valueChanged),
this, [this, row](int) { plcIOCellChanged(row, 5); });
}
bool ok;
int v = val.toInt(&ok);
if (ok && v >= 1 && v <= 4)
tc_sb->setValue(v);
}
else if (col == 6) {
// CrossRef - read-only
auto *item = new QTableWidgetItem(val);
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
m_plc_table->setItem(row, col, item);
}
else {
// Text columns: Address, Function, Comment
m_plc_table->setItem(row, col, new QTableWidgetItem(val));
}
}
/**
* @brief MasterPropertiesWidget::plcAddRow
* Add a new empty row to the PLC IO table
*/
void MasterPropertiesWidget::plcAddRow()
{
if (!m_plc_table)
return;
int row = m_plc_table->rowCount();
m_plc_table->insertRow(row);
// Type combo
auto *type_cb = new QComboBox(m_plc_table);
QStringList plc_types = ElementData::plcIOTypeList();
for (int t = 0; t < plc_types.size(); ++t) {
type_cb->addItem(plc_types.at(t), t);
}
m_plc_table->setCellWidget(row, 0, type_cb);
connect(type_cb, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, [this, row](int) { plcIOCellChanged(row, 0); });
m_plc_table->setItem(row, 1, new QTableWidgetItem());
m_plc_table->setItem(row, 2, new QTableWidgetItem());
m_plc_table->setItem(row, 3, new QTableWidgetItem());
m_plc_table->setItem(row, 4, new QTableWidgetItem());
auto *tc_sb = new QSpinBox(m_plc_table);
tc_sb->setMinimum(1);
tc_sb->setMaximum(4);
tc_sb->setValue(1);
m_plc_table->setCellWidget(row, 5, tc_sb);
connect(tc_sb, QOverload<int>::of(&QSpinBox::valueChanged),
this, [this, row](int) { plcIOCellChanged(row, 5); });
auto *crossref_item = new QTableWidgetItem();
crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable);
m_plc_table->setItem(row, 6, crossref_item);
}
/**
* @brief MasterPropertiesWidget::plcRemoveRow
* Remove selected rows from the PLC IO table
*/
void MasterPropertiesWidget::plcRemoveRow()
{
if (!m_plc_table)
return;
QModelIndexList selected = m_plc_table->selectionModel()->selectedRows();
if (selected.isEmpty())
return;
// Remove from bottom to top to preserve indices
std::sort(selected.begin(), selected.end(),
[](const QModelIndex &a, const QModelIndex &b) { return a.row() > b.row(); });
for (const QModelIndex &idx : selected) {
m_plc_table->removeRow(idx.row());
}
}
/**
* @brief MasterPropertiesWidget::plcMoveRowUp
* Move selected row up
*/
void MasterPropertiesWidget::plcMoveRowUp()
{
if (!m_plc_table)
return;
int row = m_plc_table->currentRow();
if (row <= 0)
return;
// Swap with row above
for (int col = 0; col < m_plc_table->columnCount(); ++col) {
QWidget *w1 = m_plc_table->cellWidget(row, col);
QWidget *w2 = m_plc_table->cellWidget(row - 1, col);
m_plc_table->setCellWidget(row, col, w2);
m_plc_table->setCellWidget(row - 1, col, w1);
QTableWidgetItem *i1 = m_plc_table->item(row, col);
QTableWidgetItem *i2 = m_plc_table->item(row - 1, col);
m_plc_table->setItem(row, col, i2);
m_plc_table->setItem(row - 1, col, i1);
}
m_plc_table->setCurrentCell(row - 1, m_plc_table->currentColumn());
}
/**
* @brief MasterPropertiesWidget::plcMoveRowDown
* Move selected row down
*/
void MasterPropertiesWidget::plcMoveRowDown()
{
if (!m_plc_table)
return;
int row = m_plc_table->currentRow();
if (row < 0 || row >= m_plc_table->rowCount() - 1)
return;
// Swap with row below
for (int col = 0; col < m_plc_table->columnCount(); ++col) {
QWidget *w1 = m_plc_table->cellWidget(row, col);
QWidget *w2 = m_plc_table->cellWidget(row + 1, col);
m_plc_table->setCellWidget(row, col, w2);
m_plc_table->setCellWidget(row + 1, col, w1);
QTableWidgetItem *i1 = m_plc_table->item(row, col);
QTableWidgetItem *i2 = m_plc_table->item(row + 1, col);
m_plc_table->setItem(row, col, i2);
m_plc_table->setItem(row + 1, col, i1);
}
m_plc_table->setCurrentCell(row + 1, m_plc_table->currentColumn());
}
/**
* @brief MasterPropertiesWidget::plcIOCellChanged
* Called when a cell in the PLC IO table changes
*/
void MasterPropertiesWidget::plcIOCellChanged(int row, int column)
{
Q_UNUSED(row)
Q_UNUSED(column)
if (m_plc_updating || !m_element || !m_plc_table)
return;
// Save immediately
plcUpdateDisplaySettings();
}
/**
* @brief MasterPropertiesWidget::plcUpdateDisplaySettings
* Update the IO data from the table (display settings are managed by the editor only)
*/
void MasterPropertiesWidget::plcUpdateDisplaySettings()
{
if (m_plc_updating || !m_element || !m_plc_table)
return;
m_plc_updating = true;
// Preserve existing display settings
ElementData ed = m_element->elementData();
ElementData::PlcMasterData plc_data = ed.plcMasterData();
plc_data.ios.clear();
// Read IOs from table
for (int row = 0; row < m_plc_table->rowCount(); ++row) {
ElementData::PlcIO io;
auto *type_cb = qobject_cast<QComboBox *>(m_plc_table->cellWidget(row, 0));
if (type_cb)
io.type = static_cast<ElementData::PlcIOType>(type_cb->currentData().toInt());
auto *addr_item = m_plc_table->item(row, 1);
if (addr_item)
io.address = addr_item->text();
auto *func_item = m_plc_table->item(row, 2);
if (func_item)
io.functionText = func_item->text();
auto *comment_item = m_plc_table->item(row, 3);
if (comment_item)
io.comment = comment_item->text();
auto *crossref_item = m_plc_table->item(row, 4);
if (crossref_item)
io.crossRef = crossref_item->text();
plc_data.ios.append(io);
}
ed.setPlcMasterData(plc_data);
m_element->setElementData(ed);
// Trigger update of the cross ref item
if (m_element->scene())
m_element->update();
m_plc_updating = false;
}
/**
* @brief MasterPropertiesWidget::plcShowTableContextMenu
* Show context menu for the PLC IO table
*/
void MasterPropertiesWidget::plcShowTableContextMenu(const QPoint &pos)
{
Q_UNUSED(pos)
if (!m_plc_table)
return;
QMenu menu;
menu.addAction(tr("Coller depuis le presse-papiers"), this, &MasterPropertiesWidget::plcPasteFromClipboard);
menu.exec(m_plc_table->mapToGlobal(pos));
}
+27
View File
@@ -20,6 +20,7 @@
#include <QWidget>
#include <QHash>
#include <QMap>
#include "abstractelementpropertieseditorwidget.h"
@@ -30,6 +31,10 @@ class Diagram;
class QTreeWidgetItem;
class QMenu;
class QAction;
class QTableWidget;
class QSpinBox;
class QCheckBox;
class QComboBox;
namespace Ui {
class MasterPropertiesWidget;
@@ -74,9 +79,21 @@ class MasterPropertiesWidget : public AbstractElementPropertiesEditorWidget
void diagramWasdeletedFromProject();
void customContextMenu(const QPoint &pos, int i=0);
// PLC IO table slots
void plcPasteFromClipboard();
void setCellFromValue(int row, int col, const QString &val);
void plcAddRow();
void plcRemoveRow();
void plcMoveRowUp();
void plcMoveRowDown();
void plcIOCellChanged(int row, int column);
void plcUpdateDisplaySettings();
void plcShowTableContextMenu(const QPoint &pos);
private:
Ui::MasterPropertiesWidget *ui;
QHash <QTreeWidgetItem *, Element *> m_qtwi_hash;
QMap <Element *, int> m_pending_group_indices;
QTreeWidgetItem *m_qtwi_at_context_menu = nullptr;
QPointer <Element> m_showed_element;
QETProject *m_project;
@@ -86,6 +103,16 @@ class MasterPropertiesWidget : public AbstractElementPropertiesEditorWidget
*m_show_qtwi,
*m_show_element,
*m_save_header_state;
// PLC-specific members
QWidget *m_plc_widget = nullptr;
QTableWidget *m_plc_table = nullptr;
QCheckBox *m_plc_break_checkboxes[4] = {nullptr, nullptr, nullptr, nullptr};
QSpinBox *m_plc_break_spinboxes[4] = {nullptr, nullptr, nullptr, nullptr};
QSpinBox *m_plc_row_height_spinbox = nullptr;
QList<QCheckBox *> m_plc_col_visibility_checkboxes;
QList<QSpinBox *> m_plc_col_width_spinboxes;
bool m_plc_updating = false; // Guard against recursive updates
};
#endif // MASTERPROPERTIESWIDGET_H
+1 -1
View File
@@ -17,7 +17,7 @@
*/
#include "multipastedialog.h"
#include "../qetproject.h"
#include "../conductorautonumerotation.h"
#include "../diagram.h"
#include "../diagramcommands.h"
+303
View File
@@ -0,0 +1,303 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "kautosavefile.h"
#include <QCryptographicHash>
#include <QDir>
#include <QFileInfo>
#include <QLockFile>
#include <QRandomGenerator>
#include <QStandardPaths>
#include <QTextStream>
namespace {
const auto autosaveSuffix = QStringLiteral(".qetautosave");
QString staleFilesDir()
{
auto data_dir = QStandardPaths::writableLocation(
QStandardPaths::AppDataLocation);
while (data_dir.endsWith(QLatin1Char('/'))) {
data_dir.chop(1);
}
if (data_dir.isEmpty()) {
return {};
}
return data_dir + QDir::separator() + QStringLiteral("autosave");
}
QString metadataFileName(const QString &autosave_file_name)
{
return autosave_file_name + QStringLiteral(".path");
}
QString lockFileName(const QString &autosave_file_name)
{
return autosave_file_name + QStringLiteral(".lock");
}
QUrl normalizeManagedFile(const QUrl &url)
{
if (url.isEmpty()) {
return {};
}
if (url.isLocalFile() || url.scheme().isEmpty()) {
const auto path = url.isLocalFile() ? url.toLocalFile() : url.path();
QUrl normalized;
normalized.setPath(QDir::cleanPath(QFileInfo(path).absoluteFilePath()));
return normalized;
}
return url;
}
QString storedManagedFile(const QUrl &url)
{
if (url.isLocalFile() || url.scheme().isEmpty()) {
return url.path();
}
return url.toString(QUrl::FullyEncoded);
}
QUrl managedFileFromStorage(const QString &stored_path)
{
if (!stored_path.startsWith(QLatin1Char('/'))) {
return QUrl(stored_path);
}
QUrl url;
url.setPath(QDir::cleanPath(stored_path));
return url;
}
bool writeManagedFileMetadata(const QString &autosave_file_name, const QUrl &managed_file)
{
QFile metadata_file(metadataFileName(autosave_file_name));
if (!metadata_file.open(QIODevice::WriteOnly
| QIODevice::Truncate
| QIODevice::Text)) {
return false;
}
QTextStream stream(&metadata_file);
stream << storedManagedFile(managed_file) << '\n';
stream.flush();
return metadata_file.error() == QFile::NoError;
}
QUrl readManagedFileMetadata(const QString &autosave_file_name)
{
QFile metadata_file(metadataFileName(autosave_file_name));
if (!metadata_file.open(QIODevice::ReadOnly | QIODevice::Text)) {
return {};
}
QTextStream stream(&metadata_file);
const auto stored_path = stream.readLine().trimmed();
if (stored_path.isEmpty()) {
return {};
}
return normalizeManagedFile(managedFileFromStorage(stored_path));
}
QString autosaveFileName(const QUrl &managed_file)
{
const auto stored_path = storedManagedFile(managed_file);
const auto digest = QCryptographicHash::hash(
stored_path.toUtf8(),
QCryptographicHash::Sha256).toHex().left(16);
auto basename = QFileInfo(managed_file.path()).fileName();
if (basename.isEmpty()) {
basename = QStringLiteral("autosave");
}
auto encoded_basename = QString::fromLatin1(
QUrl::toPercentEncoding(basename));
if (encoded_basename.size() > 80) {
encoded_basename.truncate(80);
}
const auto random = QString::number(
QRandomGenerator::global()->generate64(), 16);
return QStringLiteral("%1_%2_%3%4").arg(
encoded_basename,
QString::fromLatin1(digest),
random,
autosaveSuffix);
}
QStringList findAllStaleFiles(const QString &application_name)
{
Q_UNUSED(application_name)
const auto dir_path = staleFilesDir();
if (dir_path.isEmpty()) {
return {};
}
QDir dir(dir_path);
const auto entries = dir.entryList(
{QStringLiteral("*") + autosaveSuffix},
QDir::Files);
QStringList files;
for (const auto &entry : entries) {
files << dir.absoluteFilePath(entry);
}
return files;
}
bool autosaveFileIsRecoverable(const QString &autosave_file_name)
{
QLockFile lock(lockFileName(autosave_file_name));
lock.setStaleLockTime(60 * 1000);
if (!lock.tryLock()) {
return false;
}
lock.unlock();
return true;
}
} // namespace
KAutoSaveFile::KAutoSaveFile(const QUrl &filename, QObject *parent) :
QFile{parent}
{
setManagedFile(filename);
}
KAutoSaveFile::KAutoSaveFile(QObject *parent) :
QFile{parent}
{
}
KAutoSaveFile::~KAutoSaveFile()
{
releaseLock();
}
QUrl KAutoSaveFile::managedFile() const
{
return m_managed_file;
}
void KAutoSaveFile::setManagedFile(const QUrl &filename)
{
releaseLock();
m_managed_file = normalizeManagedFile(filename);
m_managed_file_name_changed = true;
setFileName({});
}
void KAutoSaveFile::releaseLock()
{
if (m_lock && m_lock->isLocked()) {
const auto autosave_file_name = fileName();
m_lock.reset();
if (!autosave_file_name.isEmpty()) {
QFile::remove(metadataFileName(autosave_file_name));
remove();
}
} else {
m_lock.reset();
}
}
bool KAutoSaveFile::open(OpenMode openmode)
{
if (m_managed_file.isEmpty()) {
return false;
}
if (m_managed_file_name_changed) {
const auto stale_dir = staleFilesDir();
if (stale_dir.isEmpty() || !QDir().mkpath(stale_dir)) {
return false;
}
setFileName(QDir(stale_dir).absoluteFilePath(
autosaveFileName(m_managed_file)));
if (!writeManagedFileMetadata(fileName(), m_managed_file)) {
setFileName({});
return false;
}
m_managed_file_name_changed = false;
}
if (!QFile::open(openmode)) {
return false;
}
if (!m_lock) {
m_lock = std::make_unique<QLockFile>(lockFileName(fileName()));
m_lock->setStaleLockTime(60 * 1000);
}
if (m_lock->isLocked() || m_lock->tryLock()) {
return true;
}
close();
return false;
}
QList<KAutoSaveFile *> KAutoSaveFile::staleFiles(
const QUrl &url,
const QString &applicationName)
{
const auto managed_file_filter = normalizeManagedFile(url);
QList<KAutoSaveFile *> stale_files;
for (const auto &file : findAllStaleFiles(applicationName)) {
const auto managed_file = readManagedFileMetadata(file);
if (managed_file.isEmpty()) {
continue;
}
if (!managed_file_filter.isEmpty()
&& managed_file != managed_file_filter) {
continue;
}
if (!autosaveFileIsRecoverable(file)) {
continue;
}
auto *stale_file = new KAutoSaveFile(managed_file);
stale_file->setFileName(file);
stale_file->m_managed_file_name_changed = false;
stale_files << stale_file;
}
return stale_files;
}
QList<KAutoSaveFile *> KAutoSaveFile::allStaleFiles(const QString &applicationName)
{
return staleFiles({}, applicationName);
}
+61
View File
@@ -0,0 +1,61 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef QET_KAUTOSAVEFILE_H
#define QET_KAUTOSAVEFILE_H
#include <QFile>
#include <QList>
#include <QUrl>
#include <memory>
class QLockFile;
/**
Small Qt-only replacement for the KAutoSaveFile API used by QET.
It stores crash-recovery files below the application data autosave folder
and protects each recovery file with QLockFile. The original managed file
path is kept in a sidecar file so allStaleFiles() can present recoverable
projects on the next startup.
*/
class KAutoSaveFile : public QFile
{
public:
explicit KAutoSaveFile(const QUrl &filename, QObject *parent = nullptr);
explicit KAutoSaveFile(QObject *parent = nullptr);
~KAutoSaveFile() override;
QUrl managedFile() const;
void setManagedFile(const QUrl &filename);
virtual void releaseLock();
bool open(OpenMode openmode) override;
static QList<KAutoSaveFile *> staleFiles(
const QUrl &url,
const QString &applicationName = QString());
static QList<KAutoSaveFile *> allStaleFiles(
const QString &applicationName = QString());
private:
QUrl m_managed_file;
std::unique_ptr<QLockFile> m_lock;
bool m_managed_file_name_changed = false;
};
#endif // QET_KAUTOSAVEFILE_H
+70
View File
@@ -0,0 +1,70 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "kcolorbutton.h"
#include <QColorDialog>
#include <QPalette>
namespace {
QColor fallbackColor()
{
return QPalette{}.color(QPalette::Button);
}
}
KColorButton::KColorButton(QWidget *parent) :
QPushButton{parent},
m_color{fallbackColor()}
{
connect(this, &QPushButton::clicked, this, &KColorButton::chooseColor);
updateButton();
}
QColor KColorButton::color() const
{
return m_color;
}
void KColorButton::setColor(const QColor &color)
{
m_color = color.isValid() ? color : fallbackColor();
updateButton();
}
void KColorButton::chooseColor()
{
const auto selected = QColorDialog::getColor(m_color, this);
if (!selected.isValid() || selected == m_color) {
return;
}
m_color = selected;
updateButton();
emit changed(m_color);
}
void KColorButton::updateButton()
{
setText(m_color.name());
auto pal = palette();
pal.setColor(QPalette::Button, m_color);
setAutoFillBackground(true);
setPalette(pal);
update();
}
+48
View File
@@ -0,0 +1,48 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef QET_KCOLORBUTTON_H
#define QET_KCOLORBUTTON_H
#include <QColor>
#include <QPushButton>
class KColorButton : public QPushButton
{
Q_OBJECT
public:
explicit KColorButton(QWidget *parent = nullptr);
QColor color() const;
public slots:
void setColor(const QColor &color);
signals:
void changed(const QColor &color);
private slots:
void chooseColor();
private:
void updateButton();
QColor m_color;
};
#endif // QET_KCOLORBUTTON_H
+49
View File
@@ -0,0 +1,49 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "kcolorcombo.h"
#include <QVariant>
KColorCombo::KColorCombo(QWidget *parent) :
QComboBox{parent}
{
connect(
this,
QOverload<int>::of(&QComboBox::activated),
this,
[this](int index) {
emit activated(itemData(index).value<QColor>());
});
}
void KColorCombo::setColors(const QList<QColor> &colors)
{
clear();
for (const auto &color : colors) {
addItem(color.name(), color);
}
}
QColor KColorCombo::color(int index) const
{
if (index < 0 || index >= count()) {
return {};
}
return itemData(index).value<QColor>();
}
+38
View File
@@ -0,0 +1,38 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef QET_KCOLORCOMBO_H
#define QET_KCOLORCOMBO_H
#include <QColor>
#include <QComboBox>
class KColorCombo : public QComboBox
{
Q_OBJECT
public:
explicit KColorCombo(QWidget *parent = nullptr);
void setColors(const QList<QColor> &colors);
QColor color(int index) const;
signals:
void activated(const QColor &color);
};
#endif // QET_KCOLORCOMBO_H
+371
View File
@@ -0,0 +1,371 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "plclinkwidget.h"
#include "../qetgraphicsitem/masterelement.h"
#include "../qetgraphicsitem/element.h"
#include "../elementprovider.h"
#include "../undocommand/linkelementcommand.h"
#include "../diagram.h"
#include "../qetproject.h"
#include "../properties/elementdata.h"
#include <QTreeWidget>
#include <QLineEdit>
#include <QPushButton>
#include <QLabel>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QMenu>
#include <QAction>
#include <QFont>
#include <QTimer>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QMenu>
#include <QAction>
PlcLinkWidget::PlcLinkWidget(Element *elmt, QWidget *parent)
: AbstractElementPropertiesEditorWidget(parent)
{
auto *main_layout = new QGridLayout(this);
// Row 0: Status label + buttons (shown when already linked)
m_label = new QLabel(tr("Cet élément est déjà lié"), this);
m_unlink_pb = new QPushButton(tr("Délier"), this);
m_show_this_pb = new QPushButton(tr("Voir cet élément"), this);
main_layout->addWidget(m_label, 0, 0);
main_layout->addWidget(m_unlink_pb, 0, 1);
main_layout->addWidget(m_show_this_pb, 0, 2);
// Row 1: Search field
m_search_field = new QLineEdit(this);
m_search_field->setPlaceholderText(tr("Recherche"));
main_layout->addWidget(m_search_field, 1, 0, 1, 3);
// Row 2: Tree widget
m_tree_widget = new QTreeWidget(this);
m_tree_widget->setHeaderLabels({
tr("Label"), tr("Type"), tr("Adresse"),
tr("Fonction"), tr("Commentaire"), tr("Anschlüsse")
});
m_tree_widget->setRootIsDecorated(true);
m_tree_widget->setIndentation(20);
m_tree_widget->setSelectionMode(QAbstractItemView::SingleSelection);
m_tree_widget->setContextMenuPolicy(Qt::CustomContextMenu);
m_tree_widget->header()->setStretchLastSection(false);
m_tree_widget->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
m_tree_widget->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
m_tree_widget->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
m_tree_widget->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
m_tree_widget->header()->setSectionResizeMode(4, QHeaderView::ResizeToContents);
m_tree_widget->header()->setSectionResizeMode(5, QHeaderView::ResizeToContents);
main_layout->addWidget(m_tree_widget, 2, 0, 1, 3);
// Row 3: Hidden masters note
m_hidden_masters_label = new QLabel(
tr("Remarque : les éléments maîtres ayant atteint leur nombre maximal "
"d'esclaves sont masqués."), this);
m_hidden_masters_label->setWordWrap(true);
QFont italic_font = m_hidden_masters_label->font();
italic_font.setItalic(true);
m_hidden_masters_label->setFont(italic_font);
m_hidden_masters_label->hide();
main_layout->addWidget(m_hidden_masters_label, 3, 0, 1, 3);
main_layout->setRowStretch(2, 1);
setMinimumWidth(500);
// Connections
connect(m_unlink_pb, &QPushButton::clicked,
this, &PlcLinkWidget::on_m_unlink_pb_clicked);
connect(m_show_this_pb, &QPushButton::clicked,
this, &PlcLinkWidget::on_m_show_this_pb_clicked);
connect(m_search_field, &QLineEdit::textEdited,
this, &PlcLinkWidget::on_m_search_field_textEdited);
connect(m_tree_widget, &QTreeWidget::customContextMenuRequested,
this, &PlcLinkWidget::on_m_tree_widget_customContextMenuRequested);
if (elmt)
setElement(elmt);
}
void PlcLinkWidget::setElement(Element *element)
{
if (m_element == element)
return;
m_element = element;
updateUi();
}
void PlcLinkWidget::apply()
{
QUndoCommand *undo = associatedUndo();
if (undo)
m_element->diagram()->undoStack().push(undo);
m_element_to_link = nullptr;
m_pending_io_index = -1;
}
QUndoCommand *PlcLinkWidget::associatedUndo() const
{
if (!m_element_to_link || m_pending_io_index < 0)
return nullptr;
LinkElementCommand *undo = new LinkElementCommand(m_element);
undo->setLink(m_element_to_link);
undo->setGroupIndex(m_pending_io_index);
return undo;
}
QString PlcLinkWidget::title() const
{
return tr("Automate (PLC)");
}
void PlcLinkWidget::updateUi()
{
if (m_element->isFree())
hideButtons();
else
showButtons();
buildPlcTree();
}
/**
@brief PlcLinkWidget::buildPlcTree
Build the tree showing all PLC-Masters with their IO entries.
IO entries already linked to a slave are shown as struck-through and greyed out.
*/
void PlcLinkWidget::buildPlcTree()
{
m_tree_widget->clear();
m_io_entry_hash.clear();
if (!m_element || !m_element->diagram() || !m_element->diagram()->project())
return;
ElementProvider ep(m_element->diagram()->project());
QVector<QPointer<Element>> masters = ep.find(ElementData::Master);
QSet<int> used_io_indices;
for (Element *elmt : masters) {
if (!elmt || elmt->elementData().m_master_type != ElementData::PLC)
continue;
const auto &plc_data = elmt->elementData().plcMasterData();
if (plc_data.ios.isEmpty())
continue;
// Skip full masters
MasterElement *me = static_cast<MasterElement*>(elmt);
if (me->isFull()) {
m_hidden_masters_label->show();
continue;
}
// Collect used IO indices for this master
used_io_indices.clear();
for (Element *linked : elmt->linkedElements()) {
int idx = elmt->groupIndexForElement(linked);
if (idx >= 0)
used_io_indices.insert(idx);
}
// Create parent item for this PLC-Master
auto *parent_item = new QTreeWidgetItem(m_tree_widget);
// Show master label + diagram info
QString master_label = elmt->actualLabel();
if (master_label.isEmpty())
master_label = elmt->name();
QString folio_info;
if (elmt->diagram()) {
folio_info = QString::number(elmt->diagram()->folioIndex() + 1);
}
parent_item->setText(0, QString("%1 (%2)")
.arg(master_label)
.arg(folio_info));
QFont bold_font = parent_item->font(0);
bold_font.setBold(true);
parent_item->setFont(0, bold_font);
parent_item->setExpanded(false);
// Add child items for each IO entry
for (int i = 0; i < plc_data.ios.size(); ++i) {
const auto &io = plc_data.ios.at(i);
auto *child_item = new QTreeWidgetItem(parent_item);
child_item->setText(1, ElementData::translatedPlcIOType(io.type));
child_item->setText(2, io.address);
child_item->setText(3, io.functionText);
child_item->setText(4, io.comment);
// Terminal count
child_item->setText(5, QString::number(io.terminalCount));
PlcIoEntry entry;
entry.master = elmt;
entry.ioIndex = i;
m_io_entry_hash.insert(child_item, entry);
// If this IO is already linked to a slave, grey it out and strike through
if (used_io_indices.contains(i)) {
QFont strike_font = child_item->font(0);
strike_font.setStrikeOut(true);
child_item->setFont(0, strike_font);
child_item->setFont(1, strike_font);
child_item->setFont(2, strike_font);
child_item->setFont(3, strike_font);
child_item->setFont(4, strike_font);
child_item->setFont(5, strike_font);
QBrush grey_brush(Qt::gray);
for (int col = 0; col < 6; ++col)
child_item->setForeground(col, grey_brush);
// Show which slave is linked
for (Element *linked : elmt->linkedElements()) {
if (elmt->groupIndexForElement(linked) == i) {
child_item->setToolTip(0,
tr("Lié à: %1").arg(linked->actualLabel()));
break;
}
}
child_item->setFlags(child_item->flags() & ~Qt::ItemIsSelectable);
}
}
}
}
void PlcLinkWidget::hideButtons()
{
m_label->hide();
m_unlink_pb->hide();
m_show_this_pb->hide();
m_search_field->show();
}
void PlcLinkWidget::showButtons()
{
m_label->show();
m_unlink_pb->show();
m_show_this_pb->show();
m_search_field->hide();
}
void PlcLinkWidget::on_m_search_field_textEdited(const QString &text)
{
for (int i = 0; i < m_tree_widget->topLevelItemCount(); ++i) {
QTreeWidgetItem *parent = m_tree_widget->topLevelItem(i);
bool any_child_visible = false;
for (int j = 0; j < parent->childCount(); ++j) {
QTreeWidgetItem *child = parent->child(j);
bool match = text.isEmpty();
if (!match) {
for (int col = 0; col < child->columnCount(); ++col) {
if (child->text(col).contains(text, Qt::CaseInsensitive)) {
match = true;
break;
}
}
}
child->setHidden(!match);
if (match) any_child_visible = true;
}
// Also check if parent label matches
if (!text.isEmpty() && parent->text(0).contains(text, Qt::CaseInsensitive)) {
any_child_visible = true;
for (int j = 0; j < parent->childCount(); ++j)
parent->child(j)->setHidden(false);
}
parent->setHidden(!any_child_visible);
}
}
void PlcLinkWidget::on_m_tree_widget_customContextMenuRequested(const QPoint &pos)
{
QTreeWidgetItem *item = m_tree_widget->itemAt(pos);
if (!item)
return;
// Only show context menu for IO child items (not parent master items)
if (!m_io_entry_hash.contains(item))
return;
const PlcIoEntry &entry = m_io_entry_hash.value(item);
// Check if this IO is already linked
Element *master = entry.master;
if (!master)
return;
QSet<int> used_indices;
for (Element *linked : master->linkedElements()) {
int idx = master->groupIndexForElement(linked);
if (idx >= 0)
used_indices.insert(idx);
}
if (used_indices.contains(entry.ioIndex))
return; // Already linked, no actions
QMenu menu;
QAction *link_action = menu.addAction(tr("Connecter"));
QAction *selected_action = menu.exec(m_tree_widget->viewport()->mapToGlobal(pos));
if (selected_action == link_action) {
m_element_to_link = master;
m_pending_io_index = entry.ioIndex;
apply();
}
}
void PlcLinkWidget::on_m_unlink_pb_clicked()
{
if (!m_element || m_element->isFree())
return;
LinkElementCommand *undo = new LinkElementCommand(m_element);
undo->unlinkAll();
m_element->diagram()->undoStack().push(undo);
updateUi();
}
void PlcLinkWidget::on_m_show_this_pb_clicked()
{
if (!m_element || !m_element->diagram())
return;
m_element->diagram()->showMe();
m_element->setHighlighted(true);
}
+87
View File
@@ -0,0 +1,87 @@
/*
Copyright 2006-2026 The QElectroTech Team
This file is part of QElectroTech.
QElectroTech is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
QElectroTech is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef PLCLINKWIDGET_H
#define PLCLINKWIDGET_H
#include "abstractelementpropertieseditorwidget.h"
#include <QHash>
#include <QPointer>
#include <QSet>
class QTreeWidgetItem;
class QTreeWidget;
class QLineEdit;
class QPushButton;
class QLabel;
class Element;
/**
@brief The PlcLinkWidget class
Provides a dedicated widget for linking a PLC-Slave to a PLC-Master IO entry.
Displays all PLC-Masters as expandable items, each showing their IO entries.
Each IO entry can only be linked to one slave. Already-linked entries are
shown as struck-through and greyed out.
*/
class PlcLinkWidget : public AbstractElementPropertiesEditorWidget
{
Q_OBJECT
public:
explicit PlcLinkWidget(Element *elmt, QWidget *parent = nullptr);
~PlcLinkWidget() override = default;
void setElement(Element *element) override;
void apply() override;
QUndoCommand *associatedUndo() const override;
QString title() const override;
public slots:
void updateUi() override;
private:
void buildPlcTree();
void hideButtons();
void showButtons();
private slots:
void on_m_search_field_textEdited(const QString &text);
void on_m_tree_widget_customContextMenuRequested(const QPoint &pos);
void on_m_unlink_pb_clicked();
void on_m_show_this_pb_clicked();
private:
QLabel *m_label{nullptr};
QPushButton *m_unlink_pb{nullptr};
QPushButton *m_show_this_pb{nullptr};
QLineEdit *m_search_field{nullptr};
QTreeWidget *m_tree_widget{nullptr};
QLabel *m_hidden_masters_label{nullptr};
// Maps IO child items to (master, io_index)
struct PlcIoEntry {
QPointer<Element> master;
int ioIndex = -1;
};
QHash<QTreeWidgetItem*, PlcIoEntry> m_io_entry_hash;
Element *m_element_to_link = nullptr;
int m_pending_io_index = -1;
};
#endif // PLCLINKWIDGET_H
+20 -3
View File
@@ -188,8 +188,11 @@ class LinkReportPotentialSelector : public AbstractPotentialSelector
//### END PRIVATE CLASS ###//
ConductorProperties PotentialSelectorDialog::chosenProperties(QList<ConductorProperties> list, QWidget *widget)
ConductorProperties PotentialSelectorDialog::chosenProperties(QList<ConductorProperties> list, QWidget *widget, bool *cancelled)
{
if (cancelled)
*cancelled = false;
if (list.isEmpty()) {
return ConductorProperties() ;
} else if (list.size() == 1) {
@@ -222,11 +225,25 @@ ConductorProperties PotentialSelectorDialog::chosenProperties(QList<ConductorPro
layout.addWidget(b);
H.insert(b, cp);
}
QDialogButtonBox *button_box = new QDialogButtonBox(QDialogButtonBox::Ok, &dialog);
// Pre-select the first entry: without this, accepting the dialog without
// ever touching a radio button silently returned blank properties too,
// the same failure mode as the missing Cancel button below.
if (!H.isEmpty())
H.constBegin().key()->setChecked(true);
QDialogButtonBox *button_box = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog);
layout.addWidget(button_box);
connect(button_box, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
connect(button_box, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
if (dialog.exec() != QDialog::Accepted) {
if (cancelled)
*cancelled = true;
return ConductorProperties();
}
dialog.exec();
for (QRadioButton *b : H.keys()) {
if(b->isChecked()) {
return H.value(b);
+7 -2
View File
@@ -64,7 +64,11 @@ namespace Ui {
the static function chosenProperties,
open a dialog who ask user to make a choice between the given
properties
properties. If the dialog is cancelled (Cancel button, Escape, or the
window's close button) and @a cancelled is non-null, *cancelled is set
to true and an empty ConductorProperties() is returned; callers that
care about a real cancellation (as opposed to "no properties to choose
from") should check it rather than relying on the returned value alone.
*/
class PotentialSelectorDialog : public QDialog
{
@@ -73,7 +77,8 @@ class PotentialSelectorDialog : public QDialog
public:
static ConductorProperties chosenProperties(
QList<ConductorProperties> list,
QWidget *parent = nullptr);
QWidget *parent = nullptr,
bool *cancelled = nullptr);
public:
explicit PotentialSelectorDialog(
+1 -1
View File
@@ -55,7 +55,7 @@ Veuillez choisir les propriétées à appliquer au nouveau potentiel.</string>
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Ok</set>
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
+7 -1
View File
@@ -63,7 +63,13 @@ ProjectPropertiesDialog::~ProjectPropertiesDialog ()
*/
void ProjectPropertiesDialog::exec()
{
m_properties_dialog->setWindowModality(Qt::WindowModal);
// ApplicationModal (not WindowModal) so no other window — including other
// MDI subwindows — can dispatch events while the dialog holds raw
// QETProject* pointers in its config pages. WindowModal only blocks the
// parent ProjectView; the rest of the MDI area stays live, which allowed
// new_project / close_project to replace the project under the dialog
// and cause a SIGSEGV. See issue #527.
m_properties_dialog->setWindowModality(Qt::ApplicationModal);
m_properties_dialog -> exec();
}
@@ -504,7 +504,7 @@ void ShapeGraphicsItemPropertiesWidget::setUpEditConnection()
void ShapeGraphicsItemPropertiesWidget::clearEditConnection()
{
for (const auto &c : qAsConst(m_edit_connection)) {
for (const auto &c : std::as_const(m_edit_connection)) {
disconnect(c);
}
m_edit_connection.clear();
+58 -16
View File
@@ -24,6 +24,7 @@
#include "ui_titleblockpropertieswidget.h"
#include <QMenu>
#include <QSet>
#include <utility>
/**
@@ -162,7 +163,11 @@ void TitleBlockPropertiesWidget::setProperties(
}
ui -> m_tbt_cb -> setCurrentIndex(index);
m_dcw -> setContext(properties.context);
// Show the saved custom values, plus any of the template's custom variables
// that aren't defined yet, so the user only fills in the missing ones (#271).
DiagramContext context = properties.context;
addTemplateVariables(context, index);
m_dcw -> setContext(context);
}
/**
@@ -435,12 +440,15 @@ void TitleBlockPropertiesWidget::updateTemplateList()
}
/**
@brief TitleBlockPropertiesWidget::changeCurrentTitleBlockTemplate
Load the additional field of title block "text"
@brief TitleBlockPropertiesWidget::templateForIndex
@param index : index in the collection-type map (= the template combo index)
@return the TitleBlockTemplate currently selected for that collection, or
nullptr.
*/
void TitleBlockPropertiesWidget::changeCurrentTitleBlockTemplate(int index)
TitleBlockTemplate *TitleBlockPropertiesWidget::templateForIndex(int index) const
{
m_dcw -> clear();
if (index < 0 || index >= m_map_index_to_collection_type.count())
return nullptr;
QET::QetCollection qc = m_map_index_to_collection_type.at(index);
TitleBlockTemplatesCollection *collection = nullptr;
@@ -448,21 +456,55 @@ void TitleBlockPropertiesWidget::changeCurrentTitleBlockTemplate(int index)
if (c -> collection() == qc)
collection = c;
if (!collection) return;
if (!collection) return nullptr;
return collection -> getTemplate(ui -> m_tbt_cb -> currentText());
}
// get template
TitleBlockTemplate *tpl = collection -> getTemplate(ui -> m_tbt_cb -> currentText());
if(tpl != nullptr) {
// get all template fields
QStringList fields = tpl -> listOfVariables();
// set fields to additional_fields_ widget
DiagramContext templateContext;
for(int i =0; i<fields.count(); i++)
templateContext.addValue(fields.at(i), "");
m_dcw -> setContext(templateContext);
/**
@brief TitleBlockPropertiesWidget::addTemplateVariables
Add to @p context every CUSTOM variable used by the currently selected
template that is not already present, with an empty value — so the user
only has to fill in the values instead of declaring the variables (#271).
The standard fields (title, author, date, …) are handled by their own
widgets and are skipped. Existing values in @p context are preserved.
*/
void TitleBlockPropertiesWidget::addTemplateVariables(
DiagramContext &context, int index) const
{
TitleBlockTemplate *tpl = templateForIndex(index);
if (!tpl) return;
// Variables rendered from the dedicated standard-field widgets; they must
// not appear in the "Custom" tab.
static const QSet<QString> reserved {
QStringLiteral("author"), QStringLiteral("date"),
QStringLiteral("title"), QStringLiteral("filename"),
QStringLiteral("plant"), QStringLiteral("locmach"),
QStringLiteral("indexrev"), QStringLiteral("version"),
QStringLiteral("folio"), QStringLiteral("folio-id"),
QStringLiteral("folio-total"), QStringLiteral("auto_page_num"),
QStringLiteral("previous-folio-num"), QStringLiteral("next-folio-num")
};
const QStringList variables = tpl -> listOfVariables();
for (const QString &name : variables) {
if (name.isEmpty() || reserved.contains(name)) continue;
if (!context.contains(name)) context.addValue(name, "");
}
}
/**
@brief TitleBlockPropertiesWidget::changeCurrentTitleBlockTemplate
When the user picks a template, append its missing custom variables to the
"Custom" tab while keeping the values already entered (#271).
*/
void TitleBlockPropertiesWidget::changeCurrentTitleBlockTemplate(int index)
{
DiagramContext context = m_dcw -> context();
addTemplateVariables(context, index);
m_dcw -> setContext(context);
}
/**
@brief TitleBlockPropertiesWidget::on_m_date_now_pb_clicked
Set the date to current date
+3
View File
@@ -30,6 +30,7 @@ class NumerotationContext;
class QETProject;
class QMenu;
class TitleBlockTemplatesCollection;
class TitleBlockTemplate;
namespace Ui {
class TitleBlockPropertiesWidget;
@@ -77,6 +78,8 @@ class TitleBlockPropertiesWidget : public QWidget
void initDialog(const bool &current_date, QETProject *project);
int getIndexFor (const QString &tbt_name,
const QET::QetCollection collection) const;
TitleBlockTemplate *templateForIndex (int index) const;
void addTemplateVariables (DiagramContext &context, int index) const;
private slots:
void editCurrentTitleBlockTemplate();
+1
View File
@@ -101,6 +101,7 @@ void XRefPropertiesWidget::buildUi()
ui -> m_type_cb -> addItem(tr("Bobine"), "coil");
ui -> m_type_cb -> addItem(tr("Organe de protection"), "protection");
ui -> m_type_cb -> addItem(tr("Commutateur / bouton"), "commutator");
ui -> m_type_cb -> addItem(tr("Automate (PLC)"), "plc");
ui -> m_snap_to_cb -> addItem(tr("En bas de page"), "bottom");
ui -> m_snap_to_cb -> addItem(tr("Sous le label de l'élément"), "label");