Merge remote-tracking branch 'upstream/master' into proto-conductor-selection-properties

# Conflicts:
#	cmake/qet_compilation_vars.cmake
#	lang/qet_en.ts
#	sources/qetdiagrameditor.cpp
This commit is contained in:
ispyisail
2026-09-27 09:07:41 +13:00
897 changed files with 351648 additions and 61060 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
+84 -74
View File
@@ -18,6 +18,7 @@
#include "bomexportdialog.h"
#include "../dataBase/ui/elementquerywidget.h"
#include "../bomexport.h"
#include "../qetapp.h"
#include "../qetinformation.h"
#include "../qetproject.h"
@@ -25,6 +26,7 @@
#include <QMessageBox>
#include <QSqlError>
#include <QSqlQueryModel>
#include <QSqlRecord>
/**
@@ -41,8 +43,11 @@ BOMExportDialog::BOMExportDialog(QETProject *project, QWidget *parent) :
m_query_widget = new ElementQueryWidget(this);
ui->m_main_layout->insertWidget(0, m_query_widget);
//By default format as bom is clicked
on_m_format_as_bom_clicked(true);
m_query_widget->setQuery(BomExport::defaultQuery());
on_m_format_as_bom_clicked(false);
m_preview_model = new QSqlQueryModel(this);
ui->m_preview_table->setModel(m_preview_model);
}
/**
@@ -67,97 +72,66 @@ int BOMExportDialog::exec()
if (dir.isEmpty()) dir = QETApp::documentDir();
QString file_name = dir % "/" % tr("nomenclature_") % QString(m_project ->title() % ".csv");
QString file_path = QFileDialog::getSaveFileName(this, tr("Enregister sous... "), file_name, tr("Fichiers csv (*.csv)"));
QFile file(file_path);
if (!file_path.isEmpty())
{
if (QFile::exists(file_path ))
{
// if file already exist -> delete it
if (!QFile::remove(file_path) )
{
QMessageBox::critical(this, tr("Erreur"),
tr("Impossible de remplacer le fichier!\n\n")+
"Destination : "+file_path+"\n");
}
}
if (file.open(QIODevice::WriteOnly | QIODevice::Text))
{
QTextStream stream(&file);
#if QT_VERSION < QT_VERSION_CHECK(5, 15, 0) // ### Qt 6: remove
stream << getBom() << endl;
#else
#if TODO_LIST
#pragma message("@TODO remove code for QT 5.15 or later")
#endif
stream << getBom() << &Qt::endl(stream);
#endif
QString error;
const auto csv = getBom(&error);
if (!error.isEmpty() || !BomExport::writeCsv(file_path, csv, &error)) {
QMessageBox::critical(
this, tr("Erreur"),
tr("Impossible d'enregistrer la nomenclature dans %1.\n%2")
.arg(file_path, error));
}
}
}
return r;
}
QString BOMExportDialog::getBom()
QByteArray BOMExportDialog::getBom(QString *error)
{
if (error) {
error->clear();
}
m_project->dataBase()->updateDB();
auto query_ = m_project->dataBase()->newQuery(m_query_widget->queryStr());
QString return_string;
QString rejection;
auto query_ = m_project->dataBase()->newQuery(m_query_widget->queryStr(), &rejection);
if (!rejection.isEmpty()) {
if (error) {
*error = rejection;
}
return {};
}
if (!query_.exec()) {
qDebug() << "BOMExportDialog::getBom : query errir : " << query_.lastError();
if (error) {
*error = query_.lastError().text();
}
return {};
}
else
QStringList header_names;
if (ui->m_include_headers->isChecked())
{
//HEADERS
if (ui->m_include_headers)
const auto record_ = query_.record();
for (int i = 0; i < record_.count(); ++i)
{
auto record_ = query_.record();
QStringList header_name;
for (auto i=0 ; i<record_.count() ; ++i)
{
auto field_name = record_.fieldName(i);
qDebug() << "field name = " << field_name;
if (field_name == "position") {
header_name << tr("Position");
} else if (field_name == "diagram_position") {
header_name << tr("Position du folio");
} else if (field_name == "designation_qty") {
header_name << tr("Quantité numéro d'article", "Special field with name : designation quantity");
} else {
header_name << QETInformation::translatedInfoKey(field_name);
if (header_name.isEmpty()) {
header_name << field_name;
}
}
const auto field_name = record_.fieldName(i);
if (field_name == QLatin1String("position")) {
header_names << tr("Position");
} else if (field_name == QLatin1String("diagram_position")) {
header_names << tr("Position du folio");
} else if (field_name == QLatin1String("designation_qty")) {
header_names << tr("Quantité numéro d'article", "Special field with name : designation quantity");
} else {
const auto translated = QETInformation::translatedInfoKey(field_name);
header_names << (translated.isEmpty() ? field_name : translated);
}
return_string = header_name.join(";") % "\n";
}
//ROWS
while (query_.next())
{
auto i=0;
QStringList values;
while (query_.value(i).isValid())
{
auto date = query_.value(i).toDate();
if (!date.isNull()) {
values << QLocale::system().toString(query_.value(i).toDate(), QLocale::ShortFormat);
} else {
values << query_.value(i).toString();
}
++i;
}
return_string += values.join(";") % "\n";
values.clear();
}
}
qDebug() << return_string;
return return_string;
return BomExport::toCsv(
query_, header_names, ui->m_include_headers->isChecked());
}
/**
@@ -168,3 +142,39 @@ void BOMExportDialog::on_m_format_as_bom_clicked(bool checked) {
m_query_widget->setGroupBy("designation", checked);
m_query_widget->setCount("COUNT(*) AS designation_qty", checked);
}
/**
@brief BOMExportDialog::on_m_preview_pb_clicked
Run the current query and show its result live, without going through
the CSV round-trip -- lets a report be checked and adjusted before
committing to a file (qelectrotech-source-mirror#886).
*/
void BOMExportDialog::on_m_preview_pb_clicked()
{
m_project->dataBase()->updateDB();
QString rejection;
auto query_ = m_project->dataBase()->newQuery(m_query_widget->queryStr(), &rejection);
if (!rejection.isEmpty()) {
QMessageBox::warning(this, tr("Requête refusée"), rejection);
return;
}
if (!query_.exec()) {
QMessageBox::warning(
this, tr("Erreur"),
tr("Erreur dans la requête :\n%1").arg(query_.lastError().text()));
return;
}
m_preview_model->setQuery(std::move(query_));
for (int i = 0; i < m_preview_model->columnCount(); ++i)
{
const auto field_name = m_preview_model->record().fieldName(i);
const auto translated = QETInformation::translatedInfoKey(field_name);
if (!translated.isEmpty()) {
m_preview_model->setHeaderData(i, Qt::Horizontal, translated);
}
}
ui->m_preview_table->resizeColumnsToContents();
}
+5 -1
View File
@@ -19,9 +19,11 @@
#define BOMEXPORTDIALOG_H
#include <QDialog>
#include <QByteArray>
class QETProject;
class ElementQueryWidget;
class QSqlQueryModel;
namespace Ui {
class BOMExportDialog;
@@ -39,15 +41,17 @@ class BOMExportDialog : public QDialog
~BOMExportDialog() override;
virtual int exec() override;
QString getBom();
QByteArray getBom(QString *error = nullptr);
private slots:
void on_m_format_as_bom_clicked(bool checked);
void on_m_preview_pb_clicked();
private:
Ui::BOMExportDialog *ui;
ElementQueryWidget *m_query_widget = nullptr;
QETProject *m_project = nullptr;
QSqlQueryModel *m_preview_model = nullptr;
};
#endif // BOMEXPORTDIALOG_H
+33 -2
View File
@@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>610</width>
<height>232</height>
<height>480</height>
</rect>
</property>
<property name="windowTitle">
@@ -36,7 +36,7 @@
<string>Formater en tant que liste de materiel</string>
</property>
<property name="checked">
<bool>true</bool>
<bool>false</bool>
</property>
</widget>
</item>
@@ -56,6 +56,37 @@
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="m_preview_layout">
<item>
<widget class="QPushButton" name="m_preview_pb">
<property name="text">
<string>Aperçu</string>
</property>
</widget>
</item>
<item>
<spacer name="m_preview_spacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QTableView" name="m_preview_table">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
+277
View File
@@ -0,0 +1,277 @@
/*
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 "conductorcolortoolbutton.h"
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../diagram.h"
#include "../diagramcontent.h"
#include "../diagramview.h"
#include "../lastusedstyle.h"
#include "../qetdiagrameditor.h"
#include "../projectview.h"
#include "../qetgraphicsitem/conductor.h"
#include "../conductorproperties.h"
#include <QColorDialog>
#include <QMenu>
#include <QPainter>
#include <QPixmap>
#include <QSet>
namespace {
/**
The colours an electrician reaches for, in the order the trade
names them: the three phases, neutral, earth, then the ones used
for control and extra-low-voltage circuits. Not a standard in
itself -- IEC 60445 only fixes blue for neutral and green/yellow
for protective earth -- but it covers the wiring a schematic
actually shows, which is what makes the toolbar worth a click.
*/
struct NamedColor { const char *context_name; QColor color; };
QList<NamedColor> standardColors()
{
return {
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Noir"), QColor(0x00, 0x00, 0x00)},
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Marron"), QColor(0x7B, 0x3F, 0x00)},
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Gris"), QColor(0x80, 0x80, 0x80)},
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Bleu"), QColor(0x00, 0x00, 0xFF)},
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Vert"), QColor(0x00, 0x80, 0x00)},
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Rouge"), QColor(0xFF, 0x00, 0x00)},
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Orange"), QColor(0xFF, 0x80, 0x00)},
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Violet"), QColor(0x80, 0x00, 0x80)},
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Blanc"), QColor(0xFF, 0xFF, 0xFF)},
};
}
const int MAX_RECENT = 6;
}
/**
@brief ConductorColorToolButton::ConductorColorToolButton
@param editor : the diagram editor this button acts on
@param parent
*/
ConductorColorToolButton::ConductorColorToolButton(QETDiagramEditor *editor, QWidget *parent) :
QToolButton(parent),
m_editor(editor)
{
setPopupMode(QToolButton::InstantPopup);
setToolTip(tr("Couleur de conducteur"));
setStatusTip(tr("Applique une couleur aux conducteurs sélectionnés, et l'utilise pour les prochains conducteurs tracés",
"status bar tip"));
m_current = LastUsedStyle::hasConductorColor() ? LastUsedStyle::conductorColor()
: QColor(Qt::black);
setMenu(new QMenu(this));
rebuildMenu();
setSwatch(m_current);
}
/**
@brief ConductorColorToolButton::currentView
@return the folio being edited, or nullptr when no project is open.
Goes through the project view because QETDiagramEditor keeps its own
currentDiagramView() private.
*/
DiagramView *ConductorColorToolButton::currentView() const
{
ProjectView *pv = m_editor ? m_editor->currentProjectView() : nullptr;
return pv ? pv->currentDiagram() : nullptr;
}
/**
@brief ConductorColorToolButton::updateEnabledState
Greyed out when there is no folio to act on, or when the project is
read-only -- the same rule the other conductor actions follow.
*/
void ConductorColorToolButton::updateEnabledState()
{
DiagramView *dv = currentView();
setEnabled(dv && dv->diagram() && !dv->diagram()->isReadOnly());
}
/**
@brief ConductorColorToolButton::rebuildMenu
The standard colours never change; the recent ones do, so the menu is
rebuilt rather than kept in sync entry by entry.
*/
void ConductorColorToolButton::rebuildMenu()
{
QMenu *m = menu();
m->clear();
for (const auto &nc : standardColors())
{
const QColor c = nc.color;
QAction *a = m->addAction(swatchIcon(c),
tr(nc.context_name));
connect(a, &QAction::triggered, this, [this, c]() { applyColor(c); });
}
if (!m_recent.isEmpty())
{
m->addSeparator();
QAction *title = m->addAction(tr("Récemment utilisées"));
title->setEnabled(false);
for (const QColor &c : std::as_const(m_recent))
{
QAction *a = m->addAction(swatchIcon(c), c.name());
connect(a, &QAction::triggered, this, [this, c]() { applyColor(c); });
}
}
m->addSeparator();
QAction *other = m->addAction(tr("Autre couleur…"));
connect(other, &QAction::triggered, this, &ConductorColorToolButton::chooseOtherColor);
}
/**
@brief ConductorColorToolButton::applyColor
Recolour the selected conductors, and remember the colour for the next
one drawn.
@param color
*/
void ConductorColorToolButton::applyColor(const QColor &color)
{
if (!color.isValid()) {
return;
}
//Always: this is the half that works with nothing selected.
LastUsedStyle::setConductorColor(color);
rememberRecent(color);
setSwatch(color);
DiagramView *dv = currentView();
if (!dv) {
return;
}
Diagram *diagram = dv->diagram();
if (!diagram || diagram->isReadOnly()) {
return;
}
DiagramContent dc(diagram);
const auto selected_conductors = dc.conductors(DiagramContent::AnyConductor);
if (selected_conductors.isEmpty()) {
return;
}
//A wire drawn across a junction is several Conductor objects
//sharing one electrical potential; selecting one segment must
//still recolour the whole potential, the way the F2 dialog's
//"apply to all" already does.
QSet<Conductor *> conductors;
for (Conductor *conductor : selected_conductors)
{
conductors << conductor;
conductors += conductor->relatedPotentialConductors();
}
QUndoCommand *undo = new QUndoCommand(tr("Modifier la couleur de %n conducteur(s)",
"undo caption", conductors.count()));
int changed = 0;
for (Conductor *conductor : conductors)
{
ConductorProperties before = conductor->properties();
if (before.color == color) {
continue;
}
ConductorProperties after = before;
after.color = color;
QVariant old_value, new_value;
old_value.setValue(before);
new_value.setValue(after);
new QPropertyUndoCommand(conductor, "properties", old_value, new_value, undo);
++changed;
}
//Every selected conductor was already this colour: pushing an
//empty command would put a no-op step in the undo stack.
if (changed) {
diagram->undoStack().push(undo);
} else {
delete undo;
}
}
/**
@brief ConductorColorToolButton::chooseOtherColor
*/
void ConductorColorToolButton::chooseOtherColor()
{
const QColor c = QColorDialog::getColor(m_current, this,
tr("Choisir une couleur de conducteur"));
if (c.isValid()) {
applyColor(c);
}
}
/**
@brief ConductorColorToolButton::rememberRecent
Most recent first, no duplicates, capped.
@param color
*/
void ConductorColorToolButton::rememberRecent(const QColor &color)
{
//A colour that is already one row up in the standard list would
//only appear twice, under a hex name it does not need.
for (const auto &nc : standardColors()) {
if (nc.color == color) {
return;
}
}
m_recent.removeAll(color);
m_recent.prepend(color);
while (m_recent.size() > MAX_RECENT) {
m_recent.removeLast();
}
rebuildMenu();
}
/**
@brief ConductorColorToolButton::setSwatch
@param color
*/
void ConductorColorToolButton::setSwatch(const QColor &color)
{
m_current = color;
setIcon(swatchIcon(color));
}
/**
@brief ConductorColorToolButton::swatchIcon
@param color
@return a plain square of that colour, outlined so that white and very
light colours are still visible against the toolbar.
*/
QIcon ConductorColorToolButton::swatchIcon(const QColor &color)
{
QPixmap pix(16, 16);
pix.fill(Qt::transparent);
QPainter p(&pix);
p.setRenderHint(QPainter::Antialiasing, false);
p.setBrush(color);
p.setPen(QPen(QColor(0x40, 0x40, 0x40), 1));
p.drawRect(0, 0, 15, 15);
p.end();
return QIcon(pix);
}
+69
View File
@@ -0,0 +1,69 @@
/*
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 CONDUCTORCOLORTOOLBUTTON_H
#define CONDUCTORCOLORTOOLBUTTON_H
#include <QColor>
#include <QList>
#include <QToolButton>
class QETDiagramEditor;
class DiagramView;
/**
@brief The ConductorColorToolButton class
One-click conductor colour, from the "Schéma" toolbar.
Picking a colour does two things: it recolours every conductor
currently selected (one undo command for the lot), and it becomes the
colour of the next conductor drawn this session, through the existing
LastUsedStyle mechanism. Either half is useful on its own -- with
nothing selected it only sets the pen for what comes next.
This deliberately stores nothing in the project and nothing in
QSettings. It is the same session-scoped "what did I just use" idea
LastUsedStyle already implements; named presets that persist per
project are a separate, larger feature (upstream issue #461) that
needs a maintainer decision first.
*/
class ConductorColorToolButton : public QToolButton
{
Q_OBJECT
public:
explicit ConductorColorToolButton(QETDiagramEditor *editor,
QWidget *parent = nullptr);
public slots:
void updateEnabledState();
private:
DiagramView *currentView() const;
void rebuildMenu();
void applyColor(const QColor &color);
void chooseOtherColor();
void rememberRecent(const QColor &color);
void setSwatch(const QColor &color);
static QIcon swatchIcon(const QColor &color);
QETDiagramEditor *m_editor = nullptr;
QList<QColor> m_recent;
QColor m_current;
};
#endif // CONDUCTORCOLORTOOLBUTTON_H
+4 -4
View File
@@ -233,7 +233,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="configure" resource="../../qelectrotech.qrc">
<normaloff>:/ico/16x16/configure.png</normaloff>:/ico/16x16/configure.png</iconset>
</property>
</widget>
@@ -363,7 +363,7 @@
<string>neutre</string>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="neutral" resource="../../qelectrotech.qrc">
<normaloff>:/ico/16x16/neutral.png</normaloff>:/ico/16x16/neutral.png</iconset>
</property>
</widget>
@@ -377,7 +377,7 @@
<string>terre</string>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="ground" resource="../../qelectrotech.qrc">
<normaloff>:/ico/16x16/ground.png</normaloff>:/ico/16x16/ground.png</iconset>
</property>
</widget>
@@ -391,7 +391,7 @@
<string>phase</string>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="phase" resource="../../qelectrotech.qrc">
<normaloff>:/ico/16x16/phase.png</normaloff>:/ico/16x16/phase.png</iconset>
</property>
</widget>
+230 -9
View File
@@ -30,7 +30,9 @@
#include "../reportpropertiewidget.h"
#include "../titleblockpropertieswidget.h"
#include "../xrefpropertieswidget.h"
#include "guidespropertieswidget.h"
#include "../autoNum/numerotationcontext.h"
#include "../autoNum/ui/selectautonumw.h"
#include <QFont>
#include <QFontDialog>
#include <QSizePolicy>
@@ -73,6 +75,59 @@ 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);
// global auto-numbering defaults (only when editing global settings, not a project)
if (!m_project) {
auto saw_conductor = new SelectAutonumW(1);
auto saw_element = new SelectAutonumW(0);
auto saw_folio = new SelectAutonumW(2);
initAutoNumTab(m_autonum_conductor, saw_conductor, QStringLiteral("autonum/conductor"));
initAutoNumTab(m_autonum_element, saw_element, QStringLiteral("autonum/element"));
initAutoNumTab(m_autonum_folio, saw_folio, QStringLiteral("autonum/folio"));
QSettings autonum_settings;
loadAutoNumTab(m_autonum_conductor, autonum_settings);
loadAutoNumTab(m_autonum_element, autonum_settings);
loadAutoNumTab(m_autonum_folio, autonum_settings);
// Intercept Return key in the combo line edits so it doesn't
// activate the dialog's default button (OK).
for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) {
if (QComboBox *combo = tab->widget->contextComboBox()) {
if (combo->lineEdit()) {
combo->lineEdit()->installEventFilter(this);
}
}
}
}
//If there is a project, we edit his properties
if (m_project) {
@@ -83,10 +138,11 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
xrefpw -> setProperties (m_project -> defaultXRefProperties());
}
connect(ipw,SIGNAL(openAutoNumFolioEditor(QString)),this,SLOT(changeToAutoFolioTab()));
connect(ipw, &TitleBlockPropertiesWidget::openAutoNumFolioEditor, this, &NewDiagramPage::changeToAutoFolioTab);
// main tab widget
QTabWidget *tab_widget = new QTabWidget(this);
m_tab_widget = tab_widget;
QWidget *diagram_widget = new QWidget();
QVBoxLayout *diagram_layout = new QVBoxLayout(diagram_widget);
@@ -98,6 +154,20 @@ 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"));
// add auto-numbering tab only for global settings (not per project)
if (!m_project) {
QWidget *autonum_widget = new QWidget();
QVBoxLayout *autonum_layout = new QVBoxLayout(autonum_widget);
autonum_layout->addWidget(new QLabel(tr("Définir les règles de numérotation automatique par défaut pour les nouveaux projets :")));
QTabWidget *autonum_inner_tab = new QTabWidget();
autonum_inner_tab->addTab(m_autonum_conductor.widget, tr("Conducteurs"));
autonum_inner_tab->addTab(m_autonum_element.widget, tr("Eléments"));
autonum_inner_tab->addTab(m_autonum_folio.widget, tr("Folios"));
autonum_layout->addWidget(autonum_inner_tab);
tab_widget -> addTab (autonum_widget, tr("Numérotation auto"));
}
QVBoxLayout *vlayout1 = new QVBoxLayout();
vlayout1->addWidget(tab_widget);
@@ -110,7 +180,7 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
*/
NewDiagramPage::~NewDiagramPage()
{
disconnect(ipw,SIGNAL(openAutoNumFolioEditor(QString)),this,SLOT(changeToAutoFolioTab()));
disconnect(ipw, &TitleBlockPropertiesWidget::openAutoNumFolioEditor, this, &NewDiagramPage::changeToAutoFolioTab);
}
/**
@@ -155,6 +225,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);
}
@@ -175,14 +256,26 @@ void NewDiagramPage::applyConf()
rpw->toSettings(settings, "diagrameditor/defaultreport");
// 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);
const QHash<QString, XRefProperties> hash_xrp = xrefpw->properties();
for (auto it = hash_xrp.constBegin() ; it != hash_xrp.constEnd() ; ++it) {
it.value().toSettings(settings,
QStringLiteral("diagrameditor/defaultxref") % it.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();
// save global auto-numbering defaults
persistAutonumSettings();
}
}
/**
@@ -247,6 +340,134 @@ void NewDiagramPage::loadSavedTbp()
applyConf();
}
/**
@brief NewDiagramPage::isPlaceholder
Return true if @a name matches the combo box's built-in placeholder text
(first item). This is locale-independent because it reads the actual item text.
*/
bool NewDiagramPage::isPlaceholder(QComboBox *combo, const QString &name)
{
return !combo->count() || name == combo->itemText(0);
}
/**
@brief NewDiagramPage::initAutoNumTab
Initialise an AutoNumTab struct and connect its signals.
*/
void NewDiagramPage::initAutoNumTab(AutoNumTab &tab, SelectAutonumW *w, const QString &prefix)
{
tab.widget = w;
tab.prefix = prefix;
connect(w, &SelectAutonumW::applyPressed, this, [this, &tab]() { saveAutoNumContext(tab); });
connect(w, &SelectAutonumW::removeClicked, this, [this, &tab]() { removeAutoNumContext(tab); });
connect(w->contextComboBox(), &QComboBox::activated, this, [this, &tab](int index) {
if (index >= 0) {
QString name = tab.widget->contextComboBox()->itemText(index);
if (tab.contexts.contains(name)) {
tab.widget->setContext(tab.contexts.value(name));
}
}
});
}
/**
@brief NewDiagramPage::loadAutoNumTab
Load saved rules from QSettings into an AutoNumTab.
*/
void NewDiagramPage::loadAutoNumTab(AutoNumTab &tab, QSettings &settings)
{
auto data = NumerotationContext::loadFromSettings(settings, tab.prefix);
tab.contexts = data.first;
for (auto it = tab.contexts.constBegin(); it != tab.contexts.constEnd(); ++it) {
tab.widget->contextComboBox()->addItem(it.key());
}
if (!tab.contexts.isEmpty() && !data.second.isEmpty()
&& tab.contexts.contains(data.second)) {
tab.widget->contextComboBox()->setCurrentText(data.second);
tab.widget->setContext(tab.contexts.value(data.second));
}
}
/**
@brief NewDiagramPage::saveAutoNumContext
Save the current context from an AutoNumTab's widget into its hash and
persist to QSettings immediately.
*/
void NewDiagramPage::saveAutoNumContext(AutoNumTab &tab)
{
QString name = tab.widget->contextComboBox()->currentText().trimmed();
if (name.isEmpty() || isPlaceholder(tab.widget->contextComboBox(), name)) {
return;
}
tab.contexts.insert(name, tab.widget->toNumContext());
if (tab.widget->contextComboBox()->findText(name) == -1) {
tab.widget->contextComboBox()->addItem(name);
}
persistAutonumSettings();
}
/**
@brief NewDiagramPage::removeAutoNumContext
Remove the current context from an AutoNumTab's hash and persist.
*/
void NewDiagramPage::removeAutoNumContext(AutoNumTab &tab)
{
QString name = tab.widget->contextComboBox()->currentText().trimmed();
if (name.isEmpty() || isPlaceholder(tab.widget->contextComboBox(), name)) {
return;
}
int idx = tab.widget->contextComboBox()->findText(name);
if (idx == -1) return;
tab.contexts.remove(name);
tab.widget->contextComboBox()->removeItem(idx);
tab.widget->contextComboBox()->setCurrentText(QString());
tab.widget->setContext(NumerotationContext());
persistAutonumSettings();
}
/**
@brief NewDiagramPage::persistAutonumSettings
Save all autonum contexts to QSettings immediately.
*/
void NewDiagramPage::persistAutonumSettings()
{
QSettings settings;
for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) {
QString current;
QComboBox *combo = tab->widget->contextComboBox();
if (!isPlaceholder(combo, combo->currentText().trimmed())
&& tab->contexts.contains(combo->currentText().trimmed())) {
current = combo->currentText().trimmed();
}
NumerotationContext::saveToSettings(tab->contexts, current,
settings, tab->prefix);
}
}
/**
@brief NewDiagramPage::eventFilter
Intercept Return/Enter in combo box line edits so it doesn't close the
settings dialog.
*/
bool NewDiagramPage::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
auto *ke = static_cast<QKeyEvent *>(event);
if ((ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter)) {
// Check if this is a line edit inside one of our autonum combos
for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) {
if (QComboBox *combo = tab->widget->contextComboBox()) {
if (combo->lineEdit() && combo->lineEdit() == obj) {
return true; // eat the event
}
}
}
}
}
return ConfigPage::eventFilter(obj, event);
}
/**
Constructeur
@param parent QWidget parent
+23
View File
@@ -20,6 +20,7 @@
#include "configpage.h"
#include "../projectpropertiesdialog.h"
#include "../titleblockpropertieswidget.h"
#include "../autoNum/numerotationcontext.h"
#include <QDialog>
#include <QtWidgets>
@@ -29,8 +30,10 @@ class TitleBlockPropertiesWidget;
class ExportPropertiesWidget;
class ReportPropertieWidget;
class XRefPropertiesWidget;
class GuidesPropertiesWidget;
class QETProject;
class TitleBlockProperties;
class SelectAutonumW;
/**
@brief The NewDiagramPage class
@@ -47,6 +50,7 @@ class NewDiagramPage : public ConfigPage {
~NewDiagramPage() override;
private:
NewDiagramPage(const NewDiagramPage &);
bool eventFilter(QObject *obj, QEvent *event) override;
public slots:
void changeToAutoFolioTab();
void setFolioAutonum(QString);
@@ -69,8 +73,27 @@ 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
QTabWidget *m_tab_widget; ///< Main tab widget (stored for later access)
// auto-numbering tab data
struct AutoNumTab {
SelectAutonumW *widget = nullptr;
QHash<QString, NumerotationContext> contexts;
QString prefix;
};
AutoNumTab m_autonum_conductor;
AutoNumTab m_autonum_element;
AutoNumTab m_autonum_folio;
void initAutoNumTab(AutoNumTab &tab, SelectAutonumW *w, const QString &prefix);
void loadAutoNumTab(AutoNumTab &tab, QSettings &settings);
void saveAutoNumContext(AutoNumTab &tab);
void removeAutoNumContext(AutoNumTab &tab);
void persistAutonumSettings();
static bool isPlaceholder(QComboBox *combo, const QString &name);
};
/**
@@ -21,8 +21,10 @@
#include "../../qeticons.h"
#include "ui_generalconfigurationpage.h"
#include "../../utils/qetsettings.h"
#include "../../utils/qetutils.h"
#include "../../qetmessagebox.h"
#include "../../textgrid.h"
#include "../nokde/kcolorbutton.h"
#include <QFileDialog>
#include <QFontDialog>
#include <QSettings>
@@ -40,9 +42,6 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
QSettings settings;
//Appearance tab
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ###Qt 6:remove
ui->m_hdpi_round_policy_widget->setDisabled(true);
#else
ui->m_hdpi_round_policy_cb->addItem(tr("Arrondi supérieur pour 0.5 et plus"), QLatin1String("Round"));
ui->m_hdpi_round_policy_cb->addItem(tr("Toujours arrondi supérieur"), QLatin1String("Ceil"));
ui->m_hdpi_round_policy_cb->addItem(tr("Toujours arrondi inférieur"), QLatin1String("Floor"));
@@ -65,9 +64,20 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
ui->m_hdpi_round_policy_cb->setCurrentIndex(4);
break;
}
#endif
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());
for (const qreal divisor : TextGrid::divisors)
ui->DiagramEditor_textGrid_cb->addItem(
divisor > 0 ? TextGrid::ratioLabel(divisor) : tr("Désactivée"),
divisor);
int text_grid_index = ui->DiagramEditor_textGrid_cb->findData(
settings.value(TextGrid::settings_key, 1).toReal());
if (text_grid_index < 0)
text_grid_index = ui->DiagramEditor_textGrid_cb->findData(qreal(1));
ui->DiagramEditor_textGrid_cb->setCurrentIndex(text_grid_index);
ui->DiagramEditor_xKeyGrid_sb->setValue(settings.value("diagrameditor/key_Xgrid", 10).toInt());
ui->DiagramEditor_yKeyGrid_sb->setValue(settings.value("diagrameditor/key_Ygrid", 10).toInt());
ui->DiagramEditor_xKeyGridFine_sb->setValue(settings.value("diagrameditor/key_fine_Xgrid", 1).toInt());
@@ -75,6 +85,12 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
ui->DiagramEditor_Grid_PointSize_min_sb->setValue(settings.value("diagrameditor/grid_pointsize_min", 1).toInt());
ui->DiagramEditor_Grid_PointSize_max_sb->setValue(settings.value("diagrameditor/grid_pointsize_max", 1).toInt());
ui->m_use_system_color_cb->setChecked(settings.value("usesystemcolors", "true").toBool());
bool sysColors = ui->m_use_system_color_cb->isChecked();
ui->m_custom_app_color_kpb->setEnabled(!sysColors);
if (settings.contains("customapplicationcolor"))
ui->m_custom_app_color_kpb->setColor(QColor(settings.value("customapplicationcolor").toString()));
else
ui->m_custom_app_color_kpb->setColor(QApplication::palette().color(QPalette::Window));
bool tabbed = settings.value("diagrameditor/viewmode", "tabbed") == "tabbed";
if(tabbed)
ui->m_use_tab_mode_rb->setChecked(true);
@@ -83,6 +99,25 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
ui->m_zoom_out_beyond_folio->setChecked(settings.value("diagrameditor/zoom-out-beyond-of-folio", false).toBool());
ui->m_use_gesture_trackpad->setChecked(settings.value("diagramview/gestures", false).toBool());
ui->m_save_label_paste->setChecked(settings.value("diagramcommands/erase-label-on-copy", true).toBool());
ui->m_enable_scripting->setChecked(QetSettings::scriptingEnabled());
#ifdef QET_HAS_SCRIPTING
if (QetSettings::scriptingForcedByEnvironment()) {
//QET_ENABLE_SCRIPTING wins over the stored value, so let the box
//say so rather than offer a tick that changes nothing.
ui->m_enable_scripting->setEnabled(false);
ui->m_enable_scripting->setToolTip(
tr("Activé par la variable d'environnement "
"QET_ENABLE_SCRIPTING ; ce réglage est sans effet "
"tant qu'elle est définie."));
}
#else
//Built without Qt Qml: there is no scripting to allow. Disabled as
//well as hidden, so applyConf() leaves the stored value alone --
//a hidden box still reports its state, and writing it here would
//quietly clear a preference set on a build that does have Qml.
ui->m_enable_scripting->setVisible(false);
ui->m_enable_scripting->setEnabled(false);
#endif
ui->m_use_folio_label->setChecked(settings.value("genericpanel/folio", true).toBool());
ui->m_border_0->setChecked(settings.value("border-columns_0", false).toBool());
ui->m_autosave_sb->setValue(settings.value("diagrameditor/autosave-interval", 0).toInt());
@@ -99,7 +134,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()) + " (" +
@@ -112,7 +147,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()) + " (" +
@@ -207,16 +242,24 @@ void GeneralConfigurationPage::applyConf()
bool must_use_system_colors = ui->m_use_system_color_cb->isChecked();
settings.setValue("usesystemcolors", must_use_system_colors);
if (was_using_system_colors != must_use_system_colors) {
QETApp::instance()->useSystemPalette(must_use_system_colors);
if (must_use_system_colors) {
QETApp::instance()->useSystemPalette(true);
} else {
QColor custom_color = ui->m_custom_app_color_kpb->color();
settings.setValue("customapplicationcolor", custom_color.name());
QETApp::instance()->useCustomPalette(custom_color);
}
} else if (!must_use_system_colors) {
QColor custom_color = ui->m_custom_app_color_kpb->color();
settings.setValue("customapplicationcolor", custom_color.name());
QETApp::instance()->useCustomPalette(custom_color);
}
settings.setValue("border-columns_0",ui->m_border_0->isChecked());
settings.setValue("lang", ui->m_lang_cb->itemData(ui->m_lang_cb->currentIndex()).toString());
//hdpi
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
QetSettings::setHdpiScaleFactorRoundingPolicy(ui->m_hdpi_round_policy_cb->currentData().toString());
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFactorRoundingPolicy());
#endif
//ELEMENT EDITOR
settings.setValue("elementeditor/default-informations", ui->m_default_elements_info->toPlainText());
@@ -230,6 +273,14 @@ void GeneralConfigurationPage::applyConf()
//DIAGRAM COMMAND
settings.setValue("diagramcommands/erase-label-on-copy", ui->m_save_label_paste->isChecked());
//SCRIPTING
//Left alone while the environment forces it on: the box is disabled
//in that case and writing its state would silently clear the user's
//real preference the first time this dialog is accepted.
if (ui->m_enable_scripting->isEnabled()) {
QetSettings::setScriptingEnabled(ui->m_enable_scripting->isChecked());
}
//GENERIC PANEL
settings.setValue("genericpanel/folio",ui->m_use_folio_label->isChecked());
@@ -240,9 +291,13 @@ 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());
settings.setValue(TextGrid::settings_key, ui->DiagramEditor_textGrid_cb->currentData());
settings.setValue("diagrameditor/key_Xgrid", ui->DiagramEditor_xKeyGrid_sb->value());
settings.setValue("diagrameditor/key_Ygrid", ui->DiagramEditor_yKeyGrid_sb->value());
settings.setValue("diagrameditor/key_fine_Xgrid", ui->DiagramEditor_xKeyGridFine_sb->value());
@@ -388,7 +443,7 @@ void GeneralConfigurationPage::fillLang()
ui->m_lang_cb->addItem(QET::Icons::hr, tr("Croate"), "hr");
ui->m_lang_cb->addItem(QET::Icons::it, tr("Italien"), "it");
ui->m_lang_cb->addItem(QET::Icons::jp, tr("Japonais"), "ja");
ui->m_lang_cb->addItem(QET::Icons::ko, tr("Coréen"), "ko");
ui->m_lang_cb->addItem(QET::Icons::ko, tr("Coréen"), "ko");
ui->m_lang_cb->addItem(QET::Icons::pl, tr("Polonais"), "pl");
ui->m_lang_cb->addItem(QET::Icons::pt, tr("Portugais"), "pt");
ui->m_lang_cb->addItem(QET::Icons::ro, tr("Roumains"), "ro");
@@ -400,9 +455,9 @@ void GeneralConfigurationPage::fillLang()
ui->m_lang_cb->addItem(QET::Icons::tr, tr("Turc"), "tr");
ui->m_lang_cb->addItem(QET::Icons::hu, tr("Hongrois"), "hu");
ui->m_lang_cb->addItem(QET::Icons::mn, tr("Mongol"), "mn");
ui->m_lang_cb->addItem(QET::Icons::uk, tr("Ukrainien"), "uk");
ui->m_lang_cb->addItem(QET::Icons::zh, tr("Chinois"), "zh");
ui->m_lang_cb->addItem(QET::Icons::se, tr("Suédois"), "sv");
ui->m_lang_cb->addItem(QET::Icons::uk, tr("Ukrainien"), "uk");
ui->m_lang_cb->addItem(QET::Icons::zh, tr("Chinois"), "zh");
ui->m_lang_cb->addItem(QET::Icons::se, tr("Suédois"), "sv");
//set current index to the lang found in setting file
//if lang doesn't exist set to system
QSettings settings;
@@ -451,11 +506,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() + ")";
@@ -555,11 +610,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() + ")";
@@ -625,3 +680,14 @@ void GeneralConfigurationPage::on_m_hdpi_round_cb_clicked(bool checked)
ui->m_hdpi_round_policy_cb->setEnabled(checked);
}
/**
@brief GeneralConfigurationPage::on_m_use_system_color_cb_toggled
Enable/disable the custom color picker when the system color
checkbox is toggled.
@param checked
*/
void GeneralConfigurationPage::on_m_use_system_color_cb_toggled(bool checked)
{
ui->m_custom_app_color_kpb->setEnabled(!checked);
}
@@ -53,6 +53,7 @@ class GeneralConfigurationPage : public ConfigPage
void on_ElementEditor_Grid_PointSize_min_sb_valueChanged(int value);
void on_m_hdpi_round_cb_clicked(bool checked);
void on_m_use_system_color_cb_toggled(bool checked);
private:
void fillLang();
+175 -107
View File
@@ -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,21 +17,32 @@
<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">
<string>Apparence</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QCheckBox" name="m_use_system_color_cb">
<property name="text">
<string>Utiliser les couleurs du système</string>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="m_system_color_layout">
<item>
<widget class="QCheckBox" name="m_use_system_color_cb">
<property name="text">
<string>Utiliser les couleurs du système</string>
</property>
</widget>
</item>
<item>
<widget class="KColorButton" name="m_custom_app_color_kpb">
<property name="toolTip">
<string>Couleur de l'application</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
@@ -59,6 +70,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">
@@ -186,6 +218,16 @@
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="m_enable_scripting">
<property name="text">
<string>Autoriser l'exécution de scripts JavaScript (Projet &gt; Exécuter un script, et --run)</string>
</property>
<property name="toolTip">
<string>Un script s'exécute avec vos droits : il peut lire et modifier le projet ouvert et écrire des fichiers. Désactivé par défaut ; n'exécutez que des scripts dont vous connaissez l'origine.</string>
</property>
</widget>
</item>
<item row="5" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -237,27 +279,6 @@
<bool>false</bool>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="1" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Répertoire de la collection commune</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="m_common_elmt_path_cb">
<item>
<property name="text">
<string>Par defaut</string>
</property>
</item>
<item>
<property name="text">
<string>Parcourir...</string>
</property>
</item>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_16">
<property name="text">
@@ -265,8 +286,8 @@
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="m_company_elmt_path_cb">
<item row="5" column="1">
<widget class="QComboBox" name="m_custom_tbt_path_cb">
<item>
<property name="text">
<string>Par defaut</string>
@@ -279,6 +300,27 @@
</item>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="m_custom_elmt_path_cb">
<item>
<property name="text">
<string>Par defaut</string>
</property>
</item>
<item>
<property name="text">
<string>Parcourir...</string>
</property>
</item>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Répertoire de la collection commune</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_17">
<property name="text">
@@ -307,8 +349,29 @@
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="m_custom_elmt_path_cb">
<item row="7" column="0">
<widget class="QLabel" name="label_23">
<property name="text">
<string>Répertoire des Macros utilisateur</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QComboBox" name="m_user_macros_path_cb">
<item>
<property name="text">
<string>Par defaut</string>
</property>
</item>
<item>
<property name="text">
<string>Parcourir...</string>
</property>
</item>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="m_common_elmt_path_cb">
<item>
<property name="text">
<string>Par defaut</string>
@@ -328,8 +391,8 @@
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="m_custom_tbt_path_cb">
<item row="2" column="1">
<widget class="QComboBox" name="m_company_elmt_path_cb">
<item>
<property name="text">
<string>Par defaut</string>
@@ -342,34 +405,13 @@
</item>
</widget>
</item>
<item row="6" column="1">
<item row="8" column="1">
<widget class="QLabel" name="label_9">
<property name="text">
<string>(Recharger les collections d'éléments pour appliquer les changements)</string>
</property>
</widget>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_11">
<property name="text">
<string>Répertoire des Macros utilisateur</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QComboBox" name="m_user_macros_path_cb">
<item>
<property name="text">
<string>Par defaut</string>
</property>
</item>
<item>
<property name="text">
<string>Parcourir...</string>
</property>
</item>
</widget>
</item>
</layout>
</widget>
</item>
@@ -742,6 +784,26 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
</property>
</spacer>
</item>
<item row="3" column="0">
<widget class="QLabel" name="Label_Diagram_textGrid">
<property name="text">
<string>Grille des textes déplacés à la souris</string>
</property>
<property name="toolTip">
<string>Fraction de la grille des folios. Maintenir Ctrl pendant le déplacement pour placer librement.</string>
</property>
</widget>
</item>
<item row="3" column="2">
<widget class="QComboBox" name="DiagramEditor_textGrid_cb">
<property name="minimumSize">
<size>
<width>80</width>
<height>0</height>
</size>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -911,30 +973,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 +996,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 +1048,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 +1071,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 +1085,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 +1172,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>
@@ -1141,4 +1202,11 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
</tabstops>
<resources/>
<connections/>
<customwidgets>
<customwidget>
<class>KColorButton</class>
<extends>QPushButton</extends>
<header>nokde/kcolorbutton.h</header>
</customwidget>
</customwidgets>
</ui>
@@ -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
+294 -12
View File
@@ -22,6 +22,8 @@
#include "../autoNum/ui/folioautonumbering.h"
#include "../autoNum/ui/formulaautonumberingw.h"
#include "../autoNum/ui/selectautonumw.h"
#include "../project/projectpropertieshandler.h"
#include "../qet.h"
#include "../qeticons.h"
#include "../qetproject.h"
#include "../borderpropertieswidget.h"
@@ -161,6 +163,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 +200,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 +222,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 +244,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 +275,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);
}
//######################################################################################//
@@ -243,9 +291,14 @@ ProjectAutoNumConfigPage::ProjectAutoNumConfigPage (QETProject *project,
QWidget *parent) :
ProjectConfigPage(project, parent)
{
initWidgets();
buildConnections();
readValuesFromProject();
// Follow the same contract ProjectMainConfigPage's constructor does --
// init() is documented to be the thing a subclass constructor calls
// (see ProjectConfigPage::init()'s doc comment). Calling the pieces
// individually here used to skip both initLayout() and adjustReadOnly(),
// and called readValuesFromProject() even when m_project was null.
// buildConnections() itself is invoked from the end of initWidgets()
// below, in the same relative position it held here.
init();
}
/**
@@ -302,9 +355,22 @@ void ProjectAutoNumConfigPage::initWidgets()
m_faw = new FolioAutonumberingW(project());
tab_widget->addTab(m_faw, tr("Numérotation auto des folios"));
QHBoxLayout *main_layout = new QHBoxLayout();
m_import_pb = new QPushButton(
tr("Importer depuis un autre projet..."), this);
m_import_pb->setToolTip(
tr("Reprendre les numérotations automatiques "
"enregistrées dans un autre projet"));
QHBoxLayout *button_layout = new QHBoxLayout();
button_layout->addStretch();
button_layout->addWidget(m_import_pb);
QVBoxLayout *main_layout = new QVBoxLayout();
main_layout->addWidget(tab_widget);
main_layout->addLayout(button_layout);
setLayout(main_layout);
buildConnections();
}
/**
@@ -313,6 +379,12 @@ void ProjectAutoNumConfigPage::initWidgets()
*/
void ProjectAutoNumConfigPage::readValuesFromProject()
{
// This is called again after an import, so start from an empty
// combo box instead of appending a second copy of every name.
m_saw_conductor->contextComboBox()->clear();
m_saw_element->contextComboBox()->clear();
m_saw_folio->contextComboBox()->clear();
//Conductor Tab
const QStringList strlc(m_project->conductorAutoNum().keys());
m_saw_conductor->contextComboBox()->addItems(strlc);
@@ -335,6 +407,9 @@ void ProjectAutoNumConfigPage::readValuesFromProject()
*/
void ProjectAutoNumConfigPage::adjustReadOnly()
{
if (m_import_pb && m_project) {
m_import_pb->setDisabled(m_project->isReadOnly());
}
}
/**
@@ -344,25 +419,28 @@ void ProjectAutoNumConfigPage::adjustReadOnly()
void ProjectAutoNumConfigPage::buildConnections()
{
//Management Tab
connect (m_amw, SIGNAL(applyPressed()), this, SLOT(applyManagement()));
connect(m_amw, &AutoNumberingManagementW::applyPressed, this, &ProjectAutoNumConfigPage::applyManagement);
//Conductor Tab
connect(m_saw_conductor, &SelectAutonumW::applyPressed, this, &ProjectAutoNumConfigPage::saveContextConductor);
connect(m_saw_conductor, &SelectAutonumW::removeClicked, this, &ProjectAutoNumConfigPage::removeContextConductor);
connect(m_saw_conductor->contextComboBox(), SIGNAL(currentIndexChanged(QString)), this, SLOT(updateContextConductor(QString)));
connect(m_saw_conductor->contextComboBox(), &QComboBox::textActivated, this, &ProjectAutoNumConfigPage::updateContextConductor);
//Element Tab
connect(m_saw_element, &SelectAutonumW::applyPressed, this, &ProjectAutoNumConfigPage::saveContextElement);
connect(m_saw_element, &SelectAutonumW::removeClicked, this, &ProjectAutoNumConfigPage::removeContextElement);
connect(m_saw_element->contextComboBox(), SIGNAL(currentIndexChanged(QString)), this, SLOT(updateContextElement(QString)));
connect(m_saw_element->contextComboBox(), &QComboBox::textActivated, this, &ProjectAutoNumConfigPage::updateContextElement);
//Folio Tab
connect(m_saw_folio, &SelectAutonumW::applyPressed, this, &ProjectAutoNumConfigPage::saveContextFolio);
connect(m_saw_folio, &SelectAutonumW::removeClicked, this, &ProjectAutoNumConfigPage::removeContextFolio);
connect(m_saw_folio->contextComboBox(), SIGNAL(currentIndexChanged(QString)), this, SLOT(updateContextFolio(QString)));
connect(m_saw_folio->contextComboBox(), &QComboBox::textActivated, this, &ProjectAutoNumConfigPage::updateContextFolio);
// Auto Folio Numbering
connect (m_faw, SIGNAL (applyPressed()), this, SLOT (applyAutoNum()));
connect(m_faw, &FolioAutonumberingW::applyPressed, this, &ProjectAutoNumConfigPage::applyAutoNum);
//Import from another project
connect(m_import_pb, &QPushButton::clicked, this, &ProjectAutoNumConfigPage::importFromProject);
}
/**
@@ -419,7 +497,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());
@@ -433,6 +511,198 @@ void ProjectAutoNumConfigPage::saveContextElement()
}
}
/**
@brief ProjectAutoNumConfigPage::importFromProject
Read the automatic numbering rules stored in another .qet project and
copy the ones the user selects into this project.
The source file is parsed as plain XML rather than opened as a
QETProject: opening it would run the whole load path, including the
modal dialog raised when the file was written by a different version
of QElectroTech.
*/
void ProjectAutoNumConfigPage::importFromProject()
{
if (!m_project || m_project->isReadOnly()) {
return;
}
const QString path = QFileDialog::getOpenFileName(
this,
tr("Importer les numérotations d'un projet"),
m_project->currentDir(),
tr("Projet QElectroTech (*.qet)"));
if (path.isEmpty()) {
return;
}
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
QMessageBox::warning(this, tr("Import impossible"),
tr("Impossible d'ouvrir %1").arg(path));
return;
}
QDomDocument doc;
if (!doc.setContent(&file)) {
QMessageBox::warning(this, tr("Import impossible"),
tr("%1 n'est pas un projet QElectroTech valide.")
.arg(QFileInfo(path).fileName()));
return;
}
file.close();
const QDomNodeList newdiagrams =
doc.elementsByTagName(QStringLiteral("newdiagrams"));
if (newdiagrams.isEmpty()) {
QMessageBox::information(this, tr("Aucune numérotation"),
tr("Ce projet ne contient aucune numérotation automatique."));
return;
}
const QDomElement root = newdiagrams.at(0).toElement();
// tag of the group, tag of one entry, label shown to the user
struct Category {
QString group_tag;
QString item_tag;
QString label;
};
const QList<Category> categories {
{QStringLiteral("conductors_autonums"),
QStringLiteral("conductor_autonum"), tr("Conducteurs")},
{QStringLiteral("element_autonums"),
QStringLiteral("element_autonum"), tr("Eléments")},
{QStringLiteral("folio_autonums"),
QStringLiteral("folio_autonum"), tr("Folios")}
};
QDialog dialog(this);
dialog.setWindowTitle(tr("Numérotations à importer"));
QVBoxLayout *layout = new QVBoxLayout(&dialog);
layout->addWidget(new QLabel(
tr("Numérotations trouvées dans %1 :")
.arg(QFileInfo(path).fileName()), &dialog));
QListWidget *list = new QListWidget(&dialog);
layout->addWidget(list);
// NumerotationContext is not a QVariant type, so the list item
// carries an index into this instead of the context itself.
QList<NumerotationContext> contexts;
for (int i = 0 ; i < categories.count() ; ++i)
{
const Category &category = categories.at(i);
QDomElement group;
for (QDomNode n = root.firstChild() ; !n.isNull() ; n = n.nextSibling()) {
if (n.toElement().tagName() == category.group_tag) {
group = n.toElement();
break;
}
}
if (group.isNull()) {
continue;
}
for (QDomElement entry : QET::findInDomElement(group, category.item_tag))
{
const QString title = entry.attribute(QStringLiteral("title"));
if (title.isEmpty()) {
continue;
}
bool exists = false;
switch (i) {
case 0: exists = m_project->conductorAutoNum().contains(title); break;
case 1: exists = m_project->elementAutoNum().contains(title); break;
default: exists = m_project->folioAutoNum().contains(title); break;
}
QListWidgetItem *item = new QListWidgetItem(
exists ? tr("%1 : %2 (existe déjà)")
.arg(category.label, title)
: QStringLiteral("%1 : %2")
.arg(category.label, title),
list);
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
item->setCheckState(exists ? Qt::Unchecked : Qt::Checked);
item->setData(Qt::UserRole, i);
item->setData(Qt::UserRole + 1, title);
NumerotationContext nc;
nc.fromXml(entry);
item->setData(Qt::UserRole + 2, contexts.count());
contexts << nc;
}
}
if (contexts.isEmpty()) {
QMessageBox::information(this, tr("Aucune numérotation"),
tr("Ce projet ne contient aucune numérotation automatique."));
return;
}
QCheckBox *overwrite_cb = new QCheckBox(
tr("Remplacer les numérotations de même nom"), &dialog);
layout->addWidget(overwrite_cb);
QDialogButtonBox *buttons = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog);
layout->addWidget(buttons);
connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
if (dialog.exec() != QDialog::Accepted) {
return;
}
int imported = 0, skipped = 0, conductors = 0;
for (int row = 0 ; row < list->count() ; ++row)
{
QListWidgetItem *item = list->item(row);
if (item->checkState() != Qt::Checked) {
continue;
}
const int category = item->data(Qt::UserRole).toInt();
const QString title = item->data(Qt::UserRole + 1).toString();
bool exists = false;
switch (category) {
case 0: exists = m_project->conductorAutoNum().contains(title); break;
case 1: exists = m_project->elementAutoNum().contains(title); break;
default: exists = m_project->folioAutoNum().contains(title); break;
}
if (exists && !overwrite_cb->isChecked()) {
++skipped;
continue;
}
const NumerotationContext &nc =
contexts.at(item->data(Qt::UserRole + 2).toInt());
switch (category) {
case 0:
m_project->addConductorAutoNum(title, nc);
++conductors;
break;
case 1: m_project->addElementAutoNum(title, nc); break;
default: m_project->addFolioAutoNum(title, nc); break;
}
++imported;
}
readValuesFromProject();
if (conductors) {
m_project->conductorAutoNumAdded();
}
QMessageBox::information(
this, tr("Import terminé"),
skipped ? tr("%1 numérotation(s) importée(s), "
"%2 conservée(s) telles quelles.")
.arg(imported).arg(skipped)
: tr("%1 numérotation(s) importée(s).").arg(imported));
}
/**
@brief ProjectAutoNumConfigPage::removeContextElement
Remove from project the current element numerotation context
@@ -444,6 +714,10 @@ void ProjectAutoNumConfigPage::removeContextElement()
return;
m_project->removeElementAutoNum (m_saw_element->contextComboBox()->currentText());
m_saw_element->contextComboBox()->removeItem (m_saw_element->contextComboBox()->currentIndex());
// removeItem() removes the current selection programmatically but
// textActivated() does not react to (by design, see buildConnections()).
// Refresh the displayed pattern explicitly so it matches the new selection.
updateContextElement(m_saw_element->contextComboBox()->currentText());
}
/**
@@ -461,7 +735,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 +763,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());
}
@@ -631,6 +905,10 @@ void ProjectAutoNumConfigPage::removeContextConductor()
if ( m_saw_conductor->contextComboBox()-> currentText() == tr("Nom de la nouvelle numérotation") ) return;
m_project -> removeConductorAutoNum (m_saw_conductor->contextComboBox()-> currentText() );
m_saw_conductor->contextComboBox()-> removeItem (m_saw_conductor->contextComboBox()-> currentIndex() );
// removeItem() removes the current selection programmatically but
// textActivated() does not react to (by design, see buildConnections()).
// Refresh the displayed pattern explicitly so it matches the new selection.
updateContextConductor(m_saw_conductor->contextComboBox()->currentText());
project()->conductorAutoNumRemoved();
}
@@ -644,6 +922,10 @@ void ProjectAutoNumConfigPage::removeContextFolio()
if ( m_saw_folio->contextComboBox() -> currentText() == tr("Nom de la nouvelle numérotation") ) return;
m_project -> removeFolioAutoNum (m_saw_folio->contextComboBox() -> currentText() );
m_saw_folio->contextComboBox() -> removeItem (m_saw_folio->contextComboBox() -> currentIndex() );
// removeItem() removes the current selection programmatically but
// textActivated() does not react to (by design, see buildConnections()).
// Refresh the displayed pattern explicitly so it matches the new selection.
updateContextFolio(m_saw_folio->contextComboBox()->currentText());
project()->folioAutoNumRemoved();
}
+12 -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 {
@@ -147,6 +156,7 @@ class ProjectAutoNumConfigPage : public ProjectConfigPage {
void updateContextElement(const QString&);//element
void saveContextElement();
void removeContextElement();
void importFromProject();
void applyAutoNum();
void applyManagement();
@@ -164,6 +174,7 @@ class ProjectAutoNumConfigPage : public ProjectConfigPage {
SelectAutonumW *m_saw_element;
FolioAutonumberingW *m_faw;
AutoNumberingManagementW *m_amw;
QPushButton *m_import_pb = nullptr;
};
@@ -0,0 +1,385 @@
/*
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 "shortcutsconfigpage.h"
#include "../../qeticons.h"
#include "../../shortcutmanager.h"
#include <QComboBox>
#include <QFrame>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QKeySequenceEdit>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QRegularExpression>
#include <QToolButton>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QVBoxLayout>
#include <algorithm>
#include <utility>
/**
@brief normalizedForSearch
Decompose \a text and strip every non-spacing combining mark, then fold the
case. Both the query terms and the row's searchable text are run through
this, so "general" matches "Général" and "Ctrl+S" matches "ctrl+s"
regardless of the keyboard layout the query was typed on.
*/
static QString normalizedForSearch(const QString &text)
{
const QString decomposed = text.normalized(QString::NormalizationForm_D);
QString stripped;
stripped.reserve(decomposed.size());
for (const QChar &c : decomposed) {
if (c.category() != QChar::Mark_NonSpacing) {
stripped.append(c);
}
}
return stripped.toCaseFolded();
}
/**
@brief ShortcutsConfigPage::ShortcutsConfigPage
@param parent
*/
ShortcutsConfigPage::ShortcutsConfigPage(QWidget *parent) :
ConfigPage(parent)
{
auto *vlayout = new QVBoxLayout();
QLabel *title_label = new QLabel(this->title());
vlayout->addWidget(title_label);
QFrame *horiz_line = new QFrame();
horiz_line->setFrameShape(QFrame::HLine);
vlayout->addWidget(horiz_line);
m_filter_edit = new QLineEdit(this);
m_filter_edit->setPlaceholderText(tr("Filtrer les raccourcis…"));
connect(m_filter_edit, &QLineEdit::textChanged, this, &ShortcutsConfigPage::filterRows);
m_quick_filter = new QComboBox(this);
m_quick_filter->setObjectName(QStringLiteral("quickFilterCombo"));
m_quick_filter->addItem(tr("Tous"));
m_quick_filter->addItem(tr("Attribués uniquement"));
m_quick_filter->addItem(tr("Non attribués uniquement"));
m_quick_filter->addItem(tr("Conflits uniquement"));
connect(m_quick_filter, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &ShortcutsConfigPage::quickFilterChanged);
m_count_label = new QLabel(this);
m_count_label->setObjectName(QStringLiteral("shortcutCountLabel"));
auto *filter_layout = new QHBoxLayout();
filter_layout->addWidget(m_filter_edit, 1);
filter_layout->addWidget(m_quick_filter);
filter_layout->addWidget(m_count_label);
vlayout->addLayout(filter_layout);
m_tree = new QTreeWidget(this);
m_tree->setHeaderLabels({tr("Action"), tr("Raccourci"), QString()});
m_tree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
m_tree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
m_tree->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
m_tree->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_tree->setSelectionMode(QAbstractItemView::NoSelection);
vlayout->addWidget(m_tree);
auto *reset_all_button = new QPushButton(tr("Tout réinitialiser"), this);
connect(reset_all_button, &QPushButton::clicked, this, &ShortcutsConfigPage::resetAllRows);
auto *bottom_layout = new QHBoxLayout();
bottom_layout->addStretch();
bottom_layout->addWidget(reset_all_button);
vlayout->addLayout(bottom_layout);
setLayout(vlayout);
populateTable();
applyFilter();
}
ShortcutsConfigPage::~ShortcutsConfigPage()
{
}
/**
@brief ShortcutsConfigPage::populateTable
Fill the tree with one collapsible top-level node per category and one child
per shortcut known to ShortcutManager, sorted by category then action name.
*/
void ShortcutsConfigPage::populateTable()
{
QList<ShortcutManager::ShortcutInfo> shortcuts = ShortcutManager::instance().allShortcuts();
std::sort(shortcuts.begin(), shortcuts.end(),
[](const ShortcutManager::ShortcutInfo &a, const ShortcutManager::ShortcutInfo &b) {
if (a.category != b.category) {
return a.category < b.category;
}
return a.description < b.description;
});
m_tree->clear();
m_rows.clear();
m_rows.reserve(shortcuts.size());
QHash<QString, QTreeWidgetItem *> category_nodes;
for (const ShortcutManager::ShortcutInfo &info : shortcuts) {
QTreeWidgetItem *category_item = category_nodes.value(info.category, nullptr);
if (!category_item) {
category_item = new QTreeWidgetItem(m_tree);
category_item->setText(0, info.category);
category_item->setFlags(category_item->flags() & ~Qt::ItemIsEditable);
category_nodes.insert(info.category, category_item);
}
auto *child = new QTreeWidgetItem(category_item);
child->setText(0, info.description);
child->setFlags(child->flags() & ~Qt::ItemIsEditable);
auto *edit = new QKeySequenceEdit(info.current_sequence, m_tree);
connect(edit, &QKeySequenceEdit::editingFinished, this, &ShortcutsConfigPage::checkConflicts);
m_tree->setItemWidget(child, 1, edit);
auto *reset_button = new QToolButton(m_tree);
reset_button->setIcon(QET::Icons::EditUndo);
reset_button->setToolTip(tr("Réinitialiser ce raccourci"));
reset_button->setAutoRaise(true);
const int row_index = m_rows.size();
connect(reset_button, &QToolButton::clicked, this, [this, row_index]() { resetRow(row_index); });
m_tree->setItemWidget(child, 2, reset_button);
m_rows << Row{info.id, info.category, info.description, info.default_sequence, edit, child, false};
}
checkConflicts();
}
/**
@brief ShortcutsConfigPage::filterRows
Re-run the combined text + quick filter whenever the search box changes.
*/
void ShortcutsConfigPage::filterRows(const QString &filter_text)
{
Q_UNUSED(filter_text)
applyFilter();
}
/**
@brief ShortcutsConfigPage::quickFilterChanged
Re-run the combined text + quick filter whenever the quick filter changes.
*/
void ShortcutsConfigPage::quickFilterChanged(int index)
{
Q_UNUSED(index)
applyFilter();
}
/**
@brief ShortcutsConfigPage::applyFilter
Hide every row that doesn't match both the search box and the quick filter.
The text query is split on whitespace and every term must match the category,
action name or current sequence (accent- and case-insensitively). Matching
category nodes are expanded so hits are not hidden inside collapsed groups,
and the "N actions" label tracks how many actions remain visible.
*/
void ShortcutsConfigPage::applyFilter()
{
const QString needle = m_filter_edit->text().trimmed();
const QStringList terms = needle.isEmpty()
? QStringList()
: needle.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
const int quick_filter = m_quick_filter->currentIndex();
int visible_actions = 0;
for (const Row &row : std::as_const(m_rows)) {
// Text is matched by substring, but the key sequence is matched exactly:
// a substring match would let "Ctrl+S" also hit "Ctrl+Shift+F" (the "S"
// of "Shift"), which is precisely the kind of false positive that hides
// the one binding the user is looking for.
const QString text_haystack = normalizedForSearch(
row.category + QLatin1Char(' ') + row.description);
const QString sequence_text = normalizedForSearch(row.edit->keySequence().toString());
bool matches = true;
for (const QString &term : terms) {
const QString t = normalizedForSearch(term);
if (!text_haystack.contains(t) && sequence_text != t) {
matches = false;
break;
}
}
if (matches) {
switch (quick_filter) {
case BoundOnly:
matches = !row.edit->keySequence().isEmpty();
break;
case UnboundOnly:
matches = row.edit->keySequence().isEmpty();
break;
case ConflictsOnly:
matches = row.conflicted;
break;
default:
break;
}
}
row.item->setHidden(!matches);
if (matches) {
++visible_actions;
}
}
const bool filtering = !needle.isEmpty() || quick_filter != ShowAll;
for (int i = 0; i < m_tree->topLevelItemCount(); ++i) {
QTreeWidgetItem *top = m_tree->topLevelItem(i);
bool any_visible = false;
for (int j = 0; j < top->childCount(); ++j) {
if (!top->child(j)->isHidden()) {
any_visible = true;
break;
}
}
top->setHidden(!any_visible);
top->setExpanded(filtering && any_visible);
}
m_count_label->setText(tr("%n action(s)", nullptr, visible_actions));
}
// IDs use stable context prefixes; translated display categories are not scopes.
// Main-window actions are shared by the editors, and depth actions by the
// diagram and element editors. Panel-specific shortcuts retain their own scope.
static bool shortcutScopesOverlap(const QString &first_id, const QString &second_id)
{
const QString first = first_id.section(QLatin1Char('.'), 0, 0);
const QString second = second_id.section(QLatin1Char('.'), 0, 0);
if (first == second || first == QLatin1String("mainwindow")
|| second == QLatin1String("mainwindow"))
return true;
const auto has_depth = [](const QString &scope) {
return scope == QLatin1String("diagrameditor")
|| scope == QLatin1String("elementeditor");
};
return (first == QLatin1String("depth") && has_depth(second))
|| (second == QLatin1String("depth") && has_depth(first));
}
/**
@brief ShortcutsConfigPage::checkConflicts
Highlight every row whose currently-edited sequence is shared, non-empty,
with another row, and explain the conflict in the shortcut editor's tooltip.
*/
void ShortcutsConfigPage::checkConflicts()
{
QHash<QString, QList<int>> sequence_to_rows;
for (int row = 0; row < m_rows.size(); ++row) {
const QString sequence_text = m_rows.at(row).edit->keySequence().toString();
if (!sequence_text.isEmpty()) {
sequence_to_rows[sequence_text] << row;
}
}
for (int row = 0; row < m_rows.size(); ++row) {
Row &current_row = m_rows[row];
const QString sequence_text = current_row.edit->keySequence().toString();
QList<int> conflicting_rows;
for (int other : sequence_to_rows.value(sequence_text)) {
if (other != row && shortcutScopesOverlap(current_row.id, m_rows.at(other).id))
conflicting_rows << other;
}
const bool conflicted = !conflicting_rows.isEmpty();
current_row.conflicted = conflicted;
if (conflicted) {
QStringList other_descriptions;
for (int other_row : conflicting_rows) {
if (other_row != row) {
other_descriptions << m_rows.at(other_row).description;
}
}
current_row.item->setBackground(0, QColor(255, 205, 205));
current_row.edit->setToolTip(
tr("Ce raccourci est aussi utilisé par : %1").arg(other_descriptions.join(QStringLiteral(", "))));
} else {
current_row.item->setBackground(0, QBrush());
current_row.edit->setToolTip(QString());
}
}
// A sequence edit can change while a filter is active (search-by-key or the
// conflicts-only quick filter); refresh the visible set so the list doesn't
// show stale results.
const bool filtering = !m_filter_edit->text().trimmed().isEmpty()
|| m_quick_filter->currentIndex() != ShowAll;
if (filtering) {
applyFilter();
}
}
/**
@brief ShortcutsConfigPage::resetRow
Reset the shortcut editor at \a row_index to its default sequence.
*/
void ShortcutsConfigPage::resetRow(int row_index)
{
if (row_index < 0 || row_index >= m_rows.size()) {
return;
}
m_rows.at(row_index).edit->setKeySequence(m_rows.at(row_index).default_sequence);
checkConflicts();
}
void ShortcutsConfigPage::resetAllRows()
{
for (const Row &row : std::as_const(m_rows)) {
row.edit->setKeySequence(row.default_sequence);
}
checkConflicts();
}
/**
@brief ShortcutsConfigPage::applyConf
Persist every row's shortcut edit through ShortcutManager, which also
applies it immediately to every currently live QAction sharing that id.
*/
void ShortcutsConfigPage::applyConf()
{
for (const Row &row : std::as_const(m_rows)) {
ShortcutManager::instance().setSequence(row.id, row.edit->keySequence());
}
}
QString ShortcutsConfigPage::title() const
{
return tr("Raccourcis", "configuration page title");
}
QIcon ShortcutsConfigPage::icon() const
{
return QET::Icons::ConfigureShortcuts;
}
@@ -0,0 +1,86 @@
/*
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 SHORTCUTSCONFIGPAGE_H
#define SHORTCUTSCONFIGPAGE_H
#include "configpage.h"
#include <QKeySequence>
class QComboBox;
class QKeySequenceEdit;
class QLabel;
class QLineEdit;
class QTreeWidget;
class QTreeWidgetItem;
/**
@brief The ShortcutsConfigPage class
Configuration page listing every shortcut registered with ShortcutManager,
letting the user rebind, reset and search them. Bindings are only
persisted (via ShortcutManager::setSequence()) when applyConf() runs, i.e.
when the user validates the surrounding ConfigDialog.
*/
class ShortcutsConfigPage : public ConfigPage
{
Q_OBJECT
public:
explicit ShortcutsConfigPage(QWidget *parent = nullptr);
~ShortcutsConfigPage() override;
void applyConf() override;
QString title() const override;
QIcon icon() const override;
private slots:
void filterRows(const QString &filter_text);
void quickFilterChanged(int index);
void checkConflicts();
void resetAllRows();
private:
enum QuickFilter {
ShowAll = 0,
BoundOnly,
UnboundOnly,
ConflictsOnly
};
struct Row {
QString id;
QString category;
QString description;
QKeySequence default_sequence;
QKeySequenceEdit *edit;
QTreeWidgetItem *item;
bool conflicted;
};
void populateTable();
void applyFilter();
void resetRow(int row_index);
QLineEdit *m_filter_edit;
QComboBox *m_quick_filter;
QLabel *m_count_label;
QTreeWidget *m_tree;
QList<Row> m_rows;
};
#endif // SHORTCUTSCONFIGPAGE_H
@@ -0,0 +1,302 @@
/*
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 "spacemouseconfigpage.h"
#include "../../qeticons.h"
#include "../../shortcutmanager.h"
#include "../../spacemouse/spacemousebuttonmap.h"
#include "../../spacemouse/spacemousemotion.h"
#include <QCheckBox>
#include <QComboBox>
#include <QFormLayout>
#include <QFrame>
#include <QGroupBox>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QLabel>
#include <QPushButton>
#include <QSpinBox>
#include <QTableWidget>
#include <QToolButton>
#include <QVBoxLayout>
#include <algorithm>
namespace {
constexpr int COL_BUTTON = 0;
constexpr int COL_ACTION = 1;
constexpr int COL_REMOVE = 2;
//The combo box's first entry: no action bound, i.e. this row does
//nothing. Real action ids are never empty (ShortcutManager assigns
//them from source, not user input), so an empty string can never
//collide with a real one.
const QString UNBOUND_LABEL = QObject::tr("(Aucune action)", "spacemouse config: unbound button");
}
/**
@brief SpaceMouseConfigPage::SpaceMouseConfigPage
@param parent
*/
SpaceMouseConfigPage::SpaceMouseConfigPage(QWidget *parent) :
ConfigPage(parent)
{
auto *vlayout = new QVBoxLayout();
QLabel *title_label = new QLabel(this->title());
vlayout->addWidget(title_label);
QFrame *horiz_line = new QFrame();
horiz_line->setFrameShape(QFrame::HLine);
vlayout->addWidget(horiz_line);
vlayout->addWidget(buildMotionGroup());
auto *intro_label = new QLabel(
tr("Associez un numéro de bouton de votre souris 3D (SpaceMouse, SpacePilot...) "
"à une action de QElectroTech. Le numéro de bouton dépend de votre appareil "
"et de son pilote -- reportez-vous à sa documentation, ou essayez successivement "
"les valeurs à partir de 0.",
"spacemouse config page intro"));
intro_label->setWordWrap(true);
auto *buttons_layout = new QVBoxLayout();
buttons_layout->addWidget(intro_label);
m_table = new QTableWidget(0, 3, this);
m_table->setHorizontalHeaderLabels({tr("N° bouton"), tr("Action"), QString()});
m_table->horizontalHeader()->setSectionResizeMode(COL_BUTTON, QHeaderView::ResizeToContents);
m_table->horizontalHeader()->setSectionResizeMode(COL_ACTION, QHeaderView::Stretch);
m_table->horizontalHeader()->setSectionResizeMode(COL_REMOVE, QHeaderView::ResizeToContents);
m_table->verticalHeader()->setVisible(false);
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_table->setSelectionMode(QAbstractItemView::NoSelection);
buttons_layout->addWidget(m_table);
auto *add_button = new QPushButton(QET::Icons::Add, tr("Ajouter une association"), this);
connect(add_button, &QPushButton::clicked, this, &SpaceMouseConfigPage::addRow);
auto *bottom_layout = new QHBoxLayout();
bottom_layout->addWidget(add_button);
bottom_layout->addStretch();
buttons_layout->addLayout(bottom_layout);
auto *buttons_group = new QGroupBox(tr("Boutons"), this);
buttons_group->setLayout(buttons_layout);
vlayout->addWidget(buttons_group);
setLayout(vlayout);
populateTable();
}
SpaceMouseConfigPage::~SpaceMouseConfigPage()
{
}
/**
@brief SpaceMouseConfigPage::buildMotionGroup
@return the "motion" part of the page, filled from the saved
SpaceMouseSettings
*/
QGroupBox *SpaceMouseConfigPage::buildMotionGroup()
{
const SpaceMouseSettings settings = SpaceMouseSettings::load();
auto speed_box = [this](int value) {
auto *spin = new QSpinBox(this);
spin->setRange(10, 400);
spin->setSingleStep(10);
spin->setSuffix(QStringLiteral(" %"));
spin->setValue(value);
return spin;
};
m_pan_speed = speed_box(settings.pan_speed);
m_zoom_speed = speed_box(settings.zoom_speed);
m_dead_zone = new QSpinBox(this);
m_dead_zone->setRange(0, 200);
m_dead_zone->setValue(settings.dead_zone);
m_dead_zone->setToolTip(tr("Les petits mouvements en dessous de ce seuil sont ignorés. "
"Augmentez-le si la vue dérive quand vous ne touchez pas la souris."));
m_zoom_axis = new QComboBox(this);
m_zoom_axis->addItem(tr("Pousser / tirer le capuchon"),
static_cast<int>(SpaceMouseSettings::ZoomAxis::PushPull));
m_zoom_axis->addItem(tr("Tourner le capuchon"),
static_cast<int>(SpaceMouseSettings::ZoomAxis::Twist));
m_zoom_axis->setCurrentIndex(
m_zoom_axis->findData(static_cast<int>(settings.zoom_axis)));
m_invert_pan_x = new QCheckBox(tr("Inverser le déplacement horizontal"), this);
m_invert_pan_x->setChecked(settings.invert_pan_x);
m_invert_pan_y = new QCheckBox(tr("Inverser le déplacement vertical"), this);
m_invert_pan_y->setChecked(settings.invert_pan_y);
m_invert_zoom = new QCheckBox(tr("Inverser le zoom"), this);
m_invert_zoom->setChecked(settings.invert_zoom);
auto *form = new QFormLayout();
form->addRow(tr("Vitesse de déplacement :"), m_pan_speed);
form->addRow(tr("Vitesse du zoom :"), m_zoom_speed);
form->addRow(tr("Zoomer en :"), m_zoom_axis);
form->addRow(tr("Zone morte :"), m_dead_zone);
form->addRow(m_invert_pan_x);
form->addRow(m_invert_pan_y);
form->addRow(m_invert_zoom);
auto *group = new QGroupBox(tr("Mouvement"), this);
group->setLayout(form);
return group;
}
/**
@brief SpaceMouseConfigPage::populateTable
Fill the table with one row per binding currently persisted in
SpaceMouseButtonMap.
*/
void SpaceMouseConfigPage::populateTable()
{
const QMap<int, QString> bindings = SpaceMouseButtonMap::allBindings();
for (auto it = bindings.constBegin(); it != bindings.constEnd(); ++it) {
appendRow(it.key(), it.value());
}
}
/**
@brief SpaceMouseConfigPage::appendRow
Add one row bound to \a button / \a action_id (either or both may be
the "unset" value -- 0 and an empty string respectively -- for a fresh
row added via the "+" button).
@param button
@param action_id
*/
void SpaceMouseConfigPage::appendRow(int button, const QString &action_id)
{
const int row = m_table->rowCount();
m_table->setRowCount(row + 1);
auto *button_spin = new QSpinBox(m_table);
button_spin->setRange(0, 999);
button_spin->setValue(button);
m_table->setCellWidget(row, COL_BUTTON, button_spin);
auto *action_combo = new QComboBox(m_table);
action_combo->addItem(UNBOUND_LABEL, QString());
QList<ShortcutManager::ShortcutInfo> shortcuts = ShortcutManager::instance().allShortcuts();
std::sort(shortcuts.begin(), shortcuts.end(),
[](const ShortcutManager::ShortcutInfo &a, const ShortcutManager::ShortcutInfo &b) {
if (a.category != b.category) {
return a.category < b.category;
}
return a.description < b.description;
});
for (const ShortcutManager::ShortcutInfo &info : shortcuts) {
action_combo->addItem(
QStringLiteral("%1 — %2").arg(info.category, info.description), info.id);
}
int index_for_current = action_combo->findData(action_id);
action_combo->setCurrentIndex(index_for_current >= 0 ? index_for_current : 0);
m_table->setCellWidget(row, COL_ACTION, action_combo);
auto *remove_button = new QToolButton(m_table);
remove_button->setIcon(QET::Icons::EditTableDeleteRow);
remove_button->setToolTip(tr("Supprimer cette association"));
connect(remove_button, &QToolButton::clicked, this, &SpaceMouseConfigPage::removeSelectedRow);
m_table->setCellWidget(row, COL_REMOVE, remove_button);
}
/**
@brief SpaceMouseConfigPage::addRow
Add a fresh, unbound row for the user to fill in.
*/
void SpaceMouseConfigPage::addRow()
{
appendRow(0, QString());
}
/**
@brief SpaceMouseConfigPage::removeSelectedRow
Remove whichever row owns the remove button that was clicked. Found by
looking the sender up in the button column rather than tracked
per-button state, so nothing has to be kept in sync as rows are added
and removed around it.
*/
void SpaceMouseConfigPage::removeSelectedRow()
{
auto *button = qobject_cast<QToolButton *>(sender());
if (!button) {
return;
}
for (int row = 0; row < m_table->rowCount(); ++row) {
if (m_table->cellWidget(row, COL_REMOVE) == button) {
m_table->removeRow(row);
return;
}
}
}
/**
@brief SpaceMouseConfigPage::applyConf
Save the motion settings, then persist exactly what the table
currently shows: every previously saved binding is cleared first,
then every row still in the table (after any
adds/edits/removals) is written back. Simpler and less error-prone than
tracking which individual rows changed, and cheap -- this runs once,
when the user validates the surrounding ConfigDialog, not on every
keystroke.
*/
void SpaceMouseConfigPage::applyConf()
{
SpaceMouseSettings settings;
settings.pan_speed = m_pan_speed->value();
settings.zoom_speed = m_zoom_speed->value();
settings.dead_zone = m_dead_zone->value();
settings.zoom_axis = static_cast<SpaceMouseSettings::ZoomAxis>(
m_zoom_axis->currentData().toInt());
settings.invert_pan_x = m_invert_pan_x->isChecked();
settings.invert_pan_y = m_invert_pan_y->isChecked();
settings.invert_zoom = m_invert_zoom->isChecked();
settings.save();
const QMap<int, QString> previous = SpaceMouseButtonMap::allBindings();
for (auto it = previous.constBegin(); it != previous.constEnd(); ++it) {
SpaceMouseButtonMap::setActionId(it.key(), QString());
}
for (int row = 0; row < m_table->rowCount(); ++row)
{
auto *button_spin = qobject_cast<QSpinBox *>(m_table->cellWidget(row, COL_BUTTON));
auto *action_combo = qobject_cast<QComboBox *>(m_table->cellWidget(row, COL_ACTION));
if (!button_spin || !action_combo) {
continue;
}
SpaceMouseButtonMap::setActionId(button_spin->value(), action_combo->currentData().toString());
}
}
QString SpaceMouseConfigPage::title() const
{
return tr("Souris 3D", "configuration page title");
}
QIcon SpaceMouseConfigPage::icon() const
{
return QET::Icons::ConfigureToolbars;
}
@@ -0,0 +1,84 @@
/*
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 SPACEMOUSECONFIGPAGE_H
#define SPACEMOUSECONFIGPAGE_H
#include "configpage.h"
class QCheckBox;
class QComboBox;
class QGroupBox;
class QSpinBox;
class QTableWidget;
/**
@brief The SpaceMouseConfigPage class
Configuration page for the 3D mouse: how motion pans and zooms the view
(SpaceMouseSettings: speeds, dead zone, which axes are inverted, and
whether zoom follows push/pull or twist), then every
device-button-to-action binding (SpaceMouseButtonMap), letting the
user add, edit and remove them.
Modelled directly on ShortcutsConfigPage, one row per binding instead
of one row per shortcut, since the two are close cousins: both end in
the same place -- ShortcutManager::trigger()/registerAction() -- just
from a different input source. Bindings are only persisted (via
SpaceMouseButtonMap::setActionId()) when applyConf() runs, i.e. when
the user validates the surrounding ConfigDialog.
Only compiled in, and only added to the Configuration dialog, when
QET_SPACEMOUSE_SUPPORT is defined -- see qetapp.cpp.
Button numbering is intentionally not fixed to any particular device:
it is exactly whatever integer SpaceMouseBackend::buttonPressed()
reports for the button pressed, which is backend- and device-specific.
Rows are added/removed freely rather than the page assuming a button
count, since that varies from 2 (SpaceMouse Compact) to over 30
(SpacePilot Pro).
*/
class SpaceMouseConfigPage : public ConfigPage
{
Q_OBJECT
public:
explicit SpaceMouseConfigPage(QWidget *parent = nullptr);
~SpaceMouseConfigPage() override;
void applyConf() override;
QString title() const override;
QIcon icon() const override;
private slots:
void addRow();
void removeSelectedRow();
private:
QGroupBox *buildMotionGroup();
void populateTable();
void appendRow(int button, const QString &action_id);
QSpinBox *m_pan_speed;
QSpinBox *m_zoom_speed;
QSpinBox *m_dead_zone;
QComboBox *m_zoom_axis;
QCheckBox *m_invert_pan_x;
QCheckBox *m_invert_pan_y;
QCheckBox *m_invert_zoom;
QTableWidget *m_table;
};
#endif // SPACEMOUSECONFIGPAGE_H
+2 -2
View File
@@ -29,7 +29,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="folder-open" resource="../../qelectrotech.qrc">
<normaloff>:/ico/16x16/folder-open.png</normaloff>:/ico/16x16/folder-open.png</iconset>
</property>
</widget>
@@ -40,7 +40,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="document-save" resource="../../qelectrotech.qrc">
<normaloff>:/ico/16x16/document-save.png</normaloff>:/ico/16x16/document-save.png</iconset>
</property>
</widget>
+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
+113
View File
@@ -0,0 +1,113 @@
/*
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 "customelementinfopartwidget.h"
#include "../diagramcontext.h"
#include "../qeticons.h"
#include <QGridLayout>
#include <QLineEdit>
#include <QToolButton>
/**
@brief CustomElementInfoPartWidget::CustomElementInfoPartWidget
Constructor
@param key initial key name (empty for a freshly added row)
@param value initial value
@param parent parent widget
*/
CustomElementInfoPartWidget::CustomElementInfoPartWidget(
const QString &key,
const QString &value,
QWidget *parent) :
QWidget(parent),
m_key_edit(new QLineEdit(key, this)),
m_value_edit(new QLineEdit(value, this)),
m_remove_button(new QToolButton(this))
{
m_key_edit->setPlaceholderText(tr("nom_de_la_propriete"));
m_key_edit->setToolTip(tr("Lettres minuscules, chiffres, tiret et underscore uniquement"));
m_value_edit->setClearButtonEnabled(true);
m_remove_button->setIcon(QET::Icons::Remove);
m_remove_button->setToolTip(tr("Supprimer cette propriété"));
m_remove_button->setAutoRaise(true);
auto *layout = new QGridLayout(this);
layout->setContentsMargins(0, 2, 0, 2);
layout->setVerticalSpacing(2);
layout->setHorizontalSpacing(0);
layout->addWidget(m_key_edit, 0, 0);
layout->addWidget(m_value_edit, 1, 0);
layout->addWidget(m_remove_button, 0, 1, 2, 1);
connect(m_key_edit, &QLineEdit::textChanged, this, &CustomElementInfoPartWidget::validateKey);
connect(m_key_edit, &QLineEdit::textChanged, this, &CustomElementInfoPartWidget::changed);
connect(m_value_edit, &QLineEdit::textChanged, this, &CustomElementInfoPartWidget::changed);
connect(m_remove_button, &QToolButton::clicked, this, [this]() {
emit removeRequested(this);
});
setFocusProxy(m_key_edit);
validateKey();
}
CustomElementInfoPartWidget::~CustomElementInfoPartWidget()
{
}
/**
@return the key name currently typed in this row
*/
QString CustomElementInfoPartWidget::key() const
{
return m_key_edit->text().trimmed();
}
/**
@return the value currently typed in this row
*/
QString CustomElementInfoPartWidget::value() const
{
return m_value_edit->text();
}
/**
@return true if the typed key is non-empty and matches
DiagramContext::isKeyAcceptable()
*/
bool CustomElementInfoPartWidget::hasValidKey() const
{
const QString k = key();
return !k.isEmpty() && DiagramContext::isKeyAcceptable(k);
}
/**
@brief CustomElementInfoPartWidget::validateKey
Flag the key field when it doesn't match the accepted format,
instead of silently dropping it later.
*/
void CustomElementInfoPartWidget::validateKey()
{
const QString k = key();
if (k.isEmpty() || DiagramContext::isKeyAcceptable(k)) {
m_key_edit->setStyleSheet(QString());
} else {
m_key_edit->setStyleSheet(QStringLiteral("border: 1px solid red;"));
}
}
+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 CUSTOMELEMENTINFOPARTWIDGET_H
#define CUSTOMELEMENTINFOPARTWIDGET_H
#include <QWidget>
class QLineEdit;
class QToolButton;
/**
@brief The CustomElementInfoPartWidget class
A single row letting the user define their own element information
key/value pair, unlike ElementInfoPartWidget which is bound to one
predefined key. The key is validated against
DiagramContext::isKeyAcceptable() as the user types.
*/
class CustomElementInfoPartWidget : public QWidget
{
Q_OBJECT
public:
explicit CustomElementInfoPartWidget(
const QString &key = QString(),
const QString &value = QString(),
QWidget *parent = nullptr);
~CustomElementInfoPartWidget() override;
QString key() const;
QString value() const;
bool hasValidKey() const;
signals:
void changed();
void removeRequested(CustomElementInfoPartWidget *self);
private slots:
void validateKey();
private:
QLineEdit *m_key_edit;
QLineEdit *m_value_edit;
QToolButton *m_remove_button;
};
#endif // CUSTOMELEMENTINFOPARTWIDGET_H
+281
View File
@@ -0,0 +1,281 @@
/*
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 "diagrambgcolorbutton.h"
#include "../diagram.h"
#include "../diagramview.h"
#include "../palettegraphicsview.h"
#include "../qetdiagrameditor.h"
#include "../projectview.h"
#include "../qetproject.h"
#include "../utils/qetsettings.h"
#include <QApplication>
#include <QColorDialog>
#include <QMenu>
#include <QPainter>
#include <QPixmap>
namespace {
struct NamedColor { const char *context_name; QColor color; };
QList<NamedColor> standardColors()
{
return {
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Blanc"), QColor(0xFF, 0xFF, 0xFF)},
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Blanc cassé"), QColor(0xFD, 0xFB, 0xF5)},
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Gris clair"), QColor(0xE0, 0xE0, 0xE0)},
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Gris"), QColor(0x80, 0x80, 0x80)},
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Gris foncé"), QColor(0x40, 0x40, 0x40)},
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Noir"), QColor(0x00, 0x00, 0x00)},
};
}
const int MAX_RECENT = 6;
}
/**
@brief DiagramBgColorToolButton::DiagramBgColorToolButton
@param editor : the diagram editor this button acts on
@param parent
*/
DiagramBgColorToolButton::DiagramBgColorToolButton(QETDiagramEditor *editor, QWidget *parent) :
QToolButton(parent),
m_editor(editor)
{
setPopupMode(QToolButton::InstantPopup);
setToolTip(tr("Couleur de fond du folio"));
setStatusTip(tr("Choisir la couleur de fond du folio",
"status bar tip"));
//main() already restored the stored choice onto the global
//Diagram::background_color and PaletteGraphicsView flag; mirror it
//here so the button opens on what the editor actually draws, rather
//than always claiming "system colour" as it used to.
if (PaletteGraphicsView::customBackgroundColor()) {
m_is_system_color = false;
m_current = Diagram::background_color;
} else {
m_is_system_color = true;
m_current = QApplication::palette().color(QPalette::Base);
}
for (const QString &name : QetSettings::sheetBackgroundRecentColors())
{
const QColor c(name);
if (c.isValid()) {
m_recent.append(c);
}
}
while (m_recent.size() > MAX_RECENT) {
m_recent.removeLast();
}
setMenu(new QMenu(this));
rebuildMenu();
setSwatch(m_current);
}
/**
@brief DiagramBgColorToolButton::rebuildMenu
*/
void DiagramBgColorToolButton::rebuildMenu()
{
QMenu *m = menu();
m->clear();
QAction *sys = m->addAction(tr("Couleur système"));
connect(sys, &QAction::triggered, this, &DiagramBgColorToolButton::applySystemColor);
m->addSeparator();
for (const auto &nc : standardColors())
{
const QColor c = nc.color;
QAction *a = m->addAction(swatchIcon(c),
tr(nc.context_name));
connect(a, &QAction::triggered, this, [this, c]() { applyColor(c); });
}
if (!m_recent.isEmpty())
{
m->addSeparator();
QAction *title = m->addAction(tr("Récemment utilisées"));
title->setEnabled(false);
for (const QColor &c : std::as_const(m_recent))
{
QAction *a = m->addAction(swatchIcon(c), c.name());
connect(a, &QAction::triggered, this, [this, c]() { applyColor(c); });
}
}
m->addSeparator();
QAction *other = m->addAction(tr("Autre couleur…"));
connect(other, &QAction::triggered, this, &DiagramBgColorToolButton::chooseOtherColor);
}
/**
@brief DiagramBgColorToolButton::applyColor
Set a custom background color on all open diagrams.
@param color
*/
void DiagramBgColorToolButton::applyColor(const QColor &color)
{
if (!color.isValid()) {
return;
}
m_is_system_color = false;
rememberRecent(color);
setSwatch(color);
PaletteGraphicsView::setCustomBackgroundColor(true);
Diagram::background_color = color;
QetSettings::setSheetBackground(QetSettings::SheetBackground{color, true});
QETDiagramEditor *editor = m_editor;
if (!editor) {
return;
}
for (ProjectView *pv : editor->openedProjects())
for (Diagram *d : pv->project()->diagrams())
d->update();
}
/**
@brief DiagramBgColorToolButton::applySystemColor
Restore the system-default background and re-enable dark-mode
inversion.
*/
void DiagramBgColorToolButton::applySystemColor()
{
m_is_system_color = true;
m_current = QApplication::palette().color(QPalette::Base);
setSwatch(m_current);
PaletteGraphicsView::setCustomBackgroundColor(false);
Diagram::background_color = Qt::white;
QetSettings::setSheetBackground(
QetSettings::SheetBackground{QColor(Qt::white), false});
QETDiagramEditor *editor = m_editor;
if (!editor) {
return;
}
for (ProjectView *pv : editor->openedProjects())
for (Diagram *d : pv->project()->diagrams())
d->update();
}
/**
@brief DiagramBgColorToolButton::chooseOtherColor
*/
void DiagramBgColorToolButton::chooseOtherColor()
{
const QColor c = QColorDialog::getColor(m_current, this,
tr("Choisir une couleur de fond"));
if (c.isValid()) {
applyColor(c);
}
}
/**
@brief DiagramBgColorToolButton::rememberRecent
Most recent first, no duplicates, capped.
@param color
*/
void DiagramBgColorToolButton::rememberRecent(const QColor &color)
{
for (const auto &nc : standardColors()) {
if (nc.color == color) {
return;
}
}
m_recent.removeAll(color);
m_recent.prepend(color);
while (m_recent.size() > MAX_RECENT) {
m_recent.removeLast();
}
rebuildMenu();
persistRecent();
}
/**
@brief DiagramBgColorToolButton::persistRecent
Write the "recently used" colours back to the settings, so they are
still there on the next start instead of starting out empty.
*/
void DiagramBgColorToolButton::persistRecent()
{
QStringList names;
for (const QColor &c : std::as_const(m_recent)) {
names << c.name();
}
QetSettings::setSheetBackgroundRecentColors(names);
}
/**
@brief DiagramBgColorToolButton::setSwatch
@param color
*/
void DiagramBgColorToolButton::setSwatch(const QColor &color)
{
m_current = color;
setIcon(swatchIcon(color));
}
/**
@brief DiagramBgColorToolButton::syncFromDiagram
Sync the button swatch to whatever Diagram::background_color is
currently set to. Called when the active folio changes.
*/
void DiagramBgColorToolButton::syncFromDiagram()
{
if (m_is_system_color) {
m_current = QApplication::palette().color(QPalette::Base);
} else {
m_current = Diagram::background_color;
}
setSwatch(m_current);
}
/**
@brief DiagramBgColorToolButton::updateEnabledState
*/
void DiagramBgColorToolButton::updateEnabledState()
{
setEnabled(m_editor && m_editor->currentProjectView());
}
/**
@brief DiagramBgColorToolButton::swatchIcon
@param color
@return a plain square of that colour, outlined so that white and very
light colours are still visible against the toolbar.
*/
QIcon DiagramBgColorToolButton::swatchIcon(const QColor &color)
{
QPixmap pix(16, 16);
pix.fill(Qt::transparent);
QPainter p(&pix);
p.setRenderHint(QPainter::Antialiasing, false);
p.setBrush(color);
p.setPen(QPen(QColor(0x40, 0x40, 0x40), 1));
p.drawRect(0, 0, 15, 15);
p.end();
return QIcon(pix);
}
+65
View File
@@ -0,0 +1,65 @@
/*
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 DIAGRAMBGCOLORTOOLBUTTON_H
#define DIAGRAMBGCOLORTOOLBUTTON_H
#include <QColor>
#include <QList>
#include <QToolButton>
class QETDiagramEditor;
/**
@brief The DiagramBgColorToolButton class
Color picker for the diagram sheet background, placed in the
"Affichage" toolbar. Mirrors the ConductorColorToolButton UX:
preset colors in the dropdown, "Autre couleur..." at the bottom,
and a swatch icon on the button itself.
Picking "Couleur système" clears any custom colour and lets the
dark-mode inversion handle the background as before.
*/
class DiagramBgColorToolButton : public QToolButton
{
Q_OBJECT
public:
explicit DiagramBgColorToolButton(QETDiagramEditor *editor,
QWidget *parent = nullptr);
public slots:
void updateEnabledState();
void syncFromDiagram();
private:
void rebuildMenu();
void applyColor(const QColor &color);
void applySystemColor();
void chooseOtherColor();
void rememberRecent(const QColor &color);
void persistRecent();
void setSwatch(const QColor &color);
static QIcon swatchIcon(const QColor &color);
QETDiagramEditor *m_editor = nullptr;
QList<QColor> m_recent;
QColor m_current;
bool m_is_system_color = false;
};
#endif // DIAGRAMBGCOLORTOOLBUTTON_H
+1 -1
View File
@@ -25,7 +25,7 @@ DiagramContextWidget::DiagramContextWidget(QWidget *parent) :
ui(new Ui::DiagramContextWidget)
{
ui->setupUi(this);
connect(ui->m_table, SIGNAL(itemChanged(QTableWidgetItem *)), this, SLOT(checkTableRows()));
connect(ui->m_table, &QTableWidget::itemChanged, this, &DiagramContextWidget::checkTableRows);
}
DiagramContextWidget::~DiagramContextWidget()
+3 -3
View File
@@ -62,7 +62,7 @@ DiagramPropertiesDialog::DiagramPropertiesDialog(Diagram *diagram, QWidget *pare
titleblock_infos = new TitleBlockPropertiesWidget(titleblock, false, diagram->project(), this);
titleblock_infos -> setReadOnly(diagram_is_read_only);
connect(titleblock_infos,SIGNAL(openAutoNumFolioEditor(QString)),this,SLOT(editAutoFolioNum()));
connect(titleblock_infos, &TitleBlockPropertiesWidget::openAutoNumFolioEditor, this, &DiagramPropertiesDialog::editAutoFolioNum);
//titleblock_infos->setMinimumSize(590,480); //Minimum Size needed for correct display
//Conductor widget
@@ -77,8 +77,8 @@ DiagramPropertiesDialog::DiagramPropertiesDialog(Diagram *diagram, QWidget *pare
// Buttons
QDialogButtonBox boutons(diagram_is_read_only ? QDialogButtonBox::Ok : QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
connect(&boutons, SIGNAL(accepted()), this, SLOT(accept()));
connect(&boutons, SIGNAL(rejected()), this, SLOT(reject()));
connect(&boutons, &QDialogButtonBox::accepted, this, &DiagramPropertiesDialog::accept);
connect(&boutons, &QDialogButtonBox::rejected, this, &DiagramPropertiesDialog::reject);
QGridLayout *glayout = new QGridLayout;
glayout->addWidget(border_infos,0,0);
@@ -45,19 +45,15 @@ void DiagramPropertiesEditorDockWidget::setDiagram(Diagram *diagram)
if (m_diagram)
{
disconnect(m_diagram, SIGNAL(selectionChanged()),
this, SLOT(selectionChanged()));
disconnect(m_diagram, SIGNAL(destroyed()),
this, SLOT(diagramWasDeleted()));
disconnect(m_diagram, &Diagram::selectionChanged, this, &DiagramPropertiesEditorDockWidget::selectionChanged);
disconnect(m_diagram, &QObject::destroyed, this, &DiagramPropertiesEditorDockWidget::diagramWasDeleted);
}
if (diagram)
{
m_diagram = diagram;
connect(m_diagram, SIGNAL(selectionChanged()),
this, SLOT(selectionChanged()), Qt::QueuedConnection);
connect(m_diagram, SIGNAL(destroyed()),
this, SLOT(diagramWasDeleted()));
connect(m_diagram, &Diagram::selectionChanged, this, &DiagramPropertiesEditorDockWidget::selectionChanged, Qt::QueuedConnection);
connect(m_diagram, &QObject::destroyed, this, &DiagramPropertiesEditorDockWidget::diagramWasDeleted);
selectionChanged();
}
else
-120
View File
@@ -1,120 +0,0 @@
/*
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 "diagramselection.h"
#include "ui_diagramselection.h"
diagramselection::diagramselection(QETProject *prj, QWidget *parent) :
QWidget(parent),
ui(new Ui::diagramselection),
prj_ (prj),
list_diagram_ (prj -> diagrams())
{
ui -> setupUi(this);
QString project_title = prj_ -> title();
if (project_title.isEmpty()) project_title = tr("Projet sans titre");
ui -> label_prj -> setText( tr("Projet : ") + project_title );
load_TableDiagram();
}
diagramselection::~diagramselection()
{
delete ui;
}
/**
@brief load all Diagrams of project in table
*/
void diagramselection::load_TableDiagram()
{
// Clear all items
ui -> tableDiagram -> clear();
for (int i=ui -> tableDiagram -> rowCount()-1; i >= 0; --i) {
ui -> tableDiagram->removeRow(i);
}
for (int i=ui -> tableDiagram -> columnCount()-1; i>=0; --i) {
ui -> tableDiagram->removeColumn(i);
}
// Set the setting of table
ui -> tableDiagram -> setColumnCount(2);
ui -> tableDiagram -> setSelectionBehavior (QAbstractItemView::SelectRows);
ui -> tableDiagram -> setSelectionMode (QAbstractItemView::SingleSelection);
ui -> tableDiagram -> setEditTriggers (QAbstractItemView::NoEditTriggers);
QStringList titles;
titles.clear();
titles << tr("Sélection") << tr("Nom");
ui-> tableDiagram -> setHorizontalHeaderLabels( titles );
// List Diagrams
for(int i=0,j=0; i<list_diagram_.count(); i++,j++){
QTableWidgetItem *item_Name = new QTableWidgetItem();
QTableWidgetItem *item_State = new QTableWidgetItem();
QString diagram_title = list_diagram_.at(i) -> title();
if (diagram_title.isEmpty())
diagram_title = tr("Folio sans titre");
item_Name -> setData(Qt::DisplayRole, diagram_title);
item_State -> setData(Qt::CheckStateRole, Qt::Checked);
ui -> tableDiagram -> setRowCount(j+1);
ui -> tableDiagram -> setItem(j, 0, item_State);
ui -> tableDiagram -> setItem(j, 1, item_Name);
}
ui -> tableDiagram -> horizontalHeader() -> setStretchLastSection(true);
}
/**
@brief get list of Diagrams is selected
@return this list of Diagrams
*/
QList<Diagram *> diagramselection::list_of_DiagramSelected()
{
QList<Diagram *> listDiag;
for(int i=0; i<ui -> tableDiagram -> rowCount();i++){
if(ui -> tableDiagram -> item(i, 0)->checkState()){
listDiag.push_back( list_diagram_[i] );
}
}
return listDiag;
}
/**
@brief contextMenuRequested
@param pos
*/
void diagramselection::on_tableDiagram_customContextMenuRequested(const QPoint &pos){
QMenu menu(this);
QAction *desl = menu.addAction( tr("Désélectionner tout") );
menu.addAction(QIcon(":/ico/16x16/dialog-ok.png"), tr("Sélectionner tout") );
// Exec Menu
QAction *ret = menu.exec(ui -> tableDiagram -> viewport() -> mapToGlobal(pos));
if (ret == desl) {
for(int i=0; i<ui -> tableDiagram -> rowCount();i++)
ui -> tableDiagram -> item(i, 0)->setCheckState(Qt::Unchecked);
}
else{
for(int i=0; i<ui -> tableDiagram -> rowCount();i++)
ui -> tableDiagram -> item(i, 0)->setCheckState(Qt::Checked);
}
}
-51
View File
@@ -1,51 +0,0 @@
/*
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 DIAGRAMSELECTION_H
#define DIAGRAMSELECTION_H
#include "../diagram.h"
#include "../qetproject.h"
#include <QWidget>
namespace Ui {
class diagramselection;
}
class diagramselection : public QWidget
{
Q_OBJECT
public:
explicit diagramselection(QETProject *prj, QWidget *parent = nullptr);
~diagramselection() override;
QList<Diagram *> list_of_DiagramSelected();
private slots:
void on_tableDiagram_customContextMenuRequested(const QPoint &pos);
private:
Ui::diagramselection *ui;
QETProject *prj_;
QList<Diagram *> list_diagram_;
void load_TableDiagram();
};
#endif // DIAGRAMSELECTION_H
-42
View File
@@ -1,42 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>diagramselection</class>
<widget class="QWidget" name="diagramselection">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label_prj">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<widget class="QTableWidget" name="tableDiagram">
<property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
<property name="frameShape">
<enum>QFrame::StyledPanel</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
+114
View File
@@ -0,0 +1,114 @@
/*
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 "duplicateoffsetdialog.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QSettings>
#include <QSpinBox>
#include <QVBoxLayout>
namespace {
// Stored as one integer per axis, in grid steps -- not pixels, so the
// remembered offset still makes sense if the grid size ever changes.
const QString kOffsetXKey = QStringLiteral("diagrameditor/duplicate_offset_x");
const QString kOffsetYKey = QStringLiteral("diagrameditor/duplicate_offset_y");
}
DuplicateOffsetDialog::DuplicateOffsetDialog(QWidget *parent) :
QDialog(parent)
{
setWindowTitle(tr("Dupliquer"));
auto *form = new QFormLayout;
m_spacing = new QSpinBox(this);
m_spacing->setRange(1, 1000);
m_spacing->setSuffix(tr(" pas de grille"));
form->addRow(tr("Espacement :"), m_spacing);
m_direction = new QComboBox(this);
// Order matches the Direction enum, so currentIndex() can be used
// directly wherever Direction is needed.
m_direction->addItem(tr("Haut"));
m_direction->addItem(tr("Bas"));
m_direction->addItem(tr("Gauche"));
m_direction->addItem(tr("Droite"));
form->addRow(tr("Direction :"), m_direction);
const QPoint saved = savedStepOffset();
// The saved value is a signed (dx, dy) pair, not itself a
// spacing+direction pair, so it has to be decomposed back into the
// two the dialog shows. Exactly one axis is ever non-zero (see
// stepOffset()), so whichever one is picks the direction; a value
// that somehow has neither (only possible if QSettings was hand-
// edited) falls back to the same default stepOffset() would.
int spacing = 1;
Direction direction = Right;
if (saved.x() > 0) { direction = Right; spacing = saved.x(); }
else if (saved.x() < 0) { direction = Left; spacing = -saved.x(); }
else if (saved.y() > 0) { direction = Down; spacing = saved.y(); }
else if (saved.y() < 0) { direction = Up; spacing = -saved.y(); }
m_spacing->setValue(spacing);
m_direction->setCurrentIndex(static_cast<int>(direction));
auto *buttons = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
auto *layout = new QVBoxLayout(this);
layout->addLayout(form);
layout->addWidget(buttons);
}
QPoint DuplicateOffsetDialog::stepOffset() const
{
const int spacing = m_spacing->value();
switch (static_cast<Direction>(m_direction->currentIndex())) {
case Up: return QPoint(0, -spacing);
case Down: return QPoint(0, spacing);
case Left: return QPoint(-spacing, 0);
case Right: return QPoint(spacing, 0);
}
return QPoint(spacing, 0); // unreachable; keeps -Wreturn-type quiet
}
QPoint DuplicateOffsetDialog::savedStepOffset()
{
QSettings settings;
if (!hasSavedStepOffset()) {
return QPoint(1, 0); // default: one grid step to the right
}
return QPoint(settings.value(kOffsetXKey).toInt(),
settings.value(kOffsetYKey).toInt());
}
void DuplicateOffsetDialog::saveStepOffset(const QPoint &steps)
{
QSettings settings;
settings.setValue(kOffsetXKey, steps.x());
settings.setValue(kOffsetYKey, steps.y());
}
bool DuplicateOffsetDialog::hasSavedStepOffset()
{
QSettings settings;
return settings.contains(kOffsetXKey) && settings.contains(kOffsetYKey);
}
+65
View File
@@ -0,0 +1,65 @@
/*
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 DUPLICATEOFFSETDIALOG_H
#define DUPLICATEOFFSETDIALOG_H
#include <QDialog>
#include <QPoint>
class QSpinBox;
class QComboBox;
/**
@brief The DuplicateOffsetDialog class
Asks how far, and in which of the four cardinal directions, Ctrl+D
(DiagramView::duplicate(), bugtracker #991) should offset a copy from
its source. Shown once, then remembered: the answer is stored in
QSettings and reused by every later Ctrl+D press without asking
again, until this dialog is reopened deliberately.
*/
class DuplicateOffsetDialog : public QDialog
{
Q_OBJECT
public:
enum Direction { Up, Down, Left, Right };
explicit DuplicateOffsetDialog(QWidget *parent = nullptr);
/// The offset in grid steps, positive along X to the right
/// and positive along Y downward -- QET's own scene axes,
/// matching the sign convention setPos() already uses
/// everywhere else in this codebase.
QPoint stepOffset() const;
/// Reads the last-confirmed spacing/direction from QSettings,
/// or the default (1 step, right) if none was ever set.
static QPoint savedStepOffset();
/// Writes @p steps to QSettings, in the same X/Y convention
/// as stepOffset().
static void saveStepOffset(const QPoint &steps);
/// Whether a direction/spacing has already been confirmed
/// once, i.e. whether Ctrl+D can skip the dialog.
static bool hasSavedStepOffset();
private:
QSpinBox *m_spacing = nullptr;
QComboBox *m_direction = nullptr;
};
#endif // DUPLICATEOFFSETDIALOG_H
+5 -5
View File
@@ -25,7 +25,7 @@
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="document-save" resource="../../qelectrotech.qrc">
<normaloff>:/ico/16x16/document-save.png</normaloff>:/ico/16x16/document-save.png</iconset>
</property>
</widget>
@@ -39,7 +39,7 @@
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="folder-open" resource="../../qelectrotech.qrc">
<normaloff>:/ico/16x16/folder-open.png</normaloff>:/ico/16x16/folder-open.png</iconset>
</property>
</widget>
@@ -66,7 +66,7 @@
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="textfield" resource="../../qelectrotech.qrc">
<normaloff>:/ico/22x22/textfield.png</normaloff>:/ico/22x22/textfield.png</iconset>
</property>
</widget>
@@ -80,7 +80,7 @@
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="object-group" resource="../../qelectrotech.qrc">
<normaloff>:/ico/16x16/object-group.png</normaloff>:/ico/16x16/object-group.png</iconset>
</property>
</widget>
@@ -94,7 +94,7 @@
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="list-remove" resource="../../qelectrotech.qrc">
<normaloff>:/ico/16x16/list-remove.png</normaloff>:/ico/16x16/list-remove.png</iconset>
</property>
</widget>
+61 -19
View File
@@ -36,6 +36,7 @@
#include <QHash>
#include <QModelIndex>
#include <QStandardItem>
#include <QTimer>
#include <QUndoCommand>
static int src_txt_row = 0;
@@ -130,7 +131,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());
@@ -250,8 +251,10 @@ QList<QStandardItem *> DynamicElementTextModel::itemsForText(
QStandardItem *color = new QStandardItem(tr("Couleur"));
color->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
// Shown as a swatch next to the value, not as the text color: black
// text (the default) is unreadable on a dark palette.
QStandardItem *colora = new QStandardItem;
colora->setData(deti->color(), Qt::ForegroundRole);
colora->setData(deti->color(), Qt::DecorationRole);
colora->setData(deti->color(), Qt::EditRole);
colora->setData(DynamicElementTextModel::color, Qt::UserRole+1);
colora->setFlags(Qt::ItemIsSelectable
@@ -718,7 +721,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 +872,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 +899,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 +964,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 +1374,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 +1393,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 +1415,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 +1432,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))
@@ -1499,7 +1502,7 @@ void DynamicElementTextModel::updateDataFromText(DynamicElementTextItem *deti,
case color:
{
qsi->child(color_txt_row,1)->setData(deti->color(), Qt::EditRole);
qsi->child(color_txt_row,1)->setData(deti->color(), Qt::ForegroundRole);
qsi->child(color_txt_row,1)->setData(deti->color(), Qt::DecorationRole);
break;
}
case pos:
@@ -1595,6 +1598,15 @@ DynamicTextItemDelegate::DynamicTextItemDelegate(QObject *parent) :
QStyledItemDelegate(parent)
{}
void DynamicTextItemDelegate::commitAndCloseDeferred(QWidget *editor) const
{
auto *self = const_cast<DynamicTextItemDelegate *>(this);
QTimer::singleShot(0, self, [self, editor]() {
emit self->commitData(editor);
emit self->closeEditor(editor);
});
}
QWidget *DynamicTextItemDelegate::createEditor(
QWidget *parent,
const QStyleOptionViewItem &option,
@@ -1682,13 +1694,43 @@ QWidget *DynamicTextItemDelegate::createEditor(
w->setProperty("ok", ok);
}
w->setObjectName("font_dialog");
commitAndCloseDeferred(w);
return w;
}
case DynamicElementTextModel::color:
{
QColorDialog *cd = new QColorDialog(index.data(Qt::EditRole).value<QColor>(), parent);
cd->setObjectName("color_dialog");
return cd;
/* Like the font case above: run the dialog synchronously via
* its static convenience function and stash the result on a
* plain placeholder widget, rather than handing back the
* QColorDialog itself as the item view's "editor".
*
* The item view's own Enter/Escape handling (the base
* QStyledItemDelegate::eventFilter(), since "color_dialog"
* isn't one of the objectNames special-cased in this
* delegate's own eventFilter() above) treats whatever
* createEditor() returned as a small inline editor it
* commits and destroys directly on a key press. A QColorDialog
* is not that: on Windows it can hand off to the native
* color picker, whose own accept/close path then races the
* view's -- pressing Enter fired both, one tearing down an
* object the other was still using (case #323 on the bug
* tracker: "QObject::installEventFilter(): Cannot filter
* events for objects in a different thread" immediately
* followed by a segfault; clicking the dialog's own OK
* button with the mouse didn't reach the view's key
* handling, so it didn't crash). Resolving the dialog
* before returning removes the second, competing teardown
* path entirely. */
QColor color = QColorDialog::getColor(index.data(Qt::EditRole).value<QColor>(), parent);
QWidget *w = new QWidget(parent);
if (color.isValid())
{
w->setProperty("color", color);
w->setProperty("ok", true);
}
w->setObjectName("color_dialog");
commitAndCloseDeferred(w);
return w;
}
case DynamicElementTextModel::pos:
{
@@ -1788,15 +1830,15 @@ void DynamicTextItemDelegate::setModelData(
{
if(QStandardItem *qsi = qsim->itemFromIndex(index))
{
QColorDialog *cd = static_cast<QColorDialog *> (editor);
if (cd->result() == QDialog::Accepted)
if (editor->property("ok").toBool() == true)
{
qsi->setData(cd->selectedColor(), Qt::EditRole);
qsi->setData(cd->selectedColor(), Qt::ForegroundRole);
QColor color = editor->property("color").value<QColor>();
qsi->setData(color, Qt::EditRole);
qsi->setData(color, Qt::DecorationRole);
}
return;
}
}
}
else if (editor->objectName() == "info_text")
+15
View File
@@ -155,6 +155,21 @@ class DynamicTextItemDelegate : public QStyledItemDelegate
private:
QStringList availableInfo(DynamicElementTextItem *deti) const;
/**
@brief commitAndCloseDeferred
Schedule commitData()/closeEditor() for @a editor on the next
event loop iteration. For editors resolved synchronously inside
createEditor() (font/color, both run their picker dialog before
returning) there is no user interaction left to drive the base
QStyledItemDelegate::eventFilter()'s usual Enter/focus-out commit
path, so without this the value sits picked-but-uncommitted
until something unrelated (e.g. clicking elsewhere) happens to
trigger it. Deferred rather than called immediately: the view
only registers the widget createEditor() returns as "the active
editor" *after* createEditor() itself returns, so emitting here
would target an editor the view doesn't know about yet.
*/
void commitAndCloseDeferred(QWidget *editor) const;
};
#endif // DYNAMICELEMENTTEXTMODEL_H
+45
View File
@@ -18,7 +18,11 @@
#include "elementinfopartwidget.h"
#include "../SearchAndReplace/searchandreplaceworker.h"
#include "../qetinformation.h"
#include "ui_elementinfopartwidget.h"
#include <QCompleter>
#include <QRegularExpressionValidator>
#include <QStringListModel>
#include <utility>
@@ -43,6 +47,14 @@ ElementInfoPartWidget::ElementInfoPartWidget(
ui->label_->setText(translated_key);
ui->m_erase_text->setVisible(false);
if (key_ == QETInformation::ELMT_WIDTH || key_ == QETInformation::ELMT_HEIGHT || key_ == QETInformation::ELMT_DEPTH)
{
auto *validator = new QETInformation::NumericInfoValidator(ui->line_edit);
ui->line_edit->setValidator(validator);
ui->line_edit->setPlaceholderText(tr("ex. 80.5"));
ui->line_edit->setToolTip(tr("Nombre décimal avec un point comme séparateur (ex. 80.5)"));
}
connect(ui->line_edit, &QLineEdit::textEdited,
this, &ElementInfoPartWidget::textEdited);
connect(ui->line_edit, &QLineEdit::textChanged,
@@ -67,6 +79,15 @@ QString ElementInfoPartWidget::text() const
return (ui->line_edit->text());
}
/**
@brief ElementInfoPartWidget::hasAcceptableInput
@return whether the line edit's current text satisfies its validator
*/
bool ElementInfoPartWidget::hasAcceptableInput() const
{
return ui->line_edit->hasAcceptableInput();
}
/**
@brief ElementInfoPartWidget::setText
Set text to line edit
@@ -90,6 +111,30 @@ void ElementInfoPartWidget::setPlaceHolderText(const QString &text)
ui->line_edit->setPlaceholderText(text);
}
/**
@brief ElementInfoPartWidget::setSuggestions
Offer suggestions as a drop-down list while typing in the line edit,
matching anywhere in the text and ignoring case.
An empty list removes the drop-down.
@param suggestions
*/
void ElementInfoPartWidget::setSuggestions(const QStringList &suggestions)
{
if (suggestions.isEmpty()) {
ui->line_edit->setCompleter(nullptr);
return;
}
if (!m_completer) {
m_suggestions_model = new QStringListModel(this);
m_completer = new QCompleter(m_suggestions_model, this);
m_completer->setCaseSensitivity(Qt::CaseInsensitive);
m_completer->setFilterMode(Qt::MatchContains);
}
m_suggestions_model->setStringList(suggestions);
ui->line_edit->setCompleter(m_completer);
}
/**
@brief ElementInfoPartWidget::setFocusTolineEdit
Set the focus to the line edit
+8 -2
View File
@@ -20,6 +20,9 @@
#include <QWidget>
class QCompleter;
class QStringListModel;
namespace Ui {
class ElementInfoPartWidget;
}
@@ -41,11 +44,12 @@ class ElementInfoPartWidget : public QWidget
QWidget *parent = nullptr);
~ElementInfoPartWidget() override;
QString key () const
{return key_;}
QString key () const {return key_;}
QString text () const;
bool hasAcceptableInput() const;
void setText (const QString &);
void setPlaceHolderText (const QString &text);
void setSuggestions (const QStringList &suggestions);
void setFocusTolineEdit();
void setEnabled(bool e);
void setDisabled(bool d);
@@ -65,6 +69,8 @@ class ElementInfoPartWidget : public QWidget
Ui::ElementInfoPartWidget *ui;
QString key_;
bool m_show_erase = false;
QCompleter *m_completer = nullptr;
QStringListModel *m_suggestions_model = nullptr;
};
#endif // ELEMENTINFOPARTWIDGET_H
+165 -6
View File
@@ -16,13 +16,18 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "elementinfowidget.h"
#include "../qet.h"
#include <QCheckBox>
#include <QPushButton>
#include "../diagram.h"
#include "../qetapp.h"
#include "../qetgraphicsitem/element.h"
#include "../dataBase/projectdatabase.h"
#include "../qetinformation.h"
#include "../qetproject.h"
#include "../ui_elementinfowidget.h"
#include "../undocommand/changeelementinformationcommand.h"
#include "customelementinfopartwidget.h"
#include "elementinfopartwidget.h"
/**
@@ -47,6 +52,7 @@ ElementInfoWidget::ElementInfoWidget(Element *elmt, QWidget *parent) :
ElementInfoWidget::~ElementInfoWidget()
{
qDeleteAll(m_eipw_list);
qDeleteAll(m_custom_eipw_list);
delete ui;
}
@@ -145,7 +151,7 @@ bool ElementInfoWidget::event(QEvent *event)
{
if (event -> type() == QEvent::WindowActivate || event -> type() == QEvent::Show)
{
QTimer::singleShot(250, this, SLOT(firstActivated()));
QTimer::singleShot(250, this, &ElementInfoWidget::firstActivated);
m_first_activation = false;
}
}
@@ -201,12 +207,25 @@ void ElementInfoWidget::buildInterface()
keys = QETInformation::elementInfoKeys();
}
//"exclude_from_bom" is part of elementInfoKeys() because the project
//database builds the element_info table from that list, but it is not
//a free-text property: it already has its own check box below. Without
//this it also gets a generic edit row, and since translatedInfoKey()
//has no entry for it that row carries no label at all - an anonymous
//line that currentInfo() then fills with "true"/"false".
keys.removeAll(QStringLiteral("exclude_from_bom"));
for (auto str : keys)
{
ElementInfoPartWidget *eipw = new ElementInfoPartWidget(str, QETInformation::translatedInfoKey(str), this);
ui->scroll_vlayout->addWidget(eipw);
m_eipw_list << eipw;
}
m_add_custom_property_btn = new QPushButton(tr("Ajouter une propriété personnalisée"), this);
connect(m_add_custom_property_btn, &QPushButton::clicked, this, [this]() { addCustomProperty(); });
ui->scroll_vlayout->addWidget(m_add_custom_property_btn);
ui->scroll_vlayout->addStretch();
// Existing potential isolating checkbox
@@ -235,6 +254,67 @@ void ElementInfoWidget::buildInterface()
m_potential_isolating_cb->setVisible(false);
}
}
/**
@brief ElementInfoWidget::predefinedKeys
@return every key this widget already exposes a dedicated row for,
whether through ElementInfoPartWidget (the ~40 ELMT_* keys) or one
of the standalone checkboxes. Anything present in the element's
informations but absent from this list is a user-defined custom
property.
*/
QStringList ElementInfoWidget::predefinedKeys() const
{
QStringList keys = (m_element.data()->elementData().m_type == ElementData::Terminal)
? QETInformation::terminalElementInfoKeys()
: QETInformation::elementInfoKeys();
keys << QStringLiteral("auto_num_locked")
<< QStringLiteral("potential_isolating")
<< QStringLiteral("exclude_from_bom");
return keys;
}
/**
@brief ElementInfoWidget::addCustomProperty
Append a new user-defined key/value row to the widget.
@param key initial key, left empty for a freshly added row
@param value initial value
*/
void ElementInfoWidget::addCustomProperty(const QString &key, const QString &value)
{
auto *widget = new CustomElementInfoPartWidget(key, value, this);
const int insert_index = ui->scroll_vlayout->indexOf(m_add_custom_property_btn);
ui->scroll_vlayout->insertWidget(insert_index >= 0 ? insert_index : ui->scroll_vlayout->count(), widget);
m_custom_eipw_list << widget;
connect(widget, &CustomElementInfoPartWidget::removeRequested, this, &ElementInfoWidget::removeCustomProperty);
connect(widget, &CustomElementInfoPartWidget::changed, this, [this]() {
if (m_live_edit) apply();
});
if (key.isEmpty()) {
widget->setFocus();
}
}
/**
@brief ElementInfoWidget::removeCustomProperty
Remove a user-defined key/value row.
@param widget the row to remove
*/
void ElementInfoWidget::removeCustomProperty(CustomElementInfoPartWidget *widget)
{
if (!m_custom_eipw_list.removeOne(widget))
return;
ui->scroll_vlayout->removeWidget(widget);
widget->deleteLater();
if (m_live_edit) apply();
}
/**
@brief ElementInfoWidget::infoPartWidgetForKey
@param key
@@ -243,7 +323,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;
@@ -252,6 +332,55 @@ ElementInfoPartWidget *ElementInfoWidget::infoPartWidgetForKey(const QString &ke
return nullptr;
}
/**
@brief ElementInfoWidget::updateSuggestions
Offer, for each information, the values already used by the other
elements of the project (supplier, manufacturer...), so they can be
picked instead of typed again.
The values come from the project database rather than from the
diagrams, which already holds them in the element_info table.
*/
void ElementInfoWidget::updateSuggestions()
{
Diagram *diagram = m_element ? m_element->diagram() : nullptr;
QETProject *project = diagram ? diagram->project() : nullptr;
if (!project || !project->dataBase()) {
return;
}
//Only a column of element_info can be queried. The key is checked
//against that list rather than trusted, because it becomes part of
//the SQL text.
const QStringList columns = QETInformation::elementInfoKeys();
const QString uuid = m_element->uuid().toString();
for (ElementInfoPartWidget *eipw : m_eipw_list)
{
const QString key = eipw->key();
//A label identifies one element, suggesting the others is noise
if (key == QETInformation::ELMT_LABEL || !columns.contains(key)) {
continue;
}
//"Schneider" and "schneider" are offered once, spelled the way
//most elements spell it: SQLite takes the bare column v from
//the row that holds MAX(n).
QStringList values;
auto query = project->dataBase()->newQuery(QStringLiteral(
"SELECT v, MAX(n) FROM ("
"SELECT \"%1\" AS v, COUNT(*) AS n FROM element_info "
"WHERE \"%1\" IS NOT NULL AND \"%1\" != '' "
"AND element_uuid != '%2' "
"GROUP BY \"%1\") "
"GROUP BY v COLLATE NOCASE "
"ORDER BY v COLLATE NOCASE").arg(key, uuid));
while (query.next()) {
values << query.value(0).toString();
}
eipw->setSuggestions(values);
}
}
/**
@brief ElementInfoWidget::updateUi
fill information fetch in m_element_info to the
@@ -271,21 +400,37 @@ void ElementInfoWidget::updateUi()
for (ElementInfoPartWidget *eipw : m_eipw_list) {
eipw -> setText (element_info[eipw->key()].toString());
}
updateSuggestions();
// Rebuild the custom-property rows to match whatever
// user-defined keys this element currently carries.
while (!m_custom_eipw_list.isEmpty()) {
CustomElementInfoPartWidget *w = m_custom_eipw_list.takeLast();
ui->scroll_vlayout->removeWidget(w);
delete w;
}
const auto known_keys = predefinedKeys();
for (const QString &key : element_info.keys()) {
if (!known_keys.contains(key)) {
addCustomProperty(key, element_info[key].toString());
}
}
// Load the lock status for auto numbering
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"));
ui->m_auto_num_locked_cb->setChecked(QET::infoFlagIsTrue(lock_value));
// 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"));
m_potential_isolating_cb->setChecked(QET::infoFlagIsTrue(isolating_value));
}
}
// 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"));
m_exclude_from_bom_cb->setChecked(QET::infoFlagIsTrue(exclude_bom_value));
}
if (m_live_edit) {
@@ -301,8 +446,11 @@ DiagramContext ElementInfoWidget::currentInfo() const
{
DiagramContext info_;
for (const auto &eipw : qAsConst(m_eipw_list))
for (const auto &eipw : std::as_const(m_eipw_list))
{
if (!eipw->hasAcceptableInput())
continue;
//add value only if they're something to store
if (!eipw->text().isEmpty())
{
@@ -314,6 +462,17 @@ DiagramContext ElementInfoWidget::currentInfo() const
}
}
for (const auto &custom : std::as_const(m_custom_eipw_list))
{
if (custom->hasValidKey() && !custom->value().isEmpty())
{
QString txt{custom->value()};
txt.remove(QStringLiteral("\r"));
txt.remove(QStringLiteral("\n"));
info_.addValue(custom->key(), txt);
}
}
// 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"));
+8
View File
@@ -26,8 +26,10 @@
class Element;
class QUndoCommand;
class ElementInfoPartWidget;
class CustomElementInfoPartWidget;
class ChangeElementInformationCommand;
class QCheckBox;
class QPushButton;
namespace Ui {
class ElementInfoWidget;
@@ -63,15 +65,21 @@ class ElementInfoWidget : public AbstractElementPropertiesEditorWidget
private:
void buildInterface();
ElementInfoPartWidget *infoPartWidgetForKey(const QString &key) const;
QStringList predefinedKeys() const;
void updateSuggestions();
private slots:
void firstActivated();
void elementInfoChange();
void addCustomProperty(const QString &key = QString(), const QString &value = QString());
void removeCustomProperty(CustomElementInfoPartWidget *widget);
//ATTRIBUTES
private:
Ui::ElementInfoWidget *ui;
QList <ElementInfoPartWidget *> m_eipw_list;
QList <CustomElementInfoPartWidget *> m_custom_eipw_list;
QPushButton *m_add_custom_property_btn = nullptr;
QCheckBox *m_potential_isolating_cb = nullptr;
QCheckBox *m_exclude_from_bom_cb = nullptr;
bool m_first_activation;
+63 -6
View File
@@ -23,12 +23,15 @@
#include "../qetgraphicsitem/dynamicelementtextitem.h"
#include "../qetgraphicsitem/element.h"
#include "../qetgraphicsitem/elementtextitemgroup.h"
#include "../qetgraphicsitem/masterelement.h"
#include "../qeticons.h"
#include "dynamicelementtextitemeditor.h"
#include "elementinfowidget.h"
#include "linksingleelementwidget.h"
#include "masterpropertieswidget.h"
#include "plclinkwidget.h"
#include <QCheckBox>
#include <QLabel>
#include <QUndoStack>
#include <QVBoxLayout>
@@ -303,7 +306,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);
@@ -375,9 +381,48 @@ QWidget *ElementPropertiesWidget::generalWidget()
description_string += QString(tr("Rotation : %1°\n")).arg(m_element.data()->rotation());
description_string += QString(tr("Dimensions : %1*%2\n")).arg(m_element -> size().width()).arg(m_element -> size().height());
description_string += QString(tr("Bornes : %1\n")).arg(m_element -> terminals().count());
if (m_element->linkType() == Element::Master){
description_string += QString(tr("Nombre maximum de contacts esclaves définis : %1\n")).arg(m_element -> elementData().m_max_slaves);
description_string += QString(tr("Nombre de contacts esclaves utilisés : %1\n")).arg(m_element ->linkedElements().count());
if (m_element->linkType() == Element::Master)
{
//The declared limit is optional: -1 means the element sets no
//limit at all, which is worth saying rather than printing "-1".
const int max_slaves = m_element->elementData().m_max_slaves;
description_string += max_slaves == -1
? QString(tr("Nombre maximum de contacts esclaves définis : non défini\n"))
: QString(tr("Nombre maximum de contacts esclaves définis : %1\n")).arg(max_slaves);
//Left as a count of linked elements: the line above is a number
//of slots, and a slave fills one slot however many contacts it
//carries, so the two stay in the same unit.
description_string += QString(tr("Nombre de contacts esclaves utilisés : %1\n")).arg(m_element->linkedElements().count());
//The breakdown below is in contacts, not slots: it answers how
//many contacts an auxiliary block must provide.
const MasterElement *master =
static_cast<const MasterElement *>(m_element.data());
const ContactUsage usage = master->contactUsage();
const ContactUsage capacity = master->contactCapacity();
if (capacity.total() > 0)
{
//The element declares contact groups, so it can say not
//only what has been used but what it has to offer. A type
//used beyond what is declared shows as e.g. "1/0", which
//is the point: it says this contact does not fit the part.
description_string += QString(tr(" Contacts : NO : %1/%2, NC : %3/%4, inverseurs : %5/%6, autres : %7/%8\n"))
.arg(usage.no).arg(capacity.no)
.arg(usage.nc).arg(capacity.nc)
.arg(usage.sw).arg(capacity.sw)
.arg(usage.other).arg(capacity.other);
}
else if (usage.total() > 0)
{
//No declared groups, so a plain count of what is in use.
description_string += QString(tr(" Contacts : NO : %1, NC : %2, inverseurs : %3, autres : %4\n"))
.arg(usage.no)
.arg(usage.nc)
.arg(usage.sw)
.arg(usage.other);
}
}
description_string += QString(tr("Emplacement : %1\n")).arg(m_element.data()->location().toString());
@@ -392,6 +437,18 @@ QWidget *ElementPropertiesWidget::generalWidget()
label->setTextInteractionFlags(Qt::TextEditorInteraction);
vlayout_->addWidget(label);
//checkbox to lock the element position on the diagram
//(same mechanism already used by images and drawn shapes)
QCheckBox *lock_pos_cb = new QCheckBox(tr("Verrouiller la position"), general_widget);
lock_pos_cb->setChecked(!m_element->isMovable());
QPointer<Element> element = m_element;
connect(lock_pos_cb, &QCheckBox::clicked, this, [element](bool checked) {
if (element) {
element->setMovable(!checked);
}
});
vlayout_->addWidget(lock_pos_cb);
//widget for the pixmap
QLabel *pix = new QLabel(general_widget);
vlayout_->addWidget(pix, 0, Qt::AlignHCenter);
@@ -399,9 +456,9 @@ QWidget *ElementPropertiesWidget::generalWidget()
//button widget
QPushButton *find_in_panel = new QPushButton(QET::Icons::ZoomDraw, tr("Retrouver dans le panel"), general_widget);
connect(find_in_panel, SIGNAL(clicked()), this, SLOT(findInPanel()));
connect(find_in_panel, &QPushButton::clicked, this, &ElementPropertiesWidget::findInPanel);
QPushButton *edit_element = new QPushButton(QET::Icons::ElementEdit, tr("Éditer l'élément"), general_widget);
connect(edit_element, SIGNAL(clicked()), this, SLOT(editElement()));
connect(edit_element, &QPushButton::clicked, this, &ElementPropertiesWidget::editElement);
QHBoxLayout *hlayout_ = new QHBoxLayout;
hlayout_->addWidget(find_in_panel);
hlayout_->addWidget(edit_element);
+287
View File
@@ -0,0 +1,287 @@
/*
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 "imagecropdialog.h"
#include <QDialogButtonBox>
#include <QLabel>
#include <QMouseEvent>
#include <QPainter>
#include <QPushButton>
#include <QVBoxLayout>
namespace {
// Fits the dialog comfortably on a normal screen regardless of the
// source image's own resolution -- same reasoning, and same value,
// as ImageTransparentColorDialog's identical constant.
constexpr int MAX_DISPLAY_SIZE = 350;
}
/**
@brief CropAreaWidget::CropAreaWidget
@param sourceImage the full-resolution image to crop
@param initialCropRect a previously-chosen crop rect (in
sourceImage's own coordinates), or an empty QRect to start covering
the whole image
@param parent
*/
CropAreaWidget::CropAreaWidget(const QImage &sourceImage, const QRect &initialCropRect, QWidget *parent) :
QWidget(parent),
m_source(sourceImage)
{
const qreal scaleW = qreal(MAX_DISPLAY_SIZE) / m_source.width();
const qreal scaleH = qreal(MAX_DISPLAY_SIZE) / m_source.height();
m_displayScale = qMin(qreal(1.0), qMin(scaleW, scaleH)); // never upscale a small image, only ever shrink a large one
const QSize displaySize = m_source.size() * m_displayScale;
m_displayImageRect = QRect(QPoint(0, 0), displaySize);
setFixedSize(displaySize);
if (initialCropRect.isEmpty())
{
// No previous crop to start from -- covers the whole image, so
// the user shrinks it from there rather than having to first
// drag out a rectangle from nothing.
m_cropRect = m_displayImageRect;
}
else
{
// Converting an existing, original-space crop rect back into
// display-space -- the reverse of what cropRect() does going
// the other way.
m_cropRect = QRect(
qRound(initialCropRect.left() * m_displayScale),
qRound(initialCropRect.top() * m_displayScale),
qRound(initialCropRect.width() * m_displayScale),
qRound(initialCropRect.height() * m_displayScale))
.intersected(m_displayImageRect);
}
}
/**
@brief CropAreaWidget::resetToFullImage
*/
void CropAreaWidget::resetToFullImage()
{
m_cropRect = m_displayImageRect;
update();
emit cropRectChanged();
}
/**
@brief CropAreaWidget::cropRect
@return the crop rectangle in the original, full-resolution image's
own coordinates -- m_cropRect itself is display-space only.
*/
QRect CropAreaWidget::cropRect() const
{
QRect r(
qRound(m_cropRect.left() / m_displayScale),
qRound(m_cropRect.top() / m_displayScale),
qRound(m_cropRect.width() / m_displayScale),
qRound(m_cropRect.height() / m_displayScale));
return r.intersected(m_source.rect());
}
/**
@brief CropAreaWidget::handleAt
@param pos widget-space position to test
@return whichever handle (or the rect's own interior, for moving)
pos is close enough to, or DragMode::None if it's not near anything
*/
CropAreaWidget::DragMode CropAreaWidget::handleAt(const QPoint &pos) const
{
const int tol = HANDLE_SIZE; // a generous hit area, larger than the drawn handle itself, since a hit-test exactly matching a small handle's visible size is needlessly hard to land
auto near = [&](const QPoint &p) { return (pos - p).manhattanLength() <= tol; };
if (near(m_cropRect.topLeft())) return DragMode::ResizeNW;
if (near(m_cropRect.topRight())) return DragMode::ResizeNE;
if (near(m_cropRect.bottomLeft())) return DragMode::ResizeSW;
if (near(m_cropRect.bottomRight())) return DragMode::ResizeSE;
if (near(QPoint(m_cropRect.center().x(), m_cropRect.top()))) return DragMode::ResizeN;
if (near(QPoint(m_cropRect.center().x(), m_cropRect.bottom()))) return DragMode::ResizeS;
if (near(QPoint(m_cropRect.left(), m_cropRect.center().y()))) return DragMode::ResizeW;
if (near(QPoint(m_cropRect.right(), m_cropRect.center().y()))) return DragMode::ResizeE;
if (m_cropRect.contains(pos))
return DragMode::Move;
return DragMode::None;
}
/**
@brief CropAreaWidget::applyDrag
Recomputes m_cropRect from m_dragStartRect plus however far pos has
moved from m_dragStartPos, according to m_dragMode -- always
clamped to stay within the displayed image and never smaller than
MIN_CROP_SIZE, so the rect can never escape its bounds or collapse
to nothing while being dragged.
@param pos current widget-space mouse position
*/
void CropAreaWidget::applyDrag(const QPoint &pos)
{
const QPoint delta = pos - m_dragStartPos;
QRect r = m_dragStartRect;
auto clampLeft = [&](int v) { r.setLeft(qBound(m_displayImageRect.left(), v, r.right() - MIN_CROP_SIZE)); };
auto clampRight = [&](int v) { r.setRight(qBound(r.left() + MIN_CROP_SIZE, v, m_displayImageRect.right())); };
auto clampTop = [&](int v) { r.setTop(qBound(m_displayImageRect.top(), v, r.bottom() - MIN_CROP_SIZE)); };
auto clampBottom = [&](int v) { r.setBottom(qBound(r.top() + MIN_CROP_SIZE, v, m_displayImageRect.bottom())); };
switch (m_dragMode)
{
case DragMode::Move:
{
// Clamping the translation itself, rather than each edge of
// the resulting rect independently -- clamping edges one at
// a time would shrink or distort the rect the instant it
// hit a boundary, instead of just stopping its movement.
QRect moved = m_dragStartRect.translated(delta);
if (moved.left() < m_displayImageRect.left())
moved.translate(m_displayImageRect.left() - moved.left(), 0);
if (moved.right() > m_displayImageRect.right())
moved.translate(m_displayImageRect.right() - moved.right(), 0);
if (moved.top() < m_displayImageRect.top())
moved.translate(0, m_displayImageRect.top() - moved.top());
if (moved.bottom() > m_displayImageRect.bottom())
moved.translate(0, m_displayImageRect.bottom() - moved.bottom());
r = moved;
break;
}
case DragMode::ResizeN: clampTop(m_dragStartRect.top() + delta.y()); break;
case DragMode::ResizeS: clampBottom(m_dragStartRect.bottom() + delta.y()); break;
case DragMode::ResizeE: clampRight(m_dragStartRect.right() + delta.x()); break;
case DragMode::ResizeW: clampLeft(m_dragStartRect.left() + delta.x()); break;
case DragMode::ResizeNE: clampTop(m_dragStartRect.top() + delta.y()); clampRight(m_dragStartRect.right() + delta.x()); break;
case DragMode::ResizeNW: clampTop(m_dragStartRect.top() + delta.y()); clampLeft(m_dragStartRect.left() + delta.x()); break;
case DragMode::ResizeSE: clampBottom(m_dragStartRect.bottom() + delta.y()); clampRight(m_dragStartRect.right() + delta.x()); break;
case DragMode::ResizeSW: clampBottom(m_dragStartRect.bottom() + delta.y()); clampLeft(m_dragStartRect.left() + delta.x()); break;
case DragMode::None: return;
}
m_cropRect = r;
}
void CropAreaWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() != Qt::LeftButton)
return;
m_dragMode = handleAt(event->pos());
m_dragStartPos = event->pos();
m_dragStartRect = m_cropRect;
}
void CropAreaWidget::mouseMoveEvent(QMouseEvent *event)
{
if (m_dragMode == DragMode::None)
return;
applyDrag(event->pos());
update();
emit cropRectChanged();
}
void CropAreaWidget::mouseReleaseEvent(QMouseEvent *event)
{
Q_UNUSED(event);
m_dragMode = DragMode::None;
}
/**
@brief CropAreaWidget::paintEvent
Draws the image, dims everything outside the crop rect (four
strips around it, rather than one overlay with a hole cut out of
it, since that would need a more involved clip/composition setup
for no real benefit here), then the rect's own dashed border and
its eight handles.
*/
void CropAreaWidget::paintEvent(QPaintEvent *)
{
QPainter painter(this);
painter.drawImage(m_displayImageRect, m_source);
const QColor dim(0, 0, 0, 140);
painter.fillRect(QRect(m_displayImageRect.left(), m_displayImageRect.top(),
m_displayImageRect.width(), m_cropRect.top() - m_displayImageRect.top()), dim);
painter.fillRect(QRect(m_displayImageRect.left(), m_cropRect.bottom() + 1,
m_displayImageRect.width(), m_displayImageRect.bottom() - m_cropRect.bottom()), dim);
painter.fillRect(QRect(m_displayImageRect.left(), m_cropRect.top(),
m_cropRect.left() - m_displayImageRect.left(), m_cropRect.height()), dim);
painter.fillRect(QRect(m_cropRect.right() + 1, m_cropRect.top(),
m_displayImageRect.right() - m_cropRect.right(), m_cropRect.height()), dim);
QPen borderPen(Qt::white);
borderPen.setStyle(Qt::DashLine);
painter.setPen(borderPen);
painter.setBrush(Qt::NoBrush);
painter.drawRect(m_cropRect);
painter.setPen(Qt::black);
painter.setBrush(Qt::white);
auto drawHandle = [&](const QPoint &center) {
painter.drawRect(QRect(center.x() - HANDLE_SIZE / 2, center.y() - HANDLE_SIZE / 2, HANDLE_SIZE, HANDLE_SIZE));
};
drawHandle(m_cropRect.topLeft());
drawHandle(m_cropRect.topRight());
drawHandle(m_cropRect.bottomLeft());
drawHandle(m_cropRect.bottomRight());
drawHandle(QPoint(m_cropRect.center().x(), m_cropRect.top()));
drawHandle(QPoint(m_cropRect.center().x(), m_cropRect.bottom()));
drawHandle(QPoint(m_cropRect.left(), m_cropRect.center().y()));
drawHandle(QPoint(m_cropRect.right(), m_cropRect.center().y()));
}
/**
@brief ImageCropDialog::ImageCropDialog
@param pixmap the image to crop
@param existingCropRect a crop rectangle remembered from a previous
session, or an empty QRect if there isn't one yet
@param parent
*/
ImageCropDialog::ImageCropDialog(const QPixmap &pixmap, const QRect &existingCropRect, QWidget *parent) :
QDialog(parent)
{
setWindowTitle(tr("Rogner l'image"));
m_cropArea = new CropAreaWidget(pixmap.toImage(), existingCropRect, this);
auto *hint = new QLabel(tr("Faites glisser les poignées, ou l'intérieur du cadre, pour ajuster la zone à conserver."), this);
hint->setWordWrap(true);
auto *resetButton = new QPushButton(tr("Réinitialiser"), this);
resetButton->setToolTip(tr("Revenir à l'image complète, sans rognage"));
connect(resetButton, &QPushButton::clicked, m_cropArea, &CropAreaWidget::resetToFullImage);
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
buttons->addButton(resetButton, QDialogButtonBox::ResetRole);
auto *layout = new QVBoxLayout(this);
layout->addWidget(hint);
layout->addWidget(m_cropArea, 0, Qt::AlignHCenter);
layout->addWidget(buttons);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
}
/**
@brief ImageCropDialog::cropRect
@return the chosen crop rectangle, in the original pixmap's own coordinates
*/
QRect ImageCropDialog::cropRect() const
{
return m_cropArea->cropRect();
}
+121
View File
@@ -0,0 +1,121 @@
/*
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 IMAGE_CROP_DIALOG_H
#define IMAGE_CROP_DIALOG_H
#include <QDialog>
#include <QImage>
#include <QRect>
#include <QWidget>
class QMouseEvent;
class QPaintEvent;
class QPushButton;
/**
@brief The CropAreaWidget class
Displays the image scaled to fit a reasonable dialog size, with a
draggable, resizable crop rectangle overlaid on top -- the area
outside it dimmed, the usual visual convention for "this part goes
away" during a crop, so it's never ambiguous which region survives.
All internal geometry (the crop rect, drag handling, hit-testing)
works in display-space pixel coordinates; only cropRect() converts
back to the original, full-resolution image's own coordinates for
the caller, the same original-vs-display distinction already used
for colour sampling in ImageTransparentColorDialog.
*/
class CropAreaWidget : public QWidget
{
Q_OBJECT
public:
/// @param sourceImage the full, uncropped image to crop from
/// @param initialCropRect a previously-chosen crop rectangle, in
/// sourceImage's own coordinates, to start from -- an empty
/// (default-constructed) QRect means "no previous crop", i.e.
/// start covering the whole image.
explicit CropAreaWidget(const QImage &sourceImage, const QRect &initialCropRect = QRect(), QWidget *parent = nullptr);
/// The current crop rectangle, in the original image's own
/// coordinates (not display-space) -- always clamped to the
/// image's own bounds and never degenerate (zero width/height).
QRect cropRect() const;
public slots:
/// Resets the crop rectangle back to covering the entire image --
/// the direct answer to "I want to undo the crop, not just
/// adjust it", since dragging every handle back out by hand to
/// exactly the original extent is needlessly fiddly.
void resetToFullImage();
signals:
void cropRectChanged();
protected:
void paintEvent(QPaintEvent *event) override;
void mousePressEvent(QMouseEvent *event) override;
void mouseMoveEvent(QMouseEvent *event) override;
void mouseReleaseEvent(QMouseEvent *event) override;
private:
enum class DragMode { None, Move, ResizeN, ResizeS, ResizeE, ResizeW, ResizeNE, ResizeNW, ResizeSE, ResizeSW };
DragMode handleAt(const QPoint &pos) const;
void applyDrag(const QPoint &pos);
QImage m_source;
qreal m_displayScale = 1.0;
QRect m_displayImageRect; // where the (possibly scaled) pixmap actually sits within this widget
QRect m_cropRect; // in display-space, relative to m_displayImageRect's own origin
DragMode m_dragMode = DragMode::None;
QPoint m_dragStartPos;
QRect m_dragStartRect;
static constexpr int HANDLE_SIZE = 8;
static constexpr int MIN_CROP_SIZE = 16;
};
/**
@brief The ImageCropDialog class
Self-contained modal dialog wrapping CropAreaWidget with OK/Cancel --
the same reasoning as ImageTransparentColorDialog: this needs
neither undo-during-drag nor coexistence with other tools, only a
rectangle the user drags out against a pixmap already in hand.
*/
class ImageCropDialog : public QDialog
{
Q_OBJECT
public:
/// @param pixmap the full, uncropped image to crop from
/// @param existingCropRect a crop rectangle remembered from a
/// previous session, shown from the start rather than resetting
/// to the full image every time the dialog is reopened -- an
/// empty QRect means there isn't one yet.
explicit ImageCropDialog(const QPixmap &pixmap, const QRect &existingCropRect = QRect(), QWidget *parent = nullptr);
/// The chosen crop rectangle, in the original pixmap's own
/// coordinates. Only meaningful after the dialog was accepted.
QRect cropRect() const;
private:
CropAreaWidget *m_cropArea;
};
#endif // IMAGE_CROP_DIALOG_H
+224 -27
View File
@@ -20,6 +20,7 @@
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../diagram.h"
#include "../qetgraphicsitem/diagramimageitem.h"
#include "../qeticons.h"
#include "../ui_imagepropertieswidget.h"
/**
@@ -34,6 +35,11 @@ ImagePropertiesWidget::ImagePropertiesWidget(DiagramImageItem *image, QWidget *p
m_image(nullptr)
{
ui->setupUi(this);
// Matches the .ui file's own initial checked="true" -- toggled()
// only fires on a subsequent CHANGE, not for the state a widget
// already starts in, so the icon needs setting explicitly here or
// the button would show blank until first clicked.
ui->m_lock_ratio_tb->setIcon(QET::Icons::ObjectLocked);
this->setDisabled(true);
setImageItem(image);
}
@@ -58,12 +64,16 @@ void ImagePropertiesWidget::setImageItem(DiagramImageItem *image)
this->setEnabled(true);
if (m_image == image) return;
if (m_image)
disconnect(m_image, &QGraphicsObject::scaleChanged, this, &ImagePropertiesWidget::updateUi);
disconnect(m_image, &DiagramImageItem::transformChanged, this, &ImagePropertiesWidget::updateUi);
m_image = image;
connect(m_image, &QGraphicsObject::scaleChanged, this, &ImagePropertiesWidget::updateUi);
connect(m_image, &DiagramImageItem::transformChanged, this, &ImagePropertiesWidget::updateUi);
m_movable = image->isMovable();
m_scale = m_image->scale();
m_scaleX = m_image->scaleFactorX();
m_scaleY = m_image->scaleFactorY();
m_rotation = m_image->rotationAngle();
m_skewX = m_image->skewX();
m_skewY = m_image->skewY();
updateUi();
}
@@ -77,16 +87,20 @@ void ImagePropertiesWidget::apply()
if (m_image->diagram())
{
if (m_live_edit) disconnect(m_image, &QGraphicsObject::scaleChanged, this, &ImagePropertiesWidget::updateUi);
if (m_live_edit) disconnect(m_image, &DiagramImageItem::transformChanged, this, &ImagePropertiesWidget::updateUi);
QUndoCommand *undo = associatedUndo();
if (undo)
m_image->diagram()->undoStack().push(undo);
if (m_live_edit) connect(m_image, &QGraphicsObject::scaleChanged, this, &ImagePropertiesWidget::updateUi);
if (m_live_edit) connect(m_image, &DiagramImageItem::transformChanged, this, &ImagePropertiesWidget::updateUi);
}
m_scale = m_image->scale();
m_scaleX = m_image->scaleFactorX();
m_scaleY = m_image->scaleFactorY();
m_rotation = m_image->rotationAngle();
m_skewX = m_image->skewX();
m_skewY = m_image->skewY();
}
/**
@@ -97,7 +111,11 @@ void ImagePropertiesWidget::reset()
{
if(!m_image) return;
m_image->setScale(m_scale);
m_image->setScaleFactorX(m_scaleX);
m_image->setScaleFactorY(m_scaleY);
m_image->setRotationAngle(m_rotation);
m_image->setSkewX(m_skewX);
m_image->setSkewY(m_skewY);
m_image->setMovable(m_movable);
updateUi();
}
@@ -115,13 +133,19 @@ bool ImagePropertiesWidget::setLiveEdit(bool live_edit)
if (m_live_edit)
{
connect (ui->m_scale_slider, &QSlider::sliderReleased, this, &ImagePropertiesWidget::apply);
connect (ui->m_scale_sb, &QSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
connect (ui->m_width_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
connect (ui->m_height_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
connect (ui->m_angle_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
connect (ui->m_skew_x_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
connect (ui->m_skew_y_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
}
else
{
disconnect (ui->m_scale_slider, &QSlider::sliderReleased, this, &ImagePropertiesWidget::apply);
disconnect (ui->m_scale_sb, &QSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
disconnect (ui->m_width_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
disconnect (ui->m_height_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
disconnect (ui->m_angle_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
disconnect (ui->m_skew_x_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
disconnect (ui->m_skew_y_sb, &QDoubleSpinBox::editingFinished, this, &ImagePropertiesWidget::apply);
}
return true;
@@ -129,18 +153,44 @@ bool ImagePropertiesWidget::setLiveEdit(bool live_edit)
/**
@brief ImagePropertiesWidget::associatedUndo
@return the change in an undo command (ItemResizerCommand).
If there is no change return nullptr
@return the change in an undo command. If there is no change return
nullptr. Chains scaleFactorX, scaleFactorY, rotationAngle, skewX and
skewY as separate QPropertyUndoCommands, matching the established
"check each field, chain if changed" pattern used elsewhere -- each
of these is genuinely independent (the resize/rotate/skew handles
can set any of them without touching the others), so a single
combined command wouldn't correctly capture "only two of five
fields actually changed".
*/
QUndoCommand* ImagePropertiesWidget::associatedUndo() const
{
const qreal newScaleX = ui->m_width_sb->value() / 100.0;
const qreal newScaleY = ui->m_height_sb->value() / 100.0;
const qreal newRotation = ui->m_angle_sb->value();
const qreal newSkewX = ui->m_skew_x_sb->value();
const qreal newSkewY = ui->m_skew_y_sb->value();
QPropertyUndoCommand *undo = nullptr;
auto chain = [&](const char *property, qreal oldValue, qreal newValue, const QString &text)
{
if (qFuzzyCompare(oldValue, newValue))
return;
if (undo)
new QPropertyUndoCommand(m_image, property, oldValue, newValue, undo);
else
{
undo = new QPropertyUndoCommand(m_image, property, oldValue, newValue);
undo->enableAnimation();
undo->setText(text);
}
};
chain("scaleFactorX", m_scaleX, newScaleX, tr("Modifier la largeur d'une image"));
chain("scaleFactorY", m_scaleY, newScaleY, tr("Modifier la hauteur d'une image"));
chain("rotationAngle", m_rotation, newRotation, tr("Modifier l'angle d'une image"));
chain("skewX", m_skewX, newSkewX, tr("Modifier l'inclinaison d'une image"));
chain("skewY", m_skewY, newSkewY, tr("Modifier l'inclinaison d'une image"));
qreal value = ui->m_scale_slider->value();
value /= 100;
if (m_scale == value) return nullptr;
QPropertyUndoCommand *undo = new QPropertyUndoCommand(m_image, "scale", m_scale, value);
undo->enableAnimation();
undo->setText(tr("Modifier la taille d'une image"));
return undo;
}
@@ -150,20 +200,167 @@ QUndoCommand* ImagePropertiesWidget::associatedUndo() const
*/
void ImagePropertiesWidget::updateUi()
{
if (!m_image) return;
ui->m_scale_slider->setValue(m_image->scale() * 100);
// Two different uses of the same flag: an entry guard here (skip
// entirely if a locked width/height operation is already in
// progress elsewhere -- its own explicit updateUi() call once
// finished is what actually refreshes things), and, immediately
// below, a re-entrancy guard around this function's OWN setValue()
// calls, so they can't recurse back into
// on_m_width_sb_valueChanged()/on_m_height_sb_valueChanged() when
// this runs normally (e.g. from setImageItem(), or a transformChanged
// signal from a resize handle used directly on the canvas while
// this dialog is open).
if (!m_image || m_updating_ratio) return;
m_updating_ratio = true;
ui->m_width_sb->setValue(m_image->scaleFactorX() * 100.0);
ui->m_height_sb->setValue(m_image->scaleFactorY() * 100.0);
m_updating_ratio = false;
ui->m_angle_sb->setValue(m_image->rotationAngle());
ui->m_skew_x_sb->setValue(m_image->skewX());
ui->m_skew_y_sb->setValue(m_image->skewY());
ui->m_lock_pos_cb->setChecked(!m_image->isMovable());
}
/**
@brief ImagePropertiesWidget::on_m_scale_slider_valueChanged
Update the size of image when move slider.
@param value
@brief ImagePropertiesWidget::on_m_lock_ratio_tb_toggled
Keeps the button's own icon and tooltip in sync with its checked
state -- Qt shows the checked/unchecked state visually (a sunken
look) regardless, but the lock/unlock glyph makes the meaning
obvious at a glance rather than needing that convention noticed.
@param checked
*/
void ImagePropertiesWidget::on_m_scale_slider_valueChanged(int value)
void ImagePropertiesWidget::on_m_lock_ratio_tb_toggled(bool checked)
{
qreal scale = value;
m_image->setScale(scale / 100);
ui->m_lock_ratio_tb->setIcon(checked ? QET::Icons::ObjectLocked : QET::Icons::ObjectUnlocked);
ui->m_lock_ratio_tb->setToolTip(checked
? tr("Verrouillé : modifier la largeur ou la hauteur ajuste l'autre pour conserver les proportions. Cliquer pour déverrouiller.")
: tr("Déverrouillé : largeur et hauteur peuvent être modifiées indépendamment. Cliquer pour verrouiller."));
}
/**
@brief ImagePropertiesWidget::on_m_restore_ratio_pb_clicked
The same action as the item's own "Restaurer les proportions"
context-menu entry, offered here too since this dialog is exactly
where someone would notice the proportions are off in the first
place. Deliberately bypasses on_m_height_sb_valueChanged()'s own
lock-following logic (guarded by m_updating_ratio, then applied
directly rather than through the spinbox's own signal) rather than
going through it normally: if the lock happened to be engaged,
that logic would treat this as an ordinary edit and scale width by
the same ratio the height change just applied -- compounding into
some other, unrelated value instead of the plain "make height
match width" this button promises regardless of lock state.
*/
void ImagePropertiesWidget::on_m_restore_ratio_pb_clicked()
{
if (!m_image) return;
const qreal newScaleY = m_image->scaleFactorX();
m_updating_ratio = true;
ui->m_height_sb->setValue(newScaleY * 100.0);
m_updating_ratio = false;
m_image->setScaleFactorY(newScaleY);
apply();
}
/**
@brief ImagePropertiesWidget::on_m_width_sb_valueChanged
Applies the new width live, and -- if "Conserver les proportions" is
checked -- scales height by the same ratio this change just applied
to width, so the two stay locked together. Guarded by
m_updating_ratio: the setValue() call below would otherwise
re-trigger on_m_height_sb_valueChanged(), which would then try to
scale width again in response, recursing indefinitely.
@param value the new width, as a percentage
*/
void ImagePropertiesWidget::on_m_width_sb_valueChanged(double value)
{
if (!m_image || m_updating_ratio) return;
const qreal oldScaleX = m_image->scaleFactorX();
const qreal newScaleX = value / 100.0;
const bool wasLocked = ui->m_lock_ratio_tb->isChecked();
// Guards updateUi() itself here, not just the mutual spinbox
// updates below: setScaleFactorX() emits transformChanged()
// immediately, synchronously calling updateUi() before this
// function gets any further -- at that exact moment scaleX has
// already changed but scaleY hasn't caught up yet, so updateUi()'s
// "auto-unlock if currently mismatched" check would fire and
// uncheck the lock before the compensating height change below
// ever runs, defeating the lock on every single use. Released
// before the explicit updateUi() call at the end, once both axes
// genuinely agree again (if locked) and that check is meaningful.
m_updating_ratio = true;
m_image->setScaleFactorX(newScaleX);
if (wasLocked && !qFuzzyIsNull(oldScaleX))
{
const qreal ratio = newScaleX / oldScaleX;
const qreal newScaleY = m_image->scaleFactorY() * ratio;
ui->m_height_sb->setValue(newScaleY * 100.0);
m_image->setScaleFactorY(newScaleY);
}
m_updating_ratio = false;
updateUi();
}
/**
@brief ImagePropertiesWidget::on_m_height_sb_valueChanged
Mirror of on_m_width_sb_valueChanged() -- see its comment.
@param value the new height, as a percentage
*/
void ImagePropertiesWidget::on_m_height_sb_valueChanged(double value)
{
if (!m_image || m_updating_ratio) return;
const qreal oldScaleY = m_image->scaleFactorY();
const qreal newScaleY = value / 100.0;
const bool wasLocked = ui->m_lock_ratio_tb->isChecked();
m_updating_ratio = true; // see on_m_width_sb_valueChanged()'s identical comment
m_image->setScaleFactorY(newScaleY);
if (wasLocked && !qFuzzyIsNull(oldScaleY))
{
const qreal ratio = newScaleY / oldScaleY;
const qreal newScaleX = m_image->scaleFactorX() * ratio;
ui->m_width_sb->setValue(newScaleX * 100.0);
m_image->setScaleFactorX(newScaleX);
}
m_updating_ratio = false;
updateUi();
}
/**
@brief ImagePropertiesWidget::on_m_angle_sb_valueChanged
@param value the new rotation, in degrees
*/
void ImagePropertiesWidget::on_m_angle_sb_valueChanged(double value)
{
if (!m_image) return;
m_image->setRotationAngle(value);
}
/**
@brief ImagePropertiesWidget::on_m_skew_x_sb_valueChanged
@param value the new X skew, in degrees
*/
void ImagePropertiesWidget::on_m_skew_x_sb_valueChanged(double value)
{
if (!m_image) return;
m_image->setSkewX(value);
}
/**
@brief ImagePropertiesWidget::on_m_skew_y_sb_valueChanged
@param value the new Y skew, in degrees
*/
void ImagePropertiesWidget::on_m_skew_y_sb_valueChanged(double value)
{
if (!m_image) return;
m_image->setSkewY(value);
}
/**
+21 -2
View File
@@ -48,14 +48,33 @@ class ImagePropertiesWidget : public PropertiesEditorWidget
void updateUi() override;
private slots:
void on_m_scale_slider_valueChanged(int value);
void on_m_width_sb_valueChanged(double value);
void on_m_height_sb_valueChanged(double value);
void on_m_angle_sb_valueChanged(double value);
void on_m_skew_x_sb_valueChanged(double value);
void on_m_skew_y_sb_valueChanged(double value);
void on_m_lock_ratio_tb_toggled(bool checked);
void on_m_restore_ratio_pb_clicked();
void on_m_lock_pos_cb_clicked();
private:
Ui::ImagePropertiesWidget *ui;
DiagramImageItem *m_image;
bool m_movable;
qreal m_scale;
// All tracked independently -- the image may already be
// non-uniformly scaled, rotated, and/or skewed via the resize/
// rotate/skew handles before this dialog is even opened, and
// undo/reset have to restore each to whatever it actually was.
qreal m_scaleX;
qreal m_scaleY;
qreal m_rotation;
qreal m_skewX;
qreal m_skewY;
// Guards the width/height spinboxes' mutual updates when
// "Conserver les proportions" is checked, so setting one
// programmatically in response to the other doesn't re-trigger
// its own valueChanged and recurse.
bool m_updating_ratio = false;
};
#endif // IMAGEPROPERTIESWIDGET_H
+140 -71
View File
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>244</width>
<height>105</height>
<width>280</width>
<height>230</height>
</rect>
</property>
<property name="windowTitle">
@@ -17,47 +17,149 @@
<property name="sizeConstraint">
<enum>QLayout::SetMinimumSize</enum>
</property>
<item row="2" column="0">
<item row="0" column="0">
<widget class="QLabel" name="label_width">
<property name="text">
<string>Largeur</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="m_width_sb">
<property name="suffix">
<string> %</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>1.000000000000000</double>
</property>
<property name="maximum">
<double>1000.000000000000000</double>
</property>
</widget>
</item>
<item row="0" column="2" rowspan="2">
<widget class="QToolButton" name="m_lock_ratio_tb">
<property name="toolTip">
<string>Verrouillé : modifier la largeur ou la hauteur ajuste l'autre pour conserver les proportions. Cliquer pour déverrouiller.</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_height">
<property name="text">
<string>Hauteur</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="m_height_sb">
<property name="suffix">
<string> %</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>1.000000000000000</double>
</property>
<property name="maximum">
<double>1000.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="0" colspan="3">
<widget class="QPushButton" name="m_restore_ratio_pb">
<property name="text">
<string>Restaurer les proportions</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_angle">
<property name="text">
<string>Angle</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QDoubleSpinBox" name="m_angle_sb">
<property name="suffix">
<string> °</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>-360.000000000000000</double>
</property>
<property name="maximum">
<double>360.000000000000000</double>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_skew_x">
<property name="text">
<string>Inclinaison X</string>
</property>
</widget>
</item>
<item row="4" column="1">
<widget class="QDoubleSpinBox" name="m_skew_x_sb">
<property name="suffix">
<string> °</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>-85.000000000000000</double>
</property>
<property name="maximum">
<double>85.000000000000000</double>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_skew_y">
<property name="text">
<string>Inclinaison Y</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QDoubleSpinBox" name="m_skew_y_sb">
<property name="suffix">
<string> °</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>-85.000000000000000</double>
</property>
<property name="maximum">
<double>85.000000000000000</double>
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="m_lock_pos_cb">
<property name="text">
<string>Verrouiller la position</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Dimension de l'image</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QSlider" name="m_scale_slider">
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>400</number>
</property>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="m_scale_sb">
<property name="suffix">
<string>%</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>400</number>
</property>
</widget>
</item>
<item row="3" column="0">
<item row="7" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
@@ -73,38 +175,5 @@
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>m_scale_slider</sender>
<signal>valueChanged(int)</signal>
<receiver>m_scale_sb</receiver>
<slot>setValue(int)</slot>
<hints>
<hint type="sourcelabel">
<x>81</x>
<y>40</y>
</hint>
<hint type="destinationlabel">
<x>190</x>
<y>40</y>
</hint>
</hints>
</connection>
<connection>
<sender>m_scale_sb</sender>
<signal>valueChanged(int)</signal>
<receiver>m_scale_slider</receiver>
<slot>setValue(int)</slot>
<hints>
<hint type="sourcelabel">
<x>190</x>
<y>40</y>
</hint>
<hint type="destinationlabel">
<x>81</x>
<y>40</y>
</hint>
</hints>
</connection>
</connections>
<connections/>
</ui>
+396
View File
@@ -0,0 +1,396 @@
/*
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 "imagetransparentcolordialog.h"
#include <QDialogButtonBox>
#include <QGridLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QMouseEvent>
#include <QPainter>
#include <QPushButton>
#include <QSlider>
#include <QToolButton>
#include <QVBoxLayout>
#include <QVector>
namespace {
// Fits the dialog comfortably on a normal screen regardless of the
// source image's own resolution -- a photo straight off a phone
// would otherwise make this dialog enormous.
constexpr int MAX_DISPLAY_SIZE = 350;
}
/**
@brief ClickableImageLabel::ClickableImageLabel
@param sourceImage the full-resolution image to display and sample from
@param parent
*/
ClickableImageLabel::ClickableImageLabel(const QImage &sourceImage, QWidget *parent) :
QLabel(parent),
m_source(sourceImage)
{
const qreal scaleW = qreal(MAX_DISPLAY_SIZE) / m_source.width();
const qreal scaleH = qreal(MAX_DISPLAY_SIZE) / m_source.height();
m_displayScale = qMin(qreal(1.0), qMin(scaleW, scaleH)); // never upscale a small image, only ever shrink a large one
m_displayImage = (m_displayScale < 1.0)
? m_source.scaled(m_source.size() * m_displayScale, Qt::KeepAspectRatio, Qt::SmoothTransformation)
: m_source;
setPixmap(QPixmap::fromImage(m_displayImage));
setCursor(Qt::CrossCursor);
setToolTip(tr("Cliquez pour choisir une couleur"));
}
/**
@brief ClickableImageLabel::mousePressEvent
Maps the click back to the original, full-resolution image before
sampling -- see the class-level comment for why.
@param event
*/
void ClickableImageLabel::mousePressEvent(QMouseEvent *event)
{
const QPixmap label_pixmap = pixmap();
if (event->button() != Qt::LeftButton || label_pixmap.isNull())
return;
// The label may be larger than its pixmap (layout stretching); the
// pixmap itself is always drawn centered by Qt's default alignment,
// so the click has to be re-based against the pixmap's own rect
// within the label, not the label's own top-left.
const QRect pixmapRect(
(width() - label_pixmap.width()) / 2,
(height() - label_pixmap.height()) / 2,
label_pixmap.width(), label_pixmap.height());
if (!pixmapRect.contains(event->pos()))
return;
const QPoint withinPixmap = event->pos() - pixmapRect.topLeft();
QPoint originalPos(qRound(withinPixmap.x() / m_displayScale), qRound(withinPixmap.y() / m_displayScale));
originalPos.setX(qBound(0, originalPos.x(), m_source.width() - 1));
originalPos.setY(qBound(0, originalPos.y(), m_source.height() - 1));
emit colorPicked(m_source.pixelColor(originalPos));
}
/**
@brief ImageTransparentColorDialog::ImageTransparentColorDialog
@param pixmap the image to pick a transparent colour from
@param parent
*/
ImageTransparentColorDialog::ImageTransparentColorDialog(const QPixmap &basePixmap,
const QList<PickedColor> &existingColors, QWidget *parent) :
QDialog(parent),
m_sourceImage(basePixmap.toImage()),
m_pickedColors(existingColors)
{
setWindowTitle(tr("Couleur transparente"));
if (!m_pickedColors.isEmpty())
m_lastToleranceUsed = m_pickedColors.last().tolerance;
m_sourceLabel = new ClickableImageLabel(m_sourceImage, this);
m_previewSourceImage = m_sourceLabel->displayImage();
m_previewLabel = new QLabel(this);
m_hintLabel = new QLabel(this);
m_hintLabel->setWordWrap(true);
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
m_okButton = buttons->button(QDialogButtonBox::Ok);
m_startedWithColors = !m_pickedColors.isEmpty();
updateOkEnabled();
auto *grid = new QGridLayout;
grid->addWidget(new QLabel(tr("Image source")), 0, 0);
grid->addWidget(new QLabel(tr("Aperçu")), 0, 1);
grid->addWidget(m_sourceLabel, 1, 0);
grid->addWidget(m_previewLabel, 1, 1);
// One row per picked colour, appended/removed as rebuildSwatches()
// runs -- starts empty either way, populated by the very first
// rebuildSwatches() call below (including from existingColors, if
// any were passed in).
m_swatchesLayout = new QVBoxLayout;
auto *mainLayout = new QVBoxLayout(this);
mainLayout->addLayout(grid);
mainLayout->addWidget(m_hintLabel);
mainLayout->addLayout(m_swatchesLayout);
mainLayout->addWidget(buttons);
connect(m_sourceLabel, &ClickableImageLabel::colorPicked, this, &ImageTransparentColorDialog::onColorPicked);
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
rebuildSwatches(); // shows existingColors immediately, if any
updatePreview(); // and the preview already reflects them too, rather than only appearing after the next pick
}
/**
@brief ImageTransparentColorDialog::onColorPicked
Adds to the set of picked colours rather than replacing the previous
pick -- clicking a second, different pixel used to silently discard
the first choice, with no way to work with more than one colour (a
white background *and* a grey border, say) in the same pass.
Skips an exact duplicate rather than adding a second, indistinguishable
row for it. The new colour is seeded with whatever tolerance was
last used (not some fixed default), so successive picks in one
session feel consistent.
@param color the colour sampled from the source image
*/
void ImageTransparentColorDialog::onColorPicked(const QColor &color)
{
for (const PickedColor &existing : std::as_const(m_pickedColors))
if (existing.color == color)
return;
m_pickedColors.append({color, m_lastToleranceUsed});
updateOkEnabled();
rebuildSwatches();
updatePreview();
}
/**
@brief ImageTransparentColorDialog::setToleranceForIndex
Updates one colour's own tolerance in place -- deliberately doesn't
call rebuildSwatches(): only a number changed, not which rows exist
or what order they're in, so recreating every row's widgets (and,
worse, the very slider currently being dragged) on each tick would
be both wasteful and liable to interrupt the drag itself.
@param index which entry in m_pickedColors changed
@param value its new tolerance, 0-100
*/
void ImageTransparentColorDialog::setToleranceForIndex(int index, int value)
{
if (index < 0 || index >= m_pickedColors.size())
return;
m_pickedColors[index].tolerance = value;
m_lastToleranceUsed = value;
updatePreview();
}
/**
@brief ImageTransparentColorDialog::updateOkEnabled
OK is disabled only for the one case where accepting would genuinely
do nothing: a dialog that started with no colours and still has
none. Removing every colour from a dialog that DID start with some
is a different, meaningful action -- clearing all transparency back
to a plain, opaque image -- and has to stay confirmable, not silently
blocked the same way. Re-run after every add or remove, since either
can cross the "any colours at all, ever" threshold in either
direction.
*/
void ImageTransparentColorDialog::updateOkEnabled()
{
m_okButton->setEnabled(!m_pickedColors.isEmpty() || m_startedWithColors);
}
/**
@brief ImageTransparentColorDialog::removeColor
Removes one colour from the set -- the counterpart onColorPicked()
was missing entirely before: picking the wrong pixel by mistake had
no way to undo except cancelling the whole dialog and starting over.
@param index position in m_pickedColors to remove
*/
void ImageTransparentColorDialog::removeColor(int index)
{
if (index < 0 || index >= m_pickedColors.size())
return;
m_pickedColors.removeAt(index);
updateOkEnabled();
rebuildSwatches();
updatePreview();
}
/**
@brief ImageTransparentColorDialog::rebuildSwatches
Rebuilds the list of picked-colour rows from scratch against the
current m_pickedColors -- simpler and safer than trying to
incrementally add/remove individual rows in sync with the list,
given the list only ever changes one colour at a time and is never
large enough for a full rebuild to be a real cost. Each row is a
colour swatch (a plain, round, non-interactive indicator -- there's
nothing left to click it FOR, now that each row carries its own
slider instead of one shared slider needing a row selected first),
that row's own tolerance slider, and a small "x" button that
removes it.
*/
void ImageTransparentColorDialog::rebuildSwatches()
{
QLayoutItem *item;
while ((item = m_swatchesLayout->takeAt(0)) != nullptr)
{
delete item->widget();
delete item;
}
for (int i = 0; i < m_pickedColors.size(); ++i)
{
const PickedColor &pc = m_pickedColors.at(i);
auto *row = new QWidget(this);
auto *rowLayout = new QHBoxLayout(row);
rowLayout->setContentsMargins(0, 0, 0, 0);
auto *swatch = new QLabel(row);
swatch->setFixedSize(24, 24);
swatch->setStyleSheet(QStringLiteral(
"background-color: rgb(%1,%2,%3); border: 1px solid palette(mid); border-radius: 12px;")
.arg(pc.color.red()).arg(pc.color.green()).arg(pc.color.blue()));
swatch->setToolTip(tr("rgb(%1, %2, %3)").arg(pc.color.red()).arg(pc.color.green()).arg(pc.color.blue()));
auto *slider = new QSlider(Qt::Horizontal, row);
slider->setRange(0, 100);
slider->setValue(pc.tolerance);
slider->setToolTip(tr("Tolérance pour cette couleur"));
connect(slider, &QSlider::valueChanged, this, [this, i](int value) { setToleranceForIndex(i, value); });
auto *removeButton = new QToolButton(row);
removeButton->setText(QStringLiteral("×"));
removeButton->setToolTip(tr("Retirer cette couleur"));
connect(removeButton, &QToolButton::clicked, this, [this, i]() { removeColor(i); });
rowLayout->addWidget(swatch);
rowLayout->addWidget(slider);
rowLayout->addWidget(removeButton);
m_swatchesLayout->addWidget(row);
}
m_hintLabel->setText(m_pickedColors.isEmpty()
? tr("Cliquez sur l'image pour choisir une couleur")
: tr("Cliquez sur l'image pour ajouter une couleur. Ajustez la tolérance de chaque couleur avec son curseur, ou cliquez sur × pour la retirer."));
}
/**
@brief ImageTransparentColorDialog::updatePreview
Recomputes the checkerboard-backed preview against the current set
of picked colours, each at its own tolerance. Runs against
m_previewSourceImage (the same downsampled copy ClickableImageLabel
already computed for its own display, not the full-resolution
m_sourceImage) -- this fires on every tolerance slider tick, not
just on release, and a full-resolution O(width*height*colours) pass
per tick was visibly laggy on a large source image. resultPixmap(),
below, still computes the final, committed result at full
resolution -- only this live preview is downsampled. Always
re-evaluates every picked colour from scratch against
m_previewSourceImage, not any already-keyed result, so adjusting a
tolerance or adding/removing a colour never compounds successive
passes.
*/
void ImageTransparentColorDialog::updatePreview()
{
const QImage keyed = applyColorKey(m_previewSourceImage, m_pickedColors);
m_previewLabel->setPixmap(onCheckerboard(keyed));
}
/**
@brief ImageTransparentColorDialog::resultPixmap
@return the colour-keyed pixmap against every picked colour, each at
its own tolerance, or the original pixmap unchanged if none were
ever picked (the Ok button stays disabled until at least one is, so
this is mostly a defensive fallback).
*/
QPixmap ImageTransparentColorDialog::resultPixmap() const
{
if (m_pickedColors.isEmpty())
return QPixmap::fromImage(m_sourceImage);
return QPixmap::fromImage(applyColorKey(m_sourceImage, m_pickedColors));
}
/**
@brief ImageTransparentColorDialog::applyColorKey
Binary transparency within tolerance, not a smooth falloff: every
pixel within a colour's own `tolerance` (0-100, mapped onto the
maximum possible RGB distance) of that colour becomes fully
transparent, everything else keeps its existing alpha untouched --
each of keyColors is checked against its own threshold, not one
shared across all of them, since a background colour and a border
colour rarely call for the same looseness of match. Squared
distance throughout, avoiding a sqrt per pixel; breaks out of the
inner loop on the first matching colour, since further matches
wouldn't change the outcome.
@param source the image to key
@param keyColors the colours (each with its own tolerance) to make transparent
@return the resulting image, always in Format_ARGB32
*/
QImage ImageTransparentColorDialog::applyColorKey(const QImage &source, const QList<PickedColor> &keyColors)
{
QImage result = source.convertToFormat(QImage::Format_ARGB32);
if (keyColors.isEmpty())
return result;
struct KeyEntry { QRgb rgb; qint64 threshold; };
QVector<KeyEntry> keys;
keys.reserve(keyColors.size());
for (const PickedColor &pc : keyColors)
{
const qint64 threshold = qint64(pc.tolerance) * pc.tolerance * 3 * 255 * 255 / (100 * 100);
keys.append({pc.color.rgb(), threshold});
}
for (int y = 0; y < result.height(); ++y)
{
QRgb *line = reinterpret_cast<QRgb *>(result.scanLine(y));
for (int x = 0; x < result.width(); ++x)
{
const QRgb px = line[x];
for (const KeyEntry &key : std::as_const(keys))
{
const int dr = qRed(px) - qRed(key.rgb), dg = qGreen(px) - qGreen(key.rgb), db = qBlue(px) - qBlue(key.rgb);
const qint64 distSq = qint64(dr) * dr + qint64(dg) * dg + qint64(db) * db;
if (distSq <= key.threshold)
{
line[x] = qRgba(qRed(px), qGreen(px), qBlue(px), 0);
break;
}
}
}
}
return result;
}
/**
@brief ImageTransparentColorDialog::onCheckerboard
Composites `image` over a light/dark checkerboard, the standard
visual convention for showing where an image is transparent --
without this, a fully keyed-out region would just show whatever
widget background happens to be behind it, easy to misread as "still
opaque, just white" rather than "correctly transparent".
@param image the (possibly partially transparent) image to composite
@return a checkerboard-backed pixmap ready to display
*/
QPixmap ImageTransparentColorDialog::onCheckerboard(const QImage &image)
{
const int cell = 8;
QPixmap board(image.size());
QPainter painter(&board);
for (int y = 0; y < image.height(); y += cell)
{
for (int x = 0; x < image.width(); x += cell)
{
const bool dark = ((x / cell) + (y / cell)) % 2 == 0;
painter.fillRect(x, y, cell, cell, dark ? QColor(200, 200, 200) : QColor(255, 255, 255));
}
}
painter.drawImage(0, 0, image);
painter.end();
return board;
}
+161
View File
@@ -0,0 +1,161 @@
/*
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 IMAGE_TRANSPARENT_COLOR_DIALOG_H
#define IMAGE_TRANSPARENT_COLOR_DIALOG_H
#include <QDialog>
#include <QImage>
#include <QColor>
#include <QLabel>
#include <QList>
class QVBoxLayout;
class QPushButton;
class QMouseEvent;
/**
@brief The ClickableImageLabel class
Displays an image scaled to fit a reasonable dialog size, but always
samples the picked color from the original, full-resolution image at
the corresponding coordinate -- never from the (possibly smoothly
interpolated, and therefore color-blended) scaled-down pixmap being
displayed, which would make the picked color subtly wrong for
exactly the pixels near an edge where it matters most.
*/
class ClickableImageLabel : public QLabel
{
Q_OBJECT
public:
explicit ClickableImageLabel(const QImage &sourceImage, QWidget *parent = nullptr);
/// The same downsampled image already computed for display --
/// reused as the live-preview source so a tolerance drag runs
/// its per-tick colour-key pass against a small image instead
/// of the full-resolution one, which on a large source (a
/// scanned schematic background, several Mpx) made every
/// intermediate slider tick visibly lag. resultPixmap() still
/// computes the final, committed result from the true
/// full-resolution source -- only the live preview is
/// downsampled.
QImage displayImage() const { return m_displayImage; }
signals:
void colorPicked(const QColor &color);
protected:
void mousePressEvent(QMouseEvent *event) override;
private:
QImage m_source;
QImage m_displayImage;
qreal m_displayScale = 1.0;
};
/**
@brief The ImageTransparentColorDialog class
Lets the user click directly on a preview of the image to sample one
or more colors -- each click adds to the set rather than replacing
the previous pick -- each shown as its own row: a colour swatch, a
slider for that colour's own tolerance right next to it, and a
remove button. Each colour keeps its own tolerance rather than
sharing one: a white background and a grey border rarely need the
same looseness of match, and forcing one tolerance onto both meant
either the background left ragged edges or the border ate into
content near it. A live checkerboard-backed preview of the combined
result updates as any slider moves. A self-contained modal dialog
rather than a diagram-level "click the canvas to pick" interaction
mode, since this needs neither undo-during-drag nor coexistence
with other tools; it only ever needs a handful of clicks, evaluated
against a pixmap the caller already has in hand.
*/
class ImageTransparentColorDialog : public QDialog
{
Q_OBJECT
public:
/// One picked colour and the tolerance it's individually keyed
/// with -- the whole point of this being a struct rather than
/// two parallel lists is that the two can never drift out of
/// index alignment with each other.
struct PickedColor
{
QColor color;
int tolerance;
};
/// @param basePixmap the pristine source to pick colours from --
/// the caller's responsibility to pass the true original, not
/// an already colour-keyed result, or previously-transparent
/// areas would show as plain background rather than a pickable
/// surface, and re-picking the same colour would be a no-op.
/// @param existingColors colours (with their individual
/// tolerances) already keyed out of basePixmap in a previous
/// session, shown as rows from the start rather than forcing
/// them to be re-picked from scratch.
explicit ImageTransparentColorDialog(const QPixmap &basePixmap,
const QList<PickedColor> &existingColors = {}, QWidget *parent = nullptr);
/// The resulting pixmap: basePixmap unchanged if no colour is
/// picked, colour-keyed against every picked colour (each at
/// its own tolerance) otherwise.
QPixmap resultPixmap() const;
/// The final set of picked colours and their individual
/// tolerances, for the caller to remember across dialog
/// sessions -- may differ from existingColors if any were
/// added, removed, or had their tolerance adjusted.
QList<PickedColor> pickedColors() const { return m_pickedColors; }
/// Public so DiagramImageItem can re-derive its display pixmap
/// directly (base + crop + these colours) without needing to
/// re-open this dialog every time the crop region changes --
/// binary transparency within tolerance, not a smooth falloff:
/// every pixel within a colour's own `tolerance` (0-100, mapped
/// onto the maximum possible RGB distance) of that colour
/// becomes fully transparent, everything else keeps its
/// existing alpha untouched. Squared distance throughout,
/// avoiding a sqrt per pixel; breaks out of the inner loop on
/// the first matching colour, since further matches wouldn't
/// change the outcome.
static QImage applyColorKey(const QImage &source, const QList<PickedColor> &keyColors);
private slots:
void onColorPicked(const QColor &color);
private:
void setToleranceForIndex(int index, int value);
void removeColor(int index);
void rebuildSwatches();
void updatePreview();
void updateOkEnabled();
static QPixmap onCheckerboard(const QImage &image);
QImage m_sourceImage;
QImage m_previewSourceImage; // downsampled -- see ClickableImageLabel::displayImage()'s comment for why
QList<PickedColor> m_pickedColors;
bool m_startedWithColors = false; // whether existingColors was non-empty -- see updateOkEnabled()'s comment for why this matters
int m_lastToleranceUsed = 10; // seeds a newly-picked colour's own tolerance, so successive picks in one session feel consistent rather than each resetting to some fixed default
ClickableImageLabel *m_sourceLabel;
QLabel *m_previewLabel;
QVBoxLayout *m_swatchesLayout; // one row per picked colour, stacked vertically -- each row now carries its own slider, too wide to lay out side by side the way plain swatches once were
QLabel *m_hintLabel;
QPushButton *m_okButton;
};
#endif // IMAGE_TRANSPARENT_COLOR_DIALOG_H
+2
View File
@@ -20,6 +20,7 @@
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../diagram.h"
#include "../diagramcommands.h"
#include "../lastusedstyle.h"
#include "../qetgraphicsitem/independenttextitem.h"
#include "../ui_inditextpropertieswidget.h"
@@ -455,6 +456,7 @@ void IndiTextPropertiesWidget::on_m_font_pb_clicked()
m_font_is_selected = true;
ui->m_font_pb->setText(font.family());
ui->m_size_sb->setValue(font.pointSize());
LastUsedStyle::setTextFont(m_selected_font);
apply();
} else {
ui->m_font_pb->setText(tr("Police"));
+313
View File
@@ -0,0 +1,313 @@
/*
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 "jumptoelementdialog.h"
#include "../diagram.h"
#include "../diagramview.h"
#include "../qetproject.h"
#include "../qetgraphicsitem/element.h"
#include <QEvent>
#include <QKeyEvent>
#include <QLineEdit>
#include <QListWidget>
#include <QRegularExpression>
#include <QVBoxLayout>
/**
@brief JumpToElementDialog::JumpToElementDialog
@param diagram : the diagram whose elements can be jumped to
@param parent
*/
JumpToElementDialog::JumpToElementDialog(Diagram *diagram, QWidget *parent) :
QDialog(parent),
m_diagram(diagram)
{
setWindowTitle(tr("Atteindre un élément", "window title"));
m_filter_edit = new QLineEdit(this);
m_filter_edit->setPlaceholderText(tr("Nom, label ou information de l'élément, ou case (ex. B13 ou 3-B13)…"));
m_filter_edit->installEventFilter(this);
m_result_list = new QListWidget(this);
m_result_list->setFocusPolicy(Qt::NoFocus);
auto *layout = new QVBoxLayout(this);
layout->addWidget(m_filter_edit);
layout->addWidget(m_result_list);
setLayout(layout);
resize(420, 320);
connect(m_filter_edit, &QLineEdit::textChanged, this, &JumpToElementDialog::updateFilteredList);
connect(m_result_list, &QListWidget::itemActivated, this, &JumpToElementDialog::activateCurrentItem);
buildCandidates();
updateFilteredList(QString());
m_filter_edit->setFocus();
}
JumpToElementDialog::~JumpToElementDialog()
{
}
/**
@brief JumpToElementDialog::buildCandidates
Collect every element on m_diagram into m_candidates, along with the
searchable text used to filter it (its type name, its label, and its
other element informations).
*/
void JumpToElementDialog::buildCandidates()
{
m_candidates.clear();
if (!m_diagram) {
return;
}
for (auto item : m_diagram->items()) {
Element *element = qgraphicsitem_cast<Element *>(item);
if (!element) {
continue;
}
const QString label = element->elementInformations().value(QStringLiteral("label")).toString();
const QString name = element->name();
Candidate candidate;
candidate.element = element;
candidate.label = label;
candidate.display_text = label.isEmpty() ? name : (label + QStringLiteral(" — ") + name);
QStringList search_parts;
search_parts << label << name;
const DiagramContext infos = element->elementInformations();
for (const QString &key : infos.keys()) {
search_parts << infos.value(key).toString();
}
candidate.search_text = search_parts.join(QLatin1Char(' ')).toLower();
m_candidates << candidate;
}
}
/**
@brief JumpToElementDialog::updateFilteredList
Refill m_result_list with every candidate whose search text contains
@a filter_text (case-insensitive), and select the first match.
@param filter_text
*/
void JumpToElementDialog::updateFilteredList(const QString &filter_text)
{
m_result_list->clear();
const QString needle = filter_text.trimmed().toLower();
bool exact_label_match = false;
for (int i = 0; i < m_candidates.size(); ++i) {
const Candidate &candidate = m_candidates.at(i);
if (!candidate.element) {
continue;
}
if (!needle.isEmpty() && !candidate.search_text.contains(needle)) {
continue;
}
auto *list_item = new QListWidgetItem(candidate.display_text, m_result_list);
list_item->setData(Qt::UserRole, i);
if (candidate.label.compare(needle, Qt::CaseInsensitive) == 0) {
exact_label_match = true;
}
}
//A cell of the border, on this folio (ex : B13) or on another one
//(ex : 3-B13, folio 3, the way cross references write it), comes
//first, unless an element is labelled exactly like it: Enter keeps
//jumping to that element.
if (QListWidgetItem *cell_item = cellItem(needle)) {
m_result_list->insertItem(exact_label_match ? m_result_list->count() : 0,
cell_item);
}
if (m_result_list->count() > 0) {
m_result_list->setCurrentRow(0);
}
}
/**
@brief JumpToElementDialog::activateCurrentItem
Select the element corresponding to the currently highlighted result
on the diagram, scroll it into view, and close this dialog.
*/
void JumpToElementDialog::activateCurrentItem()
{
QListWidgetItem *current = m_result_list->currentItem();
if (!current || !m_diagram) {
reject();
return;
}
const int index = current->data(Qt::UserRole).toInt();
if (index == -1) {
const QList<Diagram *> diagrams = m_diagram->project()
? m_diagram->project()->diagrams()
: QList<Diagram *>();
const int folio = current->data(Qt::UserRole + 2).toInt();
if (folio >= 0 && folio < diagrams.size()) {
zoomToCell(diagrams.at(folio), current->data(Qt::UserRole + 1).toRectF());
}
accept();
return;
}
if (index < 0 || index >= m_candidates.size()) {
reject();
return;
}
Element *element = m_candidates.at(index).element;
if (!element) {
reject();
return;
}
m_diagram->clearSelection();
element->setSelected(true);
element->ensureVisible();
accept();
}
/**
@brief JumpToElementDialog::cellItem
@param needle : the typed text, trimmed and lower case
@return a new list item for the cell \a needle names, on m_diagram
(ex : b13) or on the folio at a position of the project (ex : 3-b13,
3b13, p3b13), or nullptr if \a needle names no cell of an existing
folio. The item holds -1, the cell rect and the folio position.
*/
QListWidgetItem *JumpToElementDialog::cellItem(const QString &needle) const
{
if (!m_diagram || !m_diagram->project()) {
return nullptr;
}
const QList<Diagram *> diagrams = m_diagram->project()->diagrams();
Diagram *diagram = m_diagram;
QString cell = needle;
QString text;
QRectF cell_rect = diagram->border_and_titleblock.cellRect(cell);
if (cell_rect.isNull()) {
//A folio position then a cell, like %f-%l%c in cross references
static const QRegularExpression folio_cell_re(
QStringLiteral("^p?\\s*(\\d{1,4})\\s*[-/.:]?\\s*([a-z]+\\s*\\d{1,4})$"));
const QRegularExpressionMatch match = folio_cell_re.match(needle);
if (!match.hasMatch()) {
return nullptr;
}
const int folio = match.captured(1).toInt();
if (folio < 1 || folio > diagrams.size()) {
return nullptr;
}
diagram = diagrams.at(folio - 1);
cell = match.captured(2);
cell_rect = diagram->border_and_titleblock.cellRect(cell);
if (cell_rect.isNull()) {
return nullptr;
}
}
cell = cell.remove(QLatin1Char(' ')).toUpper();
if (diagram == m_diagram) {
text = tr("Case %1").arg(cell);
} else {
const QString title = diagram->title();
text = title.isEmpty()
? tr("Folio %1, case %2").arg(diagrams.indexOf(diagram) + 1).arg(cell)
: tr("Folio %1 (%2), case %3").arg(diagrams.indexOf(diagram) + 1).arg(title, cell);
}
auto *item = new QListWidgetItem(text);
item->setData(Qt::UserRole, -1);
item->setData(Qt::UserRole + 1, cell_rect);
item->setData(Qt::UserRole + 2, diagrams.indexOf(diagram));
return item;
}
/**
@brief JumpToElementDialog::zoomToCell
Show \a diagram and zoom its view on \a cell_rect with one cell of
context around it.
@param diagram : the folio holding the cell
@param cell_rect : the cell, in scene coordinate
*/
void JumpToElementDialog::zoomToCell(Diagram *diagram, const QRectF &cell_rect)
{
const QRectF rect = cell_rect.adjusted(-cell_rect.width(), -cell_rect.height(),
cell_rect.width(), cell_rect.height());
const bool other_folio = diagram != m_diagram;
if (other_folio) {
diagram->showMe();
}
for (QGraphicsView *view : diagram->views()) {
if (auto *diagram_view = qobject_cast<DiagramView *>(view)) {
if (other_folio) {
//A folio shown for the first time is laid out at
//its size once the tab switch has been handled
QMetaObject::invokeMethod(diagram_view, [diagram_view, rect]() {
diagram_view->zoomToRect(rect);
}, Qt::QueuedConnection);
} else {
diagram_view->zoomToRect(rect);
}
return;
}
}
}
/**
@brief JumpToElementDialog::eventFilter
Redirect Up/Down/Enter/Escape typed in the filter field to the result
list, so the user never has to leave the text field to navigate or
confirm a choice.
*/
bool JumpToElementDialog::eventFilter(QObject *watched, QEvent *event)
{
if (watched == m_filter_edit && event->type() == QEvent::KeyPress) {
auto *key_event = static_cast<QKeyEvent *>(event);
switch (key_event->key()) {
case Qt::Key_Down:
case Qt::Key_Up: {
const int row_count = m_result_list->count();
if (row_count == 0) {
return true;
}
int row = m_result_list->currentRow();
row = key_event->key() == Qt::Key_Down
? (row + 1) % row_count
: (row - 1 + row_count) % row_count;
m_result_list->setCurrentRow(row);
return true;
}
case Qt::Key_Return:
case Qt::Key_Enter:
activateCurrentItem();
return true;
case Qt::Key_Escape:
reject();
return true;
default:
break;
}
}
return QDialog::eventFilter(watched, event);
}
+72
View File
@@ -0,0 +1,72 @@
/*
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 JUMPTOELEMENTDIALOG_H
#define JUMPTOELEMENTDIALOG_H
#include <QDialog>
#include <QPointer>
class Diagram;
class Element;
class QLineEdit;
class QListWidget;
class QListWidgetItem;
/**
@brief The JumpToElementDialog class
A lightweight, transient "quick open" popup: type part of an element's
label or other information to live-filter the elements on a diagram,
then Enter to select the chosen element on the diagram and scroll it
into view. Typing a cell of the border instead (ex : B13, or 3-B13 for
the third folio of the project) offers to zoom on that cell. Up/Down move through the filtered list, Escape cancels
without changing the current selection.
*/
class JumpToElementDialog : public QDialog
{
Q_OBJECT
public:
explicit JumpToElementDialog(Diagram *diagram, QWidget *parent = nullptr);
~JumpToElementDialog() override;
protected:
bool eventFilter(QObject *watched, QEvent *event) override;
private slots:
void updateFilteredList(const QString &filter_text);
void activateCurrentItem();
private:
void buildCandidates();
QListWidgetItem *cellItem(const QString &needle) const;
void zoomToCell(Diagram *diagram, const QRectF &cell_rect);
struct Candidate {
QPointer<Element> element;
QString label;
QString display_text;
QString search_text;
};
QPointer<Diagram> m_diagram;
QList<Candidate> m_candidates;
QLineEdit *m_filter_edit;
QListWidget *m_result_list;
};
#endif // JUMPTOELEMENTDIALOG_H
+181 -39
View File
@@ -16,7 +16,7 @@
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
*/
#include "linksingleelementwidget.h"
#include "../qetgraphicsitem/masterelement.h"
#include "contactgroupselectiondialog.h"
#include "../qetgraphicsitem/conductor.h"
#include "../diagram.h"
#include "../diagramposition.h"
@@ -24,10 +24,16 @@
#include "../elementprovider.h"
#include "../undocommand/linkelementcommand.h"
#include "../qetinformation.h"
#include "../qetproject.h"
#include "../qetgraphicsitem/masterelement.h"
#include "../ui_linksingleelementwidget.h"
#include <QTreeWidgetItem>
#include <QInputDialog>
#include <QCheckBox>
#include <QSettings>
#include <QSignalBlocker>
/**
@brief LinkSingleElementWidget::LinkSingleElementWidget
@@ -50,6 +56,22 @@ LinkSingleElementWidget::LinkSingleElementWidget(Element *elmt,
m_show_qtwi = new QAction(tr("Montrer l'élément"), this);
m_show_element = new QAction(tr("Montrer l'élément esclave"), this);
m_save_header_state = new QAction(tr("Enregistrer la disposition"), this);
// Hide the full-masters checkbox for non-slave elements
const bool is_slave = (elmt && elmt->elementData().m_type == ElementData::Slave);
ui->m_hide_full_masters_cb->setVisible(is_slave);
// Restore persisted state before connecting to avoid toggled firing buildTree()
// while m_element is still null.
{
const QSignalBlocker blocker(ui->m_hide_full_masters_cb);
QSettings settings;
ui->m_hide_full_masters_cb->setChecked(
settings.value(QStringLiteral("link-element-widget/hideFullMasters"), false).toBool());
}
connect(ui->m_hide_full_masters_cb, &QCheckBox::toggled,
this, &LinkSingleElementWidget::hideFullMastersToggled);
connect(m_show_qtwi, &QAction::triggered, this, [=]()
{
@@ -175,6 +197,7 @@ void LinkSingleElementWidget::apply()
m_unlink = false;
m_element_to_link = nullptr;
m_pending_qtwi = nullptr;
m_pending_group_index = -1;
}
/**
@@ -188,8 +211,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();
@@ -242,6 +269,7 @@ void LinkSingleElementWidget::buildTree()
if (m_element->elementData().m_type == ElementData::Slave)
{
m_full_masters.clear();
for(const auto &elmt : elmt_vector)
{
@@ -281,6 +309,16 @@ void LinkSingleElementWidget::buildTree()
QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_tree_widget, str_list);
m_qtwi_elmt_hash.insert(qtwi, elmt);
m_qtwi_strl_hash.insert(qtwi, search_list);
// Check if this master is full
if (elmt->linkType() == Element::Master) {
MasterElement *me = qobject_cast<MasterElement*>(elmt);
if (me && me->isFull()) {
m_full_masters.insert(qtwi);
if (ui->m_hide_full_masters_cb->isChecked())
qtwi->setHidden(true);
}
}
}
@@ -345,8 +383,16 @@ void LinkSingleElementWidget::buildTree()
QSettings settings;
QVariant v = settings.value(QStringLiteral("link-element-widget/report-state"));
if(!v.isNull())
ui->m_tree_widget->header()->restoreState(v.toByteArray());
auto *header = ui->m_tree_widget->header();
if (v.isNull() || !header->restoreState(v.toByteArray()))
{
// Keep logical column IDs stable for saved layouts, but show the
// folio identity first even when the candidate has no conductor.
for (int column = 5; column < 8; ++column)
header->moveSection(header->visualIndex(column), column - 5);
ui->m_tree_widget->resizeColumnToContents(5);
ui->m_tree_widget->resizeColumnToContents(6);
}
}
setUpCompleter();
@@ -386,20 +432,28 @@ 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);
// If the master is full, we'll remove it from the list!
if (master->isFull()) {
// PLC slave can only link to PLC master, and vice versa
if (element_is_plc != master_is_plc) {
elmt_vector.removeAt(i);
continue;
}
// A master at its declared limit stays in the list. Removing
// it made a full master indistinguishable from one that does
// not exist: the candidate simply was not there, with nothing
// to say why. The limit is advisory -- see the prompt in
// MasterPropertiesWidget::on_link_button_clicked() -- so the
// user decides, rather than the list deciding for them.
}
}
return elmt_vector;
@@ -432,6 +486,8 @@ void LinkSingleElementWidget::setUpCompleter()
*/
void LinkSingleElementWidget::clearTreeWidget()
{
m_pending_qtwi = nullptr;
while(ui->m_tree_widget->topLevelItemCount())
{
QTreeWidgetItem *qtwi = ui->m_tree_widget->takeTopLevelItem(0);
@@ -444,6 +500,7 @@ void LinkSingleElementWidget::clearTreeWidget()
m_qtwi_elmt_hash.clear();
m_qtwi_strl_hash.clear();
m_full_masters.clear();
}
void LinkSingleElementWidget::setUpHeaderLabels()
@@ -513,7 +570,7 @@ void LinkSingleElementWidget::diagramWasRemovedFromProject()
// contains the master element linked to the edited element
// we must wait for this elements to be unlinked,
// or else the list of available master isn't up to date
QTimer::singleShot(10, this, SLOT(updateUi()));
QTimer::singleShot(10, this, &LinkSingleElementWidget::updateUi);
}
void LinkSingleElementWidget::showedElementWasDeleted()
@@ -529,9 +586,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 +686,7 @@ void LinkSingleElementWidget::linkTriggered()
Qt::NoBrush));
}
}
for (int i=0 ; i<6 ; i++)
{
m_qtwi_at_context_menu->setBackground(i,
@@ -561,7 +695,7 @@ void LinkSingleElementWidget::linkTriggered()
}
m_pending_qtwi = m_qtwi_at_context_menu;
}
}
/**
@@ -575,6 +709,7 @@ void LinkSingleElementWidget::hideButtons()
ui->m_show_linked_pb->hide();
ui->m_show_this_pb->hide();
ui->m_search_field->show();
ui->m_hide_full_masters_cb->show();
}
/**
@@ -588,6 +723,7 @@ void LinkSingleElementWidget::showButtons()
ui->m_show_linked_pb->show();
ui->m_show_this_pb->show();
ui->m_search_field->hide();
ui->m_hide_full_masters_cb->hide();
}
void LinkSingleElementWidget::headerCustomContextMenuRequested(
@@ -625,8 +761,7 @@ void LinkSingleElementWidget::on_m_tree_widget_itemDoubleClicked(
if (m_showed_element)
{
disconnect(m_showed_element, SIGNAL(destroyed()),
this, SLOT(showedElementWasDeleted()));
disconnect(m_showed_element, &QObject::destroyed, this, &LinkSingleElementWidget::showedElementWasDeleted);
m_showed_element->setHighlighted(false);
}
@@ -634,8 +769,7 @@ void LinkSingleElementWidget::on_m_tree_widget_itemDoubleClicked(
elmt->diagram()->showMe();
elmt->setHighlighted(true);
m_showed_element = elmt;
connect(m_showed_element, SIGNAL(destroyed()),
this, SLOT(showedElementWasDeleted()));
connect(m_showed_element, &QObject::destroyed, this, &LinkSingleElementWidget::showedElementWasDeleted);
}
@@ -685,27 +819,35 @@ void LinkSingleElementWidget::on_m_show_this_pb_clicked()
If arg1 is empty, show all items.
@param arg1
*/
void LinkSingleElementWidget::on_m_search_field_textEdited(const QString &arg1)
void LinkSingleElementWidget::on_m_search_field_textEdited(const QString &)
{
//Show all items if arg1 is empty, if not hide all items
foreach(QTreeWidgetItem *qtwi, m_qtwi_elmt_hash.keys())
qtwi->setHidden(!arg1.isEmpty());
QList <QTreeWidgetItem *> qtwi_list;
foreach(QTreeWidgetItem *qtwi, m_qtwi_strl_hash.keys())
updateItemsVisibility();
}
void LinkSingleElementWidget::hideFullMastersToggled(bool checked)
{
QSettings settings;
settings.setValue(QStringLiteral("link-element-widget/hideFullMasters"), checked);
updateItemsVisibility();
}
void LinkSingleElementWidget::updateItemsVisibility()
{
const QString text = ui->m_search_field->text();
const bool hide_full = ui->m_hide_full_masters_cb->isChecked();
for (auto it = m_qtwi_strl_hash.cbegin(); it != m_qtwi_strl_hash.cend(); ++it)
{
foreach(QString str, m_qtwi_strl_hash.value(qtwi))
{
if(str.contains(arg1, Qt::CaseInsensitive))
{
qtwi_list << qtwi;
continue;
bool match = text.isEmpty();
if (!match) {
for (const QString &str : it.value()) {
if (str.contains(text, Qt::CaseInsensitive)) {
match = true;
break;
}
}
}
it.key()->setHidden(!match
|| (hide_full && m_full_masters.contains(it.key())));
}
//Show items which match with arg1
foreach(QTreeWidgetItem *qtwi, qtwi_list)
qtwi->setHidden(false);
}
+9
View File
@@ -22,8 +22,11 @@
#include "abstractelementpropertieseditorwidget.h"
#include <QHash>
#include <QDialog>
#include <QComboBox>
class QTreeWidgetItem;
class QCheckBox;
class Element;
class QMenu;
@@ -69,6 +72,7 @@ class LinkSingleElementWidget : public AbstractElementPropertiesEditorWidget
void setUpCompleter();
void clearTreeWidget();
void setUpHeaderLabels();
void updateItemsVisibility();
private slots:
void diagramWasRemovedFromProject();
@@ -85,6 +89,7 @@ class LinkSingleElementWidget : public AbstractElementPropertiesEditorWidget
void on_m_show_this_pb_clicked();
void on_m_search_field_textEdited(const QString &arg1);
void hideFullMastersToggled(bool checked);
private:
Ui::LinkSingleElementWidget *ui;
@@ -101,6 +106,10 @@ class LinkSingleElementWidget : public AbstractElementPropertiesEditorWidget
Element *m_showed_element = nullptr,
*m_element_to_link = nullptr;
QSet<QTreeWidgetItem*> m_full_masters;
int m_pending_group_index = -1;
QMenu *m_context_menu{nullptr};
QAction *m_link_action{nullptr},
*m_show_qtwi{nullptr},
+5 -15
View File
@@ -21,7 +21,7 @@
</property>
</widget>
</item>
<item row="3" column="0" colspan="3">
<item row="4" column="0" colspan="3">
<widget class="QTreeWidget" name="m_tree_widget">
<property name="indentation">
<number>5</number>
@@ -57,30 +57,20 @@
</property>
</widget>
</item>
<item row="2" column="0" colspan="3">
<item row="3" column="0" colspan="3">
<widget class="QLineEdit" name="m_search_field">
<property name="placeholderText">
<string>Recherche</string>
</property>
</widget>
</item>
<item row="4" column="0" colspan="3">
<widget class="QLabel" name="m_hidden_masters_label">
<item row="2" column="0" colspan="3">
<widget class="QCheckBox" name="m_hide_full_masters_cb">
<property name="text">
<string>Remarque : les éléments maîtres ayant atteint leur nombre maximal d'esclaves sont masqués.</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="font">
<font>
<italic>true</italic>
</font>
<string>Masquer les éléments maîtres pleins</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
+691 -109
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
@@ -140,14 +154,12 @@ void MasterPropertiesWidget::setElement(Element *element)
m_element->setHighlighted(false);
if (m_project)
disconnect(m_project, SIGNAL(diagramRemoved(QETProject*,Diagram*)),
this, SLOT(diagramWasdeletedFromProject()));
disconnect(m_project, &QETProject::diagramRemoved, this, &MasterPropertiesWidget::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*)),
this, SLOT(diagramWasdeletedFromProject()));
connect(m_project, &QETProject::diagramRemoved, this, &MasterPropertiesWidget::diagramWasdeletedFromProject);
}
else
m_project = nullptr;
@@ -157,7 +169,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 +188,8 @@ void MasterPropertiesWidget::apply()
{
if (QUndoCommand *undo = associatedUndo())
m_element -> diagram() -> undoStack().push(undo);
m_pending_group_indices.clear();
}
/**
@@ -200,6 +214,16 @@ void MasterPropertiesWidget::reset()
*/
QUndoCommand* MasterPropertiesWidget::associatedUndo() const
{
// PLC masters manage their slave links via the IO table (setElementData),
// not via the link tree widget. The link tree is always empty for PLC
// masters, so we must not create an unlinkAll command.
if (m_element &&
m_element->elementData().m_type == ElementData::Master &&
m_element->elementData().m_master_type == ElementData::PLC)
{
return nullptr;
}
QList <Element *> to_link;
QList <Element *> linked_ = m_element->linkedElements();
@@ -224,8 +248,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 +280,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
@@ -359,15 +307,24 @@ void MasterPropertiesWidget::on_link_button_clicked()
int max_slaves = max_slaves_variant.toInt();
int current_slaves = ui->m_link_tree_widget->topLevelItemCount();
// If a limit is set and reached
// If a limit is set and reached, say so but let the user decide.
// The limit records how many contacts the part is expected to
// carry; it is not a rule the drawing has to obey, and refusing
// the link obstructs drawing a schematic before the hardware has
// been chosen.
if (max_slaves != -1 && current_slaves >= max_slaves) {
// Show a message box with the actual window as parent to ensure it's on top
QMessageBox::warning(this->window(),
tr("Nombre maximal d'esclaves atteint."),
tr("Cet élément maître ne peut plus accepter aucun nouveau contact esclave, la limite fixée a été atteinte (Limite: %1).").arg(max_slaves));
return;
const auto answer = QMessageBox::warning(
this->window(),
tr("Nombre maximal d'esclaves atteint."),
tr("La limite fixée pour cet élément maître est atteinte (Limite: %1).\n\n"
"Voulez-vous tout de même lier ce contact esclave ?").arg(max_slaves),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::Yes);
if (answer != QMessageBox::Yes) {
return;
}
}
}
@@ -375,6 +332,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);
@@ -415,8 +404,7 @@ void MasterPropertiesWidget::showElementFromTWI(QTreeWidgetItem *qtwi, int colum
Q_UNUSED(column);
if (m_showed_element)
{
disconnect(m_showed_element, SIGNAL(destroyed()),
this, SLOT(showedElementWasDeleted()));
disconnect(m_showed_element, &QObject::destroyed, this, &MasterPropertiesWidget::showedElementWasDeleted);
m_showed_element -> setHighlighted(false);
}
if (m_element)
@@ -425,8 +413,7 @@ void MasterPropertiesWidget::showElementFromTWI(QTreeWidgetItem *qtwi, int colum
m_showed_element = m_qtwi_hash[qtwi];
m_showed_element->diagram()->showMe();
m_showed_element->setHighlighted(true);
connect(m_showed_element, SIGNAL(destroyed()),
this, SLOT(showedElementWasDeleted()));
connect(m_showed_element, &QObject::destroyed, this, &MasterPropertiesWidget::showedElementWasDeleted);
}
/**
@@ -449,7 +436,7 @@ void MasterPropertiesWidget::diagramWasdeletedFromProject()
// contains slave element linked to the edited element
// we must wait for this elements be unlinked,
// or else the linked list provides deleted elements.
QTimer::singleShot(10, this, SLOT(updateUi()));
QTimer::singleShot(10, this, &MasterPropertiesWidget::updateUi);
}
/**
@@ -497,3 +484,598 @@ 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(6);
m_plc_table->setHorizontalHeaderLabels({
tr("Type"), tr("Adresse"), tr("Fonction"),
tr("Commentaire"), tr("Réf. croisée"), tr("Bornes")
});
m_plc_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
m_plc_table->horizontalHeader()->setSectionsMovable(true);
m_plc_table->horizontalHeader()->resizeSection(0, 120);
m_plc_table->horizontalHeader()->resizeSection(1, 100);
m_plc_table->horizontalHeader()->resizeSection(2, 150);
m_plc_table->horizontalHeader()->resizeSection(3, 150);
m_plc_table->horizontalHeader()->resizeSection(4, 100);
m_plc_table->horizontalHeader()->resizeSection(5, 120);
// Restore a previously saved column layout, if any
auto *plc_hdr = m_plc_table->horizontalHeader();
QSettings plc_settings;
const QVariant plc_hdr_state = plc_settings.value(
QStringLiteral("masterpropertieswidget/plc-table-header-state"));
if (!plc_hdr_state.isNull())
plc_hdr->restoreState(plc_hdr_state.toByteArray());
// Persist the column layout whenever the user resizes or moves a column
auto save_plc_header_state = [plc_hdr]()
{
QSettings settings;
settings.setValue(QStringLiteral("masterpropertieswidget/plc-table-header-state"),
plc_hdr->saveState());
};
connect(plc_hdr, &QHeaderView::sectionResized,
this, save_plc_header_state);
connect(plc_hdr, &QHeaderView::sectionMoved,
this, save_plc_header_state);
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems);
m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_plc_table->setMinimumHeight(200);
plc_layout->addWidget(m_plc_table);
connect(m_plc_table, &QTableWidget::cellChanged, this, &MasterPropertiesWidget::plcIOCellChanged);
// Context menu for the PLC table
m_plc_table->setContextMenuPolicy(Qt::CustomContextMenu);
connect(m_plc_table, &QTableWidget::customContextMenuRequested,
this, &MasterPropertiesWidget::plcShowTableContextMenu);
// Ctrl+V shortcut for paste
auto *paste_shortcut = new QShortcut(QKeySequence::Paste, m_plc_table);
connect(paste_shortcut, &QShortcut::activated, this, &MasterPropertiesWidget::plcPasteFromClipboard);
}
ui->gridLayout->addWidget(m_plc_widget, 0, 0, 1, 5);
m_plc_widget->setVisible(true);
// Load PLC data into table (block signals to prevent cellChanged during population)
m_plc_updating = true;
m_plc_table->blockSignals(true);
// Clear existing rows
m_plc_table->setRowCount(0);
ElementData::PlcMasterData plc_data = m_element->elementData().plcMasterData();
m_plc_table->setRowCount(plc_data.ios.size());
for (int row = 0; row < plc_data.ios.size(); ++row) {
const ElementData::PlcIO &io = plc_data.ios.at(row);
// Type combo (read-only — type is defined in the editor)
auto *type_cb = new QComboBox(m_plc_table);
type_cb->setEnabled(false);
type_cb->setStyleSheet("QComboBox { background-color: #f0f0f0; }");
QStringList plc_types = ElementData::plcIOTypeList();
for (int t = 0; t < plc_types.size(); ++t) {
type_cb->addItem(plc_types.at(t), t);
}
type_cb->setCurrentIndex(static_cast<int>(io.type));
m_plc_table->setCellWidget(row, 0, type_cb);
// Address
auto *addr_item = new QTableWidgetItem(io.address);
m_plc_table->setItem(row, 1, addr_item);
// Function text
auto *func_item = new QTableWidgetItem(io.functionText);
m_plc_table->setItem(row, 2, func_item);
// Comment
auto *comment_item = new QTableWidgetItem(io.comment);
m_plc_table->setItem(row, 3, comment_item);
// CrossRef (read-only)
auto *crossref_item = new QTableWidgetItem(io.crossRef);
crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable);
m_plc_table->setItem(row, 4, crossref_item);
// Anschlüsse (read-only)
auto *terminals_item = new QTableWidgetItem(io.terminals.join(QStringLiteral(", ")));
terminals_item->setFlags(terminals_item->flags() & ~Qt::ItemIsEditable);
m_plc_table->setItem(row, 5, terminals_item);
}
m_plc_table->blockSignals(false);
m_plc_updating = false;
} else {
// Hide PLC widget if it was shown before
if (m_plc_widget)
m_plc_widget->setVisible(false);
// Restore normal widget size policies
for (QWidget *w : {static_cast<QWidget*>(ui->m_free_tree_widget),
static_cast<QWidget*>(ui->m_link_tree_widget),
static_cast<QWidget*>(ui->label),
static_cast<QWidget*>(ui->label_2),
static_cast<QWidget*>(ui->link_button),
static_cast<QWidget*>(ui->unlink_button)}) {
w->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
w->setMinimumSize(0, 0);
w->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
}
for (int col = 0; col < ui->gridLayout->columnCount(); ++col)
ui->gridLayout->setColumnStretch(col, 0);
if (Q_UNLIKELY(!m_project))
return;
ElementProvider elmt_prov(m_project);
QSettings settings;
//Build the list of free available element
QList <QTreeWidgetItem *> items_list;
for(const auto &elmt : elmt_prov.freeElement(ElementData::Slave))
{
// Filter out PLC-Slave elements — they should only link to PLC-Master
if (elmt->elementData().m_slave_type == ElementData::PLCSlave)
continue;
QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_free_tree_widget);
qtwi->setIcon(0, elmt->pixmap());
if(settings.value("genericpanel/folio", false).toBool())
{
autonum::sequentialNumbers seq;
QString F =autonum::AssignVariables::formulaToLabel(
elmt->diagram()->border_and_titleblock.folio(),
seq,
elmt->diagram(),
elmt);
qtwi->setText(1, F);
}
else
{
qtwi->setText(1, QString::number(
elmt->diagram()->folioIndex()
+ 1));
}
qtwi->setText(2, elmt->diagram()->title());
qtwi->setText(4, elmt->diagram()->convertPosition(
elmt->scenePos()).toString());
items_list.append(qtwi);
m_qtwi_hash.insert(qtwi, elmt);
}
ui->m_free_tree_widget->addTopLevelItems(items_list);
items_list.clear();
//Build the list of already linked element
const QList<Element *> link_list = m_element->linkedElements();
for(Element *elmt : link_list)
{
QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_link_tree_widget);
qtwi->setIcon(0, elmt->pixmap());
if(settings.value("genericpanel/folio", false).toBool())
{
autonum::sequentialNumbers seq;
QString F =autonum::AssignVariables::formulaToLabel(
elmt->diagram()->border_and_titleblock.folio(),
seq,
elmt->diagram(),
elmt);
qtwi->setText(1, F);
}
else
{
qtwi->setText(1, QString::number(
elmt->diagram()->folioIndex()
+ 1));
}
qtwi->setText(2, elmt->diagram()->title());
qtwi->setText(3, elmt->diagram()->convertPosition(
elmt->scenePos()).toString());
items_list.append(qtwi);
m_qtwi_hash.insert(qtwi, elmt);
}
if(items_list.count())
ui->m_link_tree_widget->addTopLevelItems(items_list);
QVariant v = settings.value("link-element-widget/master-state");
if(!v.isNull())
{
ui->m_free_tree_widget->header()->restoreState(v.toByteArray());
ui->m_link_tree_widget->header()->restoreState(v.toByteArray());
}
}
}
// ============================================================================
// PLC IO Table methods
// ============================================================================
/**
* @brief MasterPropertiesWidget::plcPasteFromClipboard
* Paste IO data from clipboard.
*
* Two modes:
* 1. Single-column data (no tabs, e.g. copied vertically from A1:A6):
* Values are pasted vertically down the current column.
* 2. Multi-column data (tab-separated, e.g. copied from a row in Excel):
* Each line becomes a separate IO row.
*/
void MasterPropertiesWidget::plcPasteFromClipboard()
{
if (!m_plc_table || !m_element)
return;
QString clipboard_text = QApplication::clipboard()->text();
if (clipboard_text.isEmpty())
return;
QStringList lines = clipboard_text.split('\n', Qt::SkipEmptyParts);
if (lines.isEmpty())
return;
bool has_tabs = false;
for (const QString &line : lines) {
if (line.contains('\t')) {
has_tabs = true;
break;
}
}
m_plc_updating = true;
if (!has_tabs) {
// Vertical paste: values go down the same column, within existing rows only
int target_col = m_plc_table->currentColumn();
if (target_col < 0) target_col = 0;
int target_row = m_plc_table->currentRow();
if (target_row < 0) target_row = 0;
int max_rows = m_plc_table->rowCount();
for (int i = 0; i < lines.size(); ++i) {
int row = target_row + i;
if (row >= max_rows) break;
setCellFromValue(row, target_col, lines.at(i).trimmed());
}
} else {
// Horizontal paste: each line is a separate IO row, within existing rows only
int target_row = m_plc_table->currentRow();
if (target_row < 0) target_row = 0;
int max_rows = m_plc_table->rowCount();
for (int i = 0; i < lines.size(); ++i) {
int row = target_row + i;
if (row >= max_rows) break;
QStringList cells = lines.at(i).split('\t');
for (int c = 0; c < cells.size(); ++c) {
if (c > 6) break;
setCellFromValue(row, c, cells.at(c).trimmed());
}
}
}
m_plc_updating = false;
plcUpdateDisplaySettings();
}
/**
* @brief MasterPropertiesWidget::setCellFromValue
* Set a single table cell value, respecting the column widget type.
*/
void MasterPropertiesWidget::setCellFromValue(int row, int col, const QString &val)
{
if (!m_plc_table || row < 0 || col < 0 || col > 6)
return;
if (col == 0) {
// Type combo
auto *type_cb = qobject_cast<QComboBox*>(m_plc_table->cellWidget(row, col));
if (!type_cb) {
type_cb = new QComboBox(m_plc_table);
QStringList plc_types = ElementData::plcIOTypeList();
for (int t = 0; t < plc_types.size(); ++t)
type_cb->addItem(plc_types.at(t), t);
m_plc_table->setCellWidget(row, col, type_cb);
connect(type_cb, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, [this, row](int) { plcIOCellChanged(row, 0); });
}
if (!val.isEmpty()) {
QStringList plc_types = ElementData::plcIOTypeList();
for (int t = 0; t < plc_types.size(); ++t) {
if (plc_types.at(t).compare(val, Qt::CaseInsensitive) == 0) {
type_cb->setCurrentIndex(t);
return;
}
}
}
}
else if (col == 5) {
// Anschlüsse (read-only)
auto *item = new QTableWidgetItem(val);
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
m_plc_table->setItem(row, col, item);
}
else {
// Text columns: Address, Function, Comment
m_plc_table->setItem(row, col, new QTableWidgetItem(val));
}
}
/**
* @brief MasterPropertiesWidget::plcAddRow
* Add a new empty row to the PLC IO table
*/
void MasterPropertiesWidget::plcAddRow()
{
if (!m_plc_table)
return;
int row = m_plc_table->rowCount();
m_plc_table->insertRow(row);
// Type combo
auto *type_cb = new QComboBox(m_plc_table);
QStringList plc_types = ElementData::plcIOTypeList();
for (int t = 0; t < plc_types.size(); ++t) {
type_cb->addItem(plc_types.at(t), t);
}
m_plc_table->setCellWidget(row, 0, type_cb);
connect(type_cb, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, [this, row](int) { plcIOCellChanged(row, 0); });
m_plc_table->setItem(row, 1, new QTableWidgetItem());
m_plc_table->setItem(row, 2, new QTableWidgetItem());
m_plc_table->setItem(row, 3, new QTableWidgetItem());
m_plc_table->setItem(row, 4, new QTableWidgetItem());
// Anschlüsse (read-only)
auto *terminals_item = new QTableWidgetItem();
terminals_item->setFlags(terminals_item->flags() & ~Qt::ItemIsEditable);
m_plc_table->setItem(row, 5, terminals_item);
}
/**
* @brief MasterPropertiesWidget::plcRemoveRow
* Remove selected rows from the PLC IO table
*/
void MasterPropertiesWidget::plcRemoveRow()
{
if (!m_plc_table)
return;
QModelIndexList selected = m_plc_table->selectionModel()->selectedRows();
if (selected.isEmpty())
return;
m_plc_updating = true;
// Remove from bottom to top to preserve indices
std::sort(selected.begin(), selected.end(),
[](const QModelIndex &a, const QModelIndex &b) { return a.row() > b.row(); });
for (const QModelIndex &idx : selected) {
m_plc_table->removeRow(idx.row());
}
m_plc_updating = false;
plcUpdateDisplaySettings();
}
/**
* @brief MasterPropertiesWidget::plcMoveRowUp
* Move selected row up
*/
void MasterPropertiesWidget::plcMoveRowUp()
{
if (!m_plc_table)
return;
int row = m_plc_table->currentRow();
if (row <= 0)
return;
m_plc_updating = true;
// Swap with row above
for (int col = 0; col < m_plc_table->columnCount(); ++col) {
QWidget *w1 = m_plc_table->cellWidget(row, col);
QWidget *w2 = m_plc_table->cellWidget(row - 1, col);
m_plc_table->setCellWidget(row, col, w2);
m_plc_table->setCellWidget(row - 1, col, w1);
QTableWidgetItem *i1 = m_plc_table->item(row, col);
QTableWidgetItem *i2 = m_plc_table->item(row - 1, col);
m_plc_table->setItem(row, col, i2);
m_plc_table->setItem(row - 1, col, i1);
}
m_plc_table->setCurrentCell(row - 1, m_plc_table->currentColumn());
m_plc_updating = false;
plcUpdateDisplaySettings();
}
/**
* @brief MasterPropertiesWidget::plcMoveRowDown
* Move selected row down
*/
void MasterPropertiesWidget::plcMoveRowDown()
{
if (!m_plc_table)
return;
int row = m_plc_table->currentRow();
if (row < 0 || row >= m_plc_table->rowCount() - 1)
return;
m_plc_updating = true;
// Swap with row below
for (int col = 0; col < m_plc_table->columnCount(); ++col) {
QWidget *w1 = m_plc_table->cellWidget(row, col);
QWidget *w2 = m_plc_table->cellWidget(row + 1, col);
m_plc_table->setCellWidget(row, col, w2);
m_plc_table->setCellWidget(row + 1, col, w1);
QTableWidgetItem *i1 = m_plc_table->item(row, col);
QTableWidgetItem *i2 = m_plc_table->item(row + 1, col);
m_plc_table->setItem(row, col, i2);
m_plc_table->setItem(row + 1, col, i1);
}
m_plc_table->setCurrentCell(row + 1, m_plc_table->currentColumn());
m_plc_updating = false;
plcUpdateDisplaySettings();
}
/**
* @brief MasterPropertiesWidget::plcIOCellChanged
* Called when a cell in the PLC IO table changes
*/
void MasterPropertiesWidget::plcIOCellChanged(int row, int column)
{
Q_UNUSED(row)
Q_UNUSED(column)
if (m_plc_updating || !m_element || !m_plc_table)
return;
// Save immediately
plcUpdateDisplaySettings();
}
/**
* @brief MasterPropertiesWidget::plcUpdateDisplaySettings
* Update the IO data from the table (display settings are managed by the editor only)
*/
void MasterPropertiesWidget::plcUpdateDisplaySettings()
{
if (m_plc_updating || !m_element || !m_plc_table)
return;
m_plc_updating = true;
// Preserve existing display settings
ElementData ed = m_element->elementData();
const auto orig_plc = ed.plcMasterData();
ElementData::PlcMasterData plc_data = orig_plc;
plc_data.ios.clear();
// Read IOs from table, preserving terminal data from original IOs by row index
for (int row = 0; row < m_plc_table->rowCount(); ++row) {
ElementData::PlcIO io;
auto *type_cb = qobject_cast<QComboBox *>(m_plc_table->cellWidget(row, 0));
if (type_cb)
io.type = static_cast<ElementData::PlcIOType>(type_cb->currentData().toInt());
auto *addr_item = m_plc_table->item(row, 1);
if (addr_item)
io.address = addr_item->text();
auto *func_item = m_plc_table->item(row, 2);
if (func_item)
io.functionText = func_item->text();
auto *comment_item = m_plc_table->item(row, 3);
if (comment_item)
io.comment = comment_item->text();
auto *crossref_item = m_plc_table->item(row, 4);
if (crossref_item)
io.crossRef = crossref_item->text();
// Preserve terminal data by matching row index directly.
// This avoids address-based lookup which fails when all addresses
// are empty (common in PLC masters) — a hash collision would cause
// only the last IO's terminals to be used for all rows.
if (row < orig_plc.ios.size()) {
const auto &orig_io = orig_plc.ios.at(row);
io.terminalCount = orig_io.terminalCount;
io.terminals = orig_io.terminals;
}
plc_data.ios.append(io);
}
ed.setPlcMasterData(plc_data);
m_element->setElementData(ed);
// Trigger update of the cross ref item
if (m_element->scene())
m_element->update();
m_plc_updating = false;
}
/**
* @brief MasterPropertiesWidget::plcShowTableContextMenu
* Show context menu for the PLC IO table
*/
void MasterPropertiesWidget::plcShowTableContextMenu(const QPoint &pos)
{
Q_UNUSED(pos)
if (!m_plc_table)
return;
QMenu menu;
menu.addAction(tr("Coller depuis le presse-papiers"), this, &MasterPropertiesWidget::plcPasteFromClipboard);
menu.exec(m_plc_table->mapToGlobal(pos));
}
+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
+2 -2
View File
@@ -136,7 +136,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="go-up" resource="../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-up.png</normaloff>:/ico/16x16/go-up.png</iconset>
</property>
</widget>
@@ -150,7 +150,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="go-down" resource="../../qelectrotech.qrc">
<normaloff>:/ico/16x16/go-down.png</normaloff>:/ico/16x16/go-down.png</iconset>
</property>
</widget>
+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
+181
View File
@@ -0,0 +1,181 @@
/*
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 "pdfpagesdialog.h"
// Whole file is a no-op unless QtPdf is available (see pdfpagesdialog.h).
#ifdef QET_HAS_QTPDF
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLabel>
#include <QSpinBox>
#include <QComboBox>
#include <QPushButton>
#include <QDialogButtonBox>
#include <QPdfDocument>
#include <QScrollArea>
#include <QPainter>
// Preview rendering DPI for the thumbnail in the dialog.
// Fixed at 96 DPI to keep the preview fast and compact regardless
// of the final render DPI selected by the user.
static const int PREVIEW_DPI = 96;
static const int PREVIEW_WIDTH = 400;
/**
@brief PdfPagesDialog::PdfPagesDialog
Constructor
@param document the loaded PDF document to preview pages from
@param parent parent widget
*/
PdfPagesDialog::PdfPagesDialog(QPdfDocument &document, QWidget *parent)
: QDialog(parent),
m_document(document)
{
setWindowTitle(tr("Sélectionner une page"));
setMinimumWidth(500);
setMinimumHeight(500);
int pageCount = document.pageCount();
// Main layout
QVBoxLayout *main_layout = new QVBoxLayout(this);
// Info label: "Ce document PDF contient X page(s)"
m_info_label = new QLabel(
tr("Ce document PDF contient %n page(s)", "", pageCount),
this
);
m_info_label->setWordWrap(true);
main_layout->addWidget(m_info_label);
// Page selection layout
QHBoxLayout *page_layout = new QHBoxLayout();
QLabel *select_label = new QLabel(tr("Page à importer :"), this);
page_layout->addWidget(select_label);
// Spinbox: min=1, max=pageCount
m_page_spinbox = new QSpinBox(this);
m_page_spinbox->setMinimum(1);
m_page_spinbox->setMaximum(pageCount);
m_page_spinbox->setValue(1);
page_layout->addWidget(m_page_spinbox);
page_layout->addStretch();
// DPI selection
QLabel *dpi_label = new QLabel(tr("Résolution :"), this);
page_layout->addWidget(dpi_label);
m_dpi_combo = new QComboBox(this);
m_dpi_combo->addItem(tr("150 DPI (écran)"), 150);
m_dpi_combo->addItem(tr("300 DPI (impression)"), 300);
m_dpi_combo->addItem(tr("600 DPI (haute qualité)"), 600);
m_dpi_combo->setCurrentIndex(0);
page_layout->addWidget(m_dpi_combo);
main_layout->addLayout(page_layout);
// Preview area
QLabel *preview_title = new QLabel(tr("Aperçu :"), this);
main_layout->addWidget(preview_title);
QScrollArea *scroll_area = new QScrollArea(this);
scroll_area->setWidgetResizable(true);
scroll_area->setAlignment(Qt::AlignCenter);
scroll_area->setMinimumHeight(250);
m_preview_label = new QLabel(this);
m_preview_label->setAlignment(Qt::AlignCenter);
m_preview_label->setMinimumSize(200, 200);
m_preview_label->setStyleSheet("QLabel { background-color: white; border: 1px solid #cccccc; }");
scroll_area->setWidget(m_preview_label);
main_layout->addWidget(scroll_area, 1);
// OK/Cancel buttons
QDialogButtonBox *button_box = new QDialogButtonBox(
QDialogButtonBox::Ok | QDialogButtonBox::Cancel,
this
);
connect(button_box, &QDialogButtonBox::accepted, this, &QDialog::accept);
connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
main_layout->addWidget(button_box);
// Update preview when spinbox or DPI changes
connect(m_page_spinbox, QOverload<int>::of(&QSpinBox::valueChanged),
this, &PdfPagesDialog::updatePreview);
connect(m_dpi_combo, QOverload<int>::of(&QComboBox::currentIndexChanged),
this, &PdfPagesDialog::updatePreview);
// Render initial preview
updatePreview();
}
/**
@brief PdfPagesDialog::selectedPage
@return the selected page number (1-based)
*/
int PdfPagesDialog::selectedPage() const
{
return m_page_spinbox->value();
}
/**
@brief PdfPagesDialog::selectedDpi
@return the selected DPI value (150, 300, or 600)
*/
int PdfPagesDialog::selectedDpi() const
{
return m_dpi_combo->currentData().toInt();
}
/**
@brief PdfPagesDialog::updatePreview
Renders the currently selected page as a thumbnail and displays it
in the preview label. The preview size scales with the selected DPI
to give a visual hint of the output quality.
*/
void PdfPagesDialog::updatePreview()
{
int pageIndex = m_page_spinbox->value() - 1; // Convert to 0-based
// Calculate preview size: fixed width, proportional height
QSizeF pageSize = m_document.pagePointSize(pageIndex);
if (pageSize.isEmpty()) return;
qreal ratio = pageSize.height() / pageSize.width();
int previewHeight = qRound(PREVIEW_WIDTH * ratio);
// Render the page to QImage at preview DPI
QImage preview = m_document.render(pageIndex, QSize(PREVIEW_WIDTH, previewHeight));
if (preview.isNull()) return;
// Fill white background to handle transparent PDFs
QImage background(preview.size(), QImage::Format_ARGB32_Premultiplied);
background.fill(Qt::white);
QPainter painter(&background);
painter.drawImage(0, 0, preview);
painter.end();
m_preview_label->setPixmap(QPixmap::fromImage(background).scaled(
PREVIEW_WIDTH, previewHeight,
Qt::KeepAspectRatio, Qt::SmoothTransformation));
}
#endif // QET_HAS_QTPDF
+60
View File
@@ -0,0 +1,60 @@
/*
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 PDFPAGESDIALOG_H
#define PDFPAGESDIALOG_H
// Needs the QtPdf module (QET_HAS_QTPDF, set by CMake only when QtPdf
// is present AND Qt >= 6.4, for QPdfDocument::pagePointSize()).
#ifdef QET_HAS_QTPDF
#include <QDialog>
class QSpinBox;
class QComboBox;
class QLabel;
class QPdfDocument;
/**
@brief The PdfPagesDialog class
Dialog for selecting a page from a PDF document.
Displays the total page count, a live preview of the selected page,
DPI options (150/300/600), and allows the user to choose which page to import.
*/
class PdfPagesDialog : public QDialog
{
Q_OBJECT
public:
explicit PdfPagesDialog(QPdfDocument &document, QWidget *parent = nullptr);
int selectedPage() const;
int selectedDpi() const;
private slots:
void updatePreview();
private:
QPdfDocument &m_document;
QLabel *m_info_label;
QLabel *m_preview_label;
QSpinBox *m_page_spinbox;
QComboBox *m_dpi_combo;
};
#endif // QET_HAS_QTPDF
#endif // PDFPAGESDIALOG_H
+430
View File
@@ -0,0 +1,430 @@
/*
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 <QCheckBox>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
#include <QMenu>
#include <QAction>
#include <QFont>
#include <QTimer>
#include <QSettings>
#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: Hide linked elements checkbox
m_hide_linked_cb = new QCheckBox(tr("Masquer les éléments connectés"), this);
main_layout->addWidget(m_hide_linked_cb, 1, 0, 1, 3);
// Row 2: Search field
m_search_field = new QLineEdit(this);
m_search_field->setPlaceholderText(tr("Recherche"));
main_layout->addWidget(m_search_field, 2, 0, 1, 3);
// Row 3: Tree widget
m_tree_widget = new QTreeWidget(this);
m_tree_widget->setHeaderLabels({
tr("Label"), tr("Type"), tr("Adresse"),
tr("Fonction"), tr("Commentaire"), tr("Bornes")
});
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, 3, 0, 1, 3);
// Row 4: 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, 4, 0, 1, 3);
main_layout->setRowStretch(3, 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);
connect(m_hide_linked_cb, &QCheckBox::toggled,
this, &PlcLinkWidget::on_m_hide_linked_cb_toggled);
QSettings settings;
m_hide_linked_cb->setChecked(
settings.value(QStringLiteral("plclinkwidget/hideLinked"), false).toBool());
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();
m_linked_children.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
bool all_children_hidden = true;
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 names
child_item->setText(5, io.terminals.join(QStringLiteral(", ")));
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
if (used_io_indices.contains(i)) {
m_linked_children.insert(child_item);
if (m_hide_linked_cb->isChecked()) {
child_item->setHidden(true);
} else {
all_children_hidden = false;
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);
}
} else {
all_children_hidden = false;
}
}
// If checkbox is on and every child is linked (hidden), hide the master too
if (m_hide_linked_cb->isChecked() && all_children_hidden) {
parent_item->setHidden(true);
}
}
}
bool PlcLinkWidget::isChildLinked(QTreeWidgetItem *child) const
{
return m_linked_children.contains(child);
}
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;
}
}
}
bool hidden = m_hide_linked_cb->isChecked() && isChildLinked(child);
child->setHidden(!match || hidden);
if (match && !hidden) any_child_visible = true;
}
// Also check if parent label matches
if (!text.isEmpty() && parent->text(0).contains(text, Qt::CaseInsensitive)) {
for (int j = 0; j < parent->childCount(); ++j) {
QTreeWidgetItem *child = parent->child(j);
bool hidden = m_hide_linked_cb->isChecked() && isChildLinked(child);
if (!hidden) {
any_child_visible = true;
child->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);
// Store the current group index so makeLink() can
// restore PLC variables and terminal labels on undo.
for (Element *elmt : m_element->linkedElements())
{
if (elmt->linkType() == Element::Master)
{
int idx = elmt->groupIndexForElement(m_element);
if (idx >= 0)
undo->setGroupIndex(idx);
break;
}
}
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);
}
void PlcLinkWidget::on_m_hide_linked_cb_toggled(bool checked)
{
QSettings settings;
settings.setValue(QStringLiteral("plclinkwidget/hideLinked"), checked);
buildPlcTree();
}
+92
View File
@@ -0,0 +1,92 @@
/*
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 QCheckBox;
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();
bool isChildLinked(QTreeWidgetItem *child) const;
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();
void on_m_hide_linked_cb_toggled(bool checked);
private:
QLabel *m_label{nullptr};
QPushButton *m_unlink_pb{nullptr};
QPushButton *m_show_this_pb{nullptr};
QCheckBox *m_hide_linked_cb{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;
QSet<QTreeWidgetItem*> m_linked_children;
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>
+10 -4
View File
@@ -44,9 +44,9 @@ ProjectPropertiesDialog::ProjectPropertiesDialog(QETProject *project, QWidget *p
m_properties_dialog->addPage(new TerminalStripProjectConfigPage { project, parent });
connect(projectAutoNumConfigPage,SIGNAL(setAutoNum(QString)),newDiagramPage,SLOT(setFolioAutonum(QString)));
connect(projectAutoNumConfigPage,SIGNAL(saveCurrentTbp()),newDiagramPage,SLOT(saveCurrentTbp()));
connect(projectAutoNumConfigPage,SIGNAL(loadSavedTbp()),newDiagramPage,SLOT(loadSavedTbp()));
connect(projectAutoNumConfigPage, qOverload<QString>(&ProjectAutoNumConfigPage::setAutoNum), newDiagramPage, &NewDiagramPage::setFolioAutonum);
connect(projectAutoNumConfigPage, &ProjectAutoNumConfigPage::saveCurrentTbp, newDiagramPage, &NewDiagramPage::saveCurrentTbp);
connect(projectAutoNumConfigPage, &ProjectAutoNumConfigPage::loadSavedTbp, newDiagramPage, &NewDiagramPage::loadSavedTbp);
}
/**
@@ -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();
}
@@ -20,6 +20,7 @@
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
#include "../diagram.h"
#include "../lastusedstyle.h"
#include "../qetgraphicsitem/qetshapeitem.h"
#include "../ui_shapegraphicsitempropertieswidget.h"
@@ -72,7 +73,8 @@ void ShapeGraphicsItemPropertiesWidget::setItem(QetShapeItem *shape)
}
m_shape = shape;
ui->m_close_polygon->setVisible(m_shape->shapeType() == QetShapeItem::Polygon);
ui->m_close_polygon->setVisible(m_shape->shapeType() == QetShapeItem::Polygon
|| m_shape->shapeType() == QetShapeItem::Path);
ui->m_filling_gb->setHidden(m_shape->shapeType() == QetShapeItem::Line);
updateUi();
@@ -163,6 +165,58 @@ QUndoCommand* ShapeGraphicsItemPropertiesWidget::associatedUndo() const
{
QPropertyUndoCommand *undo = nullptr;
//Geometry
const bool hasGeometry = (m_shape->shapeType() == QetShapeItem::Line
|| m_shape->shapeType() == QetShapeItem::Rectangle
|| m_shape->shapeType() == QetShapeItem::Ellipse);
if (hasGeometry)
{
if (m_shape->shapeType() == QetShapeItem::Line)
{
const QLineF old_line = m_shape->line();
QLineF new_line = old_line;
new_line.setLength(ui->m_geom_dim1_dsb->value());
if (new_line != old_line)
{
undo = new QPropertyUndoCommand(m_shape, "line", old_line, new_line);
undo->setText(tr("Modifier la longueur d'une ligne"));
}
}
else
{
const QRectF old_rect = m_shape->rect().normalized();
const bool isEllipse = (m_shape->shapeType() == QetShapeItem::Ellipse);
const qreal new_width = isEllipse ? ui->m_geom_dim1_dsb->value() * 2.0 : ui->m_geom_dim1_dsb->value();
const qreal new_height = isEllipse ? ui->m_geom_dim2_dsb->value() * 2.0 : ui->m_geom_dim2_dsb->value();
// Anchored on the shape's own current top-left, not
// re-centered -- matches how the resize handle itself
// behaves (the opposite corner stays put while the
// dragged one moves), so typing a width/height here
// and dragging a handle agree on what "resizing" means.
const QRectF new_rect(old_rect.topLeft(), QSizeF(new_width, new_height));
if (new_rect != old_rect)
{
undo = new QPropertyUndoCommand(m_shape, "rect", old_rect, new_rect);
undo->setText(tr("Modifier la taille d'une forme"));
}
}
const qreal old_angle = m_shape->rotation();
const qreal new_angle = ui->m_geom_angle_dsb->value();
if (qAbs(old_angle - new_angle) > 0.05)
{
if (undo)
new QPropertyUndoCommand(m_shape, "rotation", old_angle, new_angle, undo);
else
{
undo = new QPropertyUndoCommand(m_shape, "rotation", old_angle, new_angle);
undo->setText(tr("Modifier l'angle d'une forme"));
}
}
}
QPen old_pen = m_shape->pen();
QPen new_pen = old_pen;
@@ -180,6 +234,7 @@ QUndoCommand* ShapeGraphicsItemPropertiesWidget::associatedUndo() const
{
undo = new QPropertyUndoCommand(m_shape, "pen", old_pen, new_pen);
undo->setText(tr("Modifier le trait d'une forme"));
LastUsedStyle::setShapePen(new_pen);
}
QBrush old_brush = m_shape->brush();
@@ -196,6 +251,7 @@ QUndoCommand* ShapeGraphicsItemPropertiesWidget::associatedUndo() const
undo = new QPropertyUndoCommand(m_shape, "brush", old_brush, new_brush);
undo->setText(tr("Modifier le remplissage d'une forme"));
}
LastUsedStyle::setShapeBrush(new_brush);
}
if (ui->m_close_polygon->isChecked() != m_shape->isClosed())
@@ -321,6 +377,7 @@ QUndoCommand* ShapeGraphicsItemPropertiesWidget::associatedUndo() const
if (new_pen != old_pen) {
new QPropertyUndoCommand(m_shape, "pen", old_pen, new_pen, undo);
LastUsedStyle::setShapePen(new_pen);
}
QBrush old_brush = m_shape->brush();
@@ -330,6 +387,7 @@ QUndoCommand* ShapeGraphicsItemPropertiesWidget::associatedUndo() const
if (new_brush != old_brush) {
new QPropertyUndoCommand(m_shape, "brush", old_brush, new_brush, undo);
LastUsedStyle::setShapeBrush(new_brush);
}
if (ui->m_close_polygon->isChecked() != m_shape->isClosed()) {
@@ -361,6 +419,35 @@ void ShapeGraphicsItemPropertiesWidget::updateUi()
if (m_shape)
{
//Geometry
const bool hasGeometry = (m_shape->shapeType() == QetShapeItem::Line
|| m_shape->shapeType() == QetShapeItem::Rectangle
|| m_shape->shapeType() == QetShapeItem::Ellipse);
ui->m_geometry_gb->setVisible(hasGeometry);
if (hasGeometry)
{
ui->m_geom_angle_dsb->setValue(m_shape->rotation());
if (m_shape->shapeType() == QetShapeItem::Line)
{
ui->m_geom_dim1_label->setText(tr("Longueur"));
ui->m_geom_dim1_dsb->setValue(m_shape->line().length());
ui->m_geom_dim2_label->setVisible(false);
ui->m_geom_dim2_dsb->setVisible(false);
}
else
{
const QRectF r = m_shape->rect().normalized();
const bool isEllipse = (m_shape->shapeType() == QetShapeItem::Ellipse);
ui->m_geom_dim1_label->setText(isEllipse ? tr("Rayon X") : tr("Largeur"));
ui->m_geom_dim2_label->setText(isEllipse ? tr("Rayon Y") : tr("Hauteur"));
ui->m_geom_dim1_dsb->setValue(isEllipse ? r.width() / 2.0 : r.width());
ui->m_geom_dim2_dsb->setValue(isEllipse ? r.height() / 2.0 : r.height());
ui->m_geom_dim2_label->setVisible(true);
ui->m_geom_dim2_dsb->setVisible(true);
}
}
//Pen
ui->m_style_cb->setCurrentIndex(static_cast<int>(m_shape->pen().style()) - 1);
ui->m_size_dsb ->setValue(m_shape->pen().widthF());
@@ -368,7 +455,7 @@ void ShapeGraphicsItemPropertiesWidget::updateUi()
ui->m_color_kpb->setColor(m_shape->pen().color());
//Brush
if (m_shape->shapeType() == QetShapeItem::Polygon)
if (m_shape->shapeType() == QetShapeItem::Polygon || m_shape->shapeType() == QetShapeItem::Path)
ui->m_filling_gb->setVisible(m_shape->isClosed());
ui->m_brush_style_cb->setCurrentIndex(static_cast<int>(m_shape->brush().style()));
@@ -379,6 +466,7 @@ void ShapeGraphicsItemPropertiesWidget::updateUi()
}
else if (m_shapes_list.size() >= 2)
{
ui->m_geometry_gb->setVisible(false);
ui->m_close_polygon->setHidden(true);
bool same = true;
//Pen
@@ -472,6 +560,15 @@ void ShapeGraphicsItemPropertiesWidget::setUpEditConnection()
if (m_shape || !m_shapes_list.isEmpty())
{
m_edit_connection << connect (ui->m_geom_dim1_dsb, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
this, &ShapeGraphicsItemPropertiesWidget::apply);
m_edit_connection << connect (ui->m_geom_dim2_dsb, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
this, &ShapeGraphicsItemPropertiesWidget::apply);
m_edit_connection << connect (ui->m_geom_angle_dsb, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
this, &ShapeGraphicsItemPropertiesWidget::apply);
m_edit_connection << connect (ui->m_style_cb, QOverload<int>::of(&QComboBox::activated),
this, &ShapeGraphicsItemPropertiesWidget::apply);
@@ -490,21 +587,30 @@ void ShapeGraphicsItemPropertiesWidget::setUpEditConnection()
m_edit_connection << connect (ui->m_close_polygon, &QCheckBox::clicked,
this, &ShapeGraphicsItemPropertiesWidget::apply);
m_edit_connection << connect (m_shape, &QetShapeItem::penChanged,
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
if (m_shape)
{
m_edit_connection << connect (m_shape, &QetShapeItem::penChanged,
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
m_edit_connection << connect (m_shape, &QetShapeItem::closeChanged,
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
m_edit_connection << connect (m_shape, &QetShapeItem::closeChanged,
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
m_edit_connection << connect (m_shape, &QetShapeItem::brushChanged,
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
m_edit_connection << connect (m_shape, &QetShapeItem::brushChanged,
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
m_edit_connection << connect (m_shape, &QetShapeItem::geometryChanged,
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
m_edit_connection << connect (m_shape, &QetShapeItem::transformChanged,
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
}
}
}
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();
@@ -14,6 +14,78 @@
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QGroupBox" name="m_geometry_gb">
<property name="title">
<string>Géométrie</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="m_geom_dim1_label">
<property name="text">
<string>Largeur</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QDoubleSpinBox" name="m_geom_dim1_dsb">
<property name="decimals">
<number>2</number>
</property>
<property name="minimum">
<double>0.010000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="m_geom_dim2_label">
<property name="text">
<string>Hauteur</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="m_geom_dim2_dsb">
<property name="decimals">
<number>2</number>
</property>
<property name="minimum">
<double>0.010000000000000</double>
</property>
<property name="maximum">
<double>100000.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="m_geom_angle_label">
<property name="text">
<string>Angle</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QDoubleSpinBox" name="m_geom_angle_dsb">
<property name="decimals">
<number>1</number>
</property>
<property name="minimum">
<double>-360.000000000000000</double>
</property>
<property name="maximum">
<double>360.000000000000000</double>
</property>
<property name="suffix">
<string> °</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
@@ -230,7 +302,7 @@
<item>
<widget class="QCheckBox" name="m_close_polygon">
<property name="text">
<string>Polygone fermé</string>
<string>Forme fermée</string>
</property>
</widget>
</item>
@@ -257,6 +329,9 @@
</customwidget>
</customwidgets>
<tabstops>
<tabstop>m_geom_dim1_dsb</tabstop>
<tabstop>m_geom_dim2_dsb</tabstop>
<tabstop>m_geom_angle_dsb</tabstop>
<tabstop>m_style_cb</tabstop>
<tabstop>m_size_dsb</tabstop>
<tabstop>m_color_kpb</tabstop>
+96 -7
View File
@@ -1,22 +1,72 @@
#include "terminalnumberingdialog.h"
#include "ui_terminalnumberingdialog.h"
#include "../qet.h"
#include "../qetproject.h"
#include "../diagram.h"
#include "../qetgraphicsitem/element.h"
#include "../undocommand/changeelementinformationcommand.h"
#include <QUndoCommand>
#include <QCheckBox>
#include <QVBoxLayout>
#include <QSet>
#include <algorithm>
/**
* @brief TerminalNumberingDialog::TerminalNumberingDialog
* Constructor
* @param parent
* @param project Pointer to the current project (used to populate terminal strips)
*/
TerminalNumberingDialog::TerminalNumberingDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::TerminalNumberingDialog)
TerminalNumberingDialog::TerminalNumberingDialog(QWidget *parent, QETProject *project) :
QDialog(parent),
ui(new Ui::TerminalNumberingDialog)
{
ui->setupUi(this);
// Connect radio button to enable/disable the "also number letters" checkbox
connect(ui->rb_type_alpha, &QRadioButton::toggled, ui->cb_also_alpha, &QCheckBox::setEnabled);
// Collect all unique terminal strip prefixes from the project
if (project) {
QSet<QString> prefixes;
foreach (Diagram *diagram, project->diagrams()) {
foreach (QGraphicsItem *qgi, diagram->items()) {
if (Element *elmt = qgraphicsitem_cast<Element *>(qgi)) {
if (elmt->elementData().m_type == ElementData::Terminal) {
// Ignore locked terminals
DiagramContext info = elmt->elementInformations();
if (QET::infoFlagIsTrue(info.value(QStringLiteral("auto_num_locked")).toString())) {
continue;
}
QString label = elmt->actualLabel();
if (label.isEmpty()) continue;
// Handle labels with and without colon
int colonIndex = label.lastIndexOf(':');
QString prefix;
if (colonIndex != -1) {
prefix = label.left(colonIndex);
} else {
prefix = label;
}
prefixes.insert(prefix);
}
}
}
}
// Sort prefixes alphabetically and create checkboxes
QStringList sortedPrefixes = prefixes.values();
sortedPrefixes.sort(Qt::CaseInsensitive);
foreach (const QString &prefix, sortedPrefixes) {
QCheckBox *cb = new QCheckBox(prefix);
cb->setChecked(true);
ui->verticalLayout_strips_content->addWidget(cb);
m_stripCheckboxes.insert(prefix, cb);
}
}
}
/**
@@ -46,11 +96,37 @@ bool TerminalNumberingDialog::isAlphanumeric() const
return ui->rb_type_alpha->isChecked();
}
/**
* @brief TerminalNumberingDialog::alsoNumberLetters
* @return true if the "also number letters" checkbox is checked
*/
bool TerminalNumberingDialog::alsoNumberLetters() const
{
return ui->cb_also_alpha->isChecked();
}
/**
* @brief TerminalNumberingDialog::excludedStrips
* @return List of terminal strip prefixes that should be excluded from numbering
*/
QStringList TerminalNumberingDialog::excludedStrips() const
{
QStringList excluded;
QMapIterator<QString, QCheckBox*> it(m_stripCheckboxes);
while (it.hasNext()) {
it.next();
if (!it.value()->isChecked()) {
excluded.append(it.key());
}
}
return excluded;
}
/**
* @brief TerminalNumberingDialog::getUndoCommand
* Scans the given project for terminals, sorts them according to user preferences
* (X/Y axis, alphanumeric rules), and generates an undo command containing all label changes.
* * @param project Pointer to the current QETProject
* @param project Pointer to the current QETProject
* @return QUndoCommand* containing the modifications, or nullptr if no changes are needed.
*/
QUndoCommand* TerminalNumberingDialog::getUndoCommand(QETProject *project) const {
@@ -58,6 +134,8 @@ QUndoCommand* TerminalNumberingDialog::getUndoCommand(QETProject *project) const
bool axisX = isXAxisPriority();
bool alpha = isAlphanumeric();
bool alsoAlpha = alsoNumberLetters();
QStringList excluded = excludedStrips();
// 1. Helper structure to store and sort terminal data
struct TermInfo {
@@ -81,7 +159,7 @@ QUndoCommand* TerminalNumberingDialog::getUndoCommand(QETProject *project) const
DiagramContext info = elmt->elementInformations();
// Ignore locked terminals (if the user checked a 'lock' property)
if (info.value(QStringLiteral("auto_num_locked")).toString() == QLatin1String("true")) {
if (QET::infoFlagIsTrue(info.value(QStringLiteral("auto_num_locked")).toString())) {
continue;
}
@@ -97,6 +175,9 @@ QUndoCommand* TerminalNumberingDialog::getUndoCommand(QETProject *project) const
suffix = label.mid(colonIndex + 1);
}
// Skip excluded terminal strips
if (excluded.contains(prefix)) continue;
// If user chose purely numeric, skip terminals with alphabetical suffixes
if (!alpha && !suffix.isEmpty()) {
bool isNum;
@@ -154,8 +235,16 @@ QUndoCommand* TerminalNumberingDialog::getUndoCommand(QETProject *project) const
// If it was a number (e.g., "1") or empty, update it with the new counter
newLabel = ti.prefix + ":" + QString::number(newNum);
} else {
// If it was alphabetical (e.g., "N", "PE"), keep the original text but consume the count!
newLabel = ti.prefix + ":" + ti.suffix;
// Only append to purely alphabetic suffixes (N, PE), so re-running is a
// no-op and already-numbered suffixes (L1, L2, L3) keep their meaning.
const bool allLetters = !ti.suffix.isEmpty()
&& std::all_of(ti.suffix.cbegin(), ti.suffix.cend(),
[](QChar c){ return c.isLetter(); });
if (alsoAlpha && allLetters) {
newLabel = ti.prefix + ":" + ti.suffix + QString::number(newNum);
} else {
newLabel = ti.prefix + ":" + ti.suffix;
}
}
DiagramContext oldInfo = ti.elmt->elementInformations();
+6 -1
View File
@@ -2,9 +2,11 @@
#define TERMINALNUMBERINGDIALOG_H
#include <QDialog>
#include <QMap>
class QETProject;
class QUndoCommand;
class QCheckBox;
namespace Ui {
class TerminalNumberingDialog;
@@ -19,17 +21,20 @@ class TerminalNumberingDialog : public QDialog
Q_OBJECT
public:
explicit TerminalNumberingDialog(QWidget *parent = nullptr);
explicit TerminalNumberingDialog(QWidget *parent = nullptr, QETProject *project = nullptr);
~TerminalNumberingDialog();
// Getters for the user's choices
bool isXAxisPriority() const;
bool isAlphanumeric() const;
bool alsoNumberLetters() const;
QStringList excludedStrips() const;
QUndoCommand* getUndoCommand(QETProject *project) const;
private:
Ui::TerminalNumberingDialog *ui;
QMap<QString, QCheckBox*> m_stripCheckboxes;
};
#endif // TERMINALNUMBERINGDIALOG_H
+119 -52
View File
@@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>400</width>
<height>300</height>
<width>500</width>
<height>450</height>
</rect>
</property>
<property name="windowTitle">
@@ -25,56 +25,123 @@
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_axis">
<property name="title">
<string>Priorité des axes</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QRadioButton" name="rb_priority_x">
<property name="text">
<string>Priorité à l'axe X (horizontal)</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="rb_priority_y">
<property name="text">
<string>Priorité à l'axe Y (vertical)</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_type">
<property name="title">
<string>Type de numérotation</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QRadioButton" name="rb_type_num">
<property name="text">
<string>Numérique uniquement (1, 2, 3...)</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="rb_type_alpha">
<property name="text">
<string>Alphanumérique (A, B, C... 1, 2...)</string>
</property>
</widget>
</item>
</layout>
</widget>
<layout class="QHBoxLayout" name="horizontalLayout_main">
<item>
<layout class="QVBoxLayout" name="verticalLayout_left">
<item>
<widget class="QGroupBox" name="groupBox_axis">
<property name="title">
<string>Priorité des axes</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QRadioButton" name="rb_priority_x">
<property name="text">
<string>Priorité à l'axe X (horizontal)</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="rb_priority_y">
<property name="text">
<string>Priorité à l'axe Y (vertical)</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_type">
<property name="title">
<string>Type de numérotation</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QRadioButton" name="rb_type_num">
<property name="text">
<string>Numérique uniquement (1, 2, 3...)</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="rb_type_alpha">
<property name="text">
<string>Alphanumérique (A, B, C... 1, 2...)</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_also_alpha">
<item>
<spacer name="spacer_also_alpha">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QCheckBox" name="cb_also_alpha">
<property name="text">
<string>Numéroter également les lettres</string>
</property>
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QGroupBox" name="groupBox_strips">
<property name="title">
<string>Borniers</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_strips">
<item>
<widget class="QLabel" name="label_strips_description">
<property name="text">
<string>Décochez les borniers dont la numérotation doit être exclue</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QScrollArea" name="scrollArea_strips">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents_strips">
<layout class="QVBoxLayout" name="verticalLayout_strips_content"/>
</widget>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
+2 -2
View File
@@ -33,7 +33,7 @@
<string>Télechargement</string>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="edit-download" resource="../../qelectrotech.qrc">
<normaloff>:/ico/16x16/edit-download.png</normaloff>:/ico/16x16/edit-download.png</iconset>
</property>
</widget>
@@ -44,7 +44,7 @@
<string>Dossier installation</string>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="folder-open" resource="../../qelectrotech.qrc">
<normaloff>:/ico/16x16/folder-open.png</normaloff>:/ico/16x16/folder-open.png</iconset>
</property>
</widget>
+28 -10
View File
@@ -194,7 +194,16 @@ TitleBlockProperties TitleBlockPropertiesWidget::properties() const
prop.useDate = TitleBlockProperties::UseDateValue;
prop.date = ui->m_date_edit->date();
}
else if (ui->m_current_date_rb->isVisible() && ui->m_current_date_rb->isChecked()) {
else if (!ui->m_current_date_rb->isHidden() && ui->m_current_date_rb->isChecked()) {
/* isVisible() (unlike isHidden()) also depends on the whole
* ancestor chain being visible, not just this widget's own
* state -- inside a QTabWidget page (both the New Project and
* Project Properties dialogs embed this widget in one), it's
* false whenever this isn't the active tab, silently dropping
* "current date" back to the useDate/date default (no date)
* even though the radio button is still checked underneath.
* isHidden() mirrors the read side's own check in initDialog(),
* and isn't affected by an ancestor tab switch. */
prop.useDate = TitleBlockProperties::CurrentDate;
prop.date = QDate::currentDate();
}
@@ -237,7 +246,16 @@ TitleBlockProperties TitleBlockPropertiesWidget::propertiesAutoNum(
prop.useDate = TitleBlockProperties::UseDateValue;
prop.date = ui->m_date_edit->date();
}
else if (ui->m_current_date_rb->isVisible() && ui->m_current_date_rb->isChecked()) {
else if (!ui->m_current_date_rb->isHidden() && ui->m_current_date_rb->isChecked()) {
/* isVisible() (unlike isHidden()) also depends on the whole
* ancestor chain being visible, not just this widget's own
* state -- inside a QTabWidget page (both the New Project and
* Project Properties dialogs embed this widget in one), it's
* false whenever this isn't the active tab, silently dropping
* "current date" back to the useDate/date default (no date)
* even though the radio button is still checked underneath.
* isHidden() mirrors the read side's own check in initDialog(),
* and isn't affected by an ancestor tab switch. */
prop.useDate = TitleBlockProperties::CurrentDate;
prop.date = QDate::currentDate();
}
@@ -335,23 +353,23 @@ void TitleBlockPropertiesWidget::initDialog(
this);
connect(m_tbt_edit,
SIGNAL(triggered()),
&QAction::triggered,
this,
SLOT(editCurrentTitleBlockTemplate()));
&TitleBlockPropertiesWidget::editCurrentTitleBlockTemplate);
connect(m_tbt_duplicate,
SIGNAL(triggered()),
&QAction::triggered,
this,
SLOT(duplicateCurrentTitleBlockTemplate()));
&TitleBlockPropertiesWidget::duplicateCurrentTitleBlockTemplate);
m_tbt_menu = new QMenu(tr("Title block templates actions"), ui->m_tbt_pb);
m_tbt_menu -> addAction(m_tbt_edit);
m_tbt_menu -> addAction(m_tbt_duplicate);
ui -> m_tbt_pb -> setMenu(m_tbt_menu);
connect(ui->m_tbt_cb,
SIGNAL(currentIndexChanged(int)),
this,
SLOT(changeCurrentTitleBlockTemplate(int)));
connect(ui->m_tbt_cb,
qOverload<int>(&QComboBox::currentIndexChanged),
this,
&TitleBlockPropertiesWidget::changeCurrentTitleBlockTemplate);
if (project!= nullptr){
keys_2 = project -> folioAutoNum().keys();
+5 -5
View File
@@ -94,7 +94,7 @@
<string notr="true"/>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="titleblock-bottom" resource="../../qelectrotech.qrc">
<normaloff>:/ico/22x22/titleblock-bottom.png</normaloff>:/ico/22x22/titleblock-bottom.png</iconset>
</property>
</item>
@@ -103,7 +103,7 @@
<string notr="true"/>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="titleblock-right" resource="../../qelectrotech.qrc">
<normaloff>:/ico/22x22/titleblock-right.png</normaloff>:/ico/22x22/titleblock-right.png</iconset>
</property>
</item>
@@ -121,7 +121,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="edit-rename" resource="../../qelectrotech.qrc">
<normaloff>:/ico/22x22/edit-rename.png</normaloff>:/ico/22x22/edit-rename.png</iconset>
</property>
<property name="iconSize">
@@ -210,7 +210,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="configure" resource="../../qelectrotech.qrc">
<normaloff>:/ico/16x16/configure.png</normaloff>:/ico/16x16/configure.png</iconset>
</property>
</widget>
@@ -305,7 +305,7 @@
<string/>
</property>
<property name="icon">
<iconset resource="../../qelectrotech.qrc">
<iconset theme="start" resource="../../qelectrotech.qrc">
<normaloff>:/ico/22x22/start.png</normaloff>:/ico/22x22/start.png</iconset>
</property>
</widget>
+114
View File
@@ -0,0 +1,114 @@
/*
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 "wiringlistdialog.h"
#include "../dataBase/projectdatabase.h"
#include "../qetproject.h"
#include <QDialogButtonBox>
#include <QHeaderView>
#include <QLabel>
#include <QSqlQueryModel>
#include <QTableView>
#include <QVBoxLayout>
/**
@brief WiringListDialog::WiringListDialog
@param project : project whose wiring list is shown
@param parent : parent widget
*/
WiringListDialog::WiringListDialog(QETProject *project, QWidget *parent) :
QDialog(parent),
m_project(project)
{
setWindowTitle(tr("Liste de câblage", "window title"));
resize(900, 500);
auto *layout = new QVBoxLayout(this);
//The wiring list reads the database rather than the diagrams, and a
//conductor's row is only as fresh as the last thing that touched it.
//Refresh before querying so the dialog cannot show a wire number that
//was edited earlier in the session.
m_project->dataBase()->updateDB();
auto *model = new QSqlQueryModel(this);
model->setQuery(QStringLiteral(
"SELECT wire_number, from_element_label, from_terminal,"
" to_element_label, to_terminal, diagram_position"
" FROM wiring_list_view"
//Wire numbers are text, so a plain sort puts "10" before "9".
//Numeric ones first, ordered by value; anything non-numeric
//after, ordered as text. The trailing wire_number keeps ties
//stable.
" ORDER BY diagram_position,"
" CASE WHEN wire_number GLOB '[0-9]*' THEN 0 ELSE 1 END,"
" CAST(wire_number AS INTEGER),"
" wire_number"),
m_project->dataBase()->database());
model->setHeaderData(0, Qt::Horizontal, tr("Fil", "column title"));
model->setHeaderData(1, Qt::Horizontal, tr("Composant 1", "column title"));
model->setHeaderData(2, Qt::Horizontal, tr("Borne 1", "column title"));
model->setHeaderData(3, Qt::Horizontal, tr("Composant 2", "column title"));
model->setHeaderData(4, Qt::Horizontal, tr("Borne 2", "column title"));
model->setHeaderData(5, Qt::Horizontal, tr("Folio", "column title"));
const int excluded = m_project->dataBase()->excludedConductorCount();
//QSqlQueryModel fetches lazily, so rowCount() straight after
//setQuery() reports the first batch (256) rather than the query's
//size. Draining it first is what makes the count below true for a
//project with more wires than that.
while (model->canFetchMore()) {
model->fetchMore();
}
const int listed = model->rowCount();
auto *summary = new QLabel(this);
summary->setWordWrap(true);
if (excluded > 0)
{
//Rare now that Terminal::stableUuid() gives every terminal an
//identity: what is left is a conductor whose endpoint has no
//parent element at all. Still worth saying out loud rather than
//presenting a short list as if it were complete.
summary->setText(tr("%n conducteur(s) listé(s).", "wiring list summary", listed)
% QStringLiteral(" ")
% tr("%n conducteur(s) exclu(s) : une extrémité n'est rattachée"
" à aucun élément.",
"wiring list exclusion warning", excluded));
}
else {
summary->setText(tr("%n conducteur(s) listé(s).", "wiring list summary", listed));
}
layout->addWidget(summary);
auto *view = new QTableView(this);
view->setModel(model);
view->setEditTriggers(QAbstractItemView::NoEditTriggers);
view->setSelectionBehavior(QAbstractItemView::SelectRows);
view->setAlternatingRowColors(true);
view->verticalHeader()->setVisible(false);
view->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
layout->addWidget(view);
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Close, this);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
layout->addWidget(buttons);
}
+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 WIRINGLISTDIALOG_H
#define WIRINGLISTDIALOG_H
#include <QDialog>
class QETProject;
/**
@brief The WiringListDialog class
Read-only view of the project's from-to wiring list, read from the
wiring_list_view of projectDataBase.
Deliberately not an exporter: QET already ships a wiring-list CSV export
(Projet > Exporter le plan de câblage, and --export-cables), which walks
the project XML and covers that need. This dialog exists to make the
database view inspectable, and above all to state how many conductors
are missing from it and why -- a count the CSV export cannot give,
because it never excludes anything in the first place.
*/
class WiringListDialog : public QDialog
{
Q_OBJECT
public:
explicit WiringListDialog(QETProject *project, QWidget *parent = nullptr);
private:
QETProject *m_project = nullptr;
};
#endif // WIRINGLISTDIALOG_H
+37 -6
View File
@@ -37,9 +37,10 @@ XRefPropertiesWidget::XRefPropertiesWidget(QHash <QString, XRefProperties> prope
{
ui->setupUi(this);
buildUi();
connect(ui->m_display_has_cross_rb, SIGNAL(toggled(bool)), ui->m_cross_properties_gb, SLOT(setEnabled(bool)));
connect(ui->m_type_cb, SIGNAL(currentIndexChanged(int)), this, SLOT(typeChanged()));
connect(ui->m_snap_to_cb, SIGNAL(currentIndexChanged(int)), this, SLOT(enableOffsetSB(int)));
connect(ui->m_display_has_cross_rb, &QRadioButton::toggled, ui->m_cross_properties_gb, &QWidget::setEnabled);
connect(ui->m_display_has_contacts_rb, &QRadioButton::toggled, ui->m_show_all_slaves_cb, &QWidget::setEnabled);
connect(ui->m_type_cb, qOverload<int>(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::typeChanged);
connect(ui->m_snap_to_cb, qOverload<int>(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::enableOffsetSB);
updateDisplay();
}
@@ -49,9 +50,10 @@ XRefPropertiesWidget::XRefPropertiesWidget(QHash <QString, XRefProperties> prope
*/
XRefPropertiesWidget::~XRefPropertiesWidget()
{
disconnect(ui->m_display_has_cross_rb, SIGNAL(toggled(bool)), ui->m_cross_properties_gb, SLOT(setEnabled(bool)));
disconnect(ui->m_type_cb, SIGNAL(currentIndexChanged(int)), this, SLOT(typeChanged()));
disconnect(ui->m_snap_to_cb, SIGNAL(currentIndexChanged(int)), this, SLOT(enableOffsetSB(int)));
disconnect(ui->m_display_has_cross_rb, &QRadioButton::toggled, ui->m_cross_properties_gb, &QWidget::setEnabled);
disconnect(ui->m_display_has_contacts_rb, &QRadioButton::toggled, ui->m_show_all_slaves_cb, &QWidget::setEnabled);
disconnect(ui->m_type_cb, qOverload<int>(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::typeChanged);
disconnect(ui->m_snap_to_cb, qOverload<int>(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::enableOffsetSB);
delete ui;
}
@@ -101,6 +103,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");
@@ -110,6 +113,7 @@ void XRefPropertiesWidget::buildUi()
ui -> m_xrefpos_cb -> addItem(tr("Left"),"left");
ui -> m_xrefpos_cb -> addItem(tr("Right"),"right");
ui -> m_xrefpos_cb -> addItem(tr("Text alignment"),"alignment");
ui -> m_xrefpos_cb -> addItem(tr("Champ de texte"),"text_field");
m_previous_type_index = ui -> m_type_cb -> currentIndex();
}
@@ -138,14 +142,17 @@ void XRefPropertiesWidget::saveProperties(int index) {
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "left") xrp.setXrefPos(Qt::AlignLeft);
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "right") xrp.setXrefPos(Qt::AlignRight);
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "alignment") xrp.setXrefPos(Qt::AlignBaseline);
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "text_field") xrp.setXrefPos(Qt::AlignHCenter);
xrp.setShowPowerContac(ui->m_show_power_cb->isChecked());
xrp.setShowTerminalName(ui->m_show_terminal_name_cb->isChecked());
xrp.setShowAllConfiguredSlaves(ui->m_show_all_slaves_cb->isChecked());
xrp.setPrefix("power", ui->m_power_prefix_le->text());
xrp.setPrefix("delay", ui->m_delay_prefix_le->text());
xrp.setPrefix("switch", ui->m_switch_prefix_le->text());
xrp.setMasterLabel(ui->m_master_le->text());
xrp.setSlaveLabel(ui->m_slave_le->text());
xrp.setOffset(ui->m_offset_sb->value());
xrp.setSlaveOffset(ui->m_slave_offset_sb->value());
m_properties.insert(type, xrp);
}
@@ -176,6 +183,9 @@ void XRefPropertiesWidget::updateDisplay()
int offset = xrp.offset();
ui->m_offset_sb->setValue(offset);
int slave_offset = xrp.slaveOffset();
ui->m_slave_offset_sb->setValue(slave_offset);
if (xrp.snapTo() == XRefProperties::Bottom){
ui->m_snap_to_cb->setCurrentIndex(ui->m_snap_to_cb->findData("bottom"));
ui->m_offset_sb->setEnabled(true);
@@ -190,12 +200,33 @@ void XRefPropertiesWidget::updateDisplay()
else if(xrp.getXrefPos() == Qt::AlignRight) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("right"));
else if(xrp.getXrefPos() == Qt::AlignBaseline) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("alignment"));
else if(xrp.getXrefPos() == Qt::AlignBottom) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("bottom"));
else if(xrp.getXrefPos() == Qt::AlignHCenter) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("text_field"));
ui->m_show_power_cb->setChecked(xrp.showPowerContact());
ui->m_show_terminal_name_cb->setChecked(xrp.showTerminalName());
ui->m_show_all_slaves_cb->setChecked(xrp.showAllConfiguredSlaves());
//The radio button only emits toggled() when it really changes: loading
//a type whose display did not change left the checkbox with the enabled
//state of the previously displayed type (it stayed clickable although
//the cross display was selected). Set the state explicitly here.
ui->m_show_all_slaves_cb->setEnabled(
ui->m_display_has_contacts_rb->isChecked());
ui->m_power_prefix_le-> setText(xrp.prefix("power"));
ui->m_delay_prefix_le-> setText(xrp.prefix("delay"));
ui->m_switch_prefix_le->setText(xrp.prefix("switch"));
ui->m_cross_properties_gb->setDisabled(!ui->m_display_has_cross_rb->isChecked());
//The cross ref of a PLC master is always drawn as its IO table, and
//the slaves are referenced directly into that table: the contacts/
//cross choice, the two display checkboxes and the cross options below
//have no effect at all for this type, so they are hidden instead of
//being offered for nothing. The positioning settings and the labels
//(the table really uses them) stay available.
const bool is_plc = type == QLatin1String("plc");
ui->m_display_has_contacts_rb->setVisible(!is_plc);
ui->m_display_has_cross_rb->setVisible(!is_plc);
ui->m_show_terminal_name_cb->setVisible(!is_plc);
ui->m_show_all_slaves_cb->setVisible(!is_plc);
ui->m_cross_properties_gb->setVisible(!is_plc);
}
/**
+61 -14
View File
@@ -90,20 +90,56 @@
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QLabel" name="label_10">
<property name="text">
<string>XRef slave position</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="m_xrefpos_cb"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_6">
<item>
<widget class="QLabel" name="label_10">
<property name="text">
<string>XRef slave position</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="m_xrefpos_cb"/>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QLabel" name="label_11">
<property name="text">
<string>Distance label - slave :</string>
</property>
</widget>
</item>
<item>
<widget class="QSpinBox" name="m_slave_offset_sb">
<property name="toolTip">
<string>Distance in pixels between the label and the slave cross reference</string>
</property>
<property name="suffix">
<string notr="true">px</string>
</property>
<property name="correctionMode">
<enum>QAbstractSpinBox::CorrectToNearestValue</enum>
</property>
<property name="minimum">
<number>-50</number>
</property>
<property name="maximum">
<number>100</number>
</property>
<property name="singleStep">
<number>1</number>
</property>
<property name="value">
<number>0</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
@@ -115,6 +151,16 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="m_show_all_slaves_cb">
<property name="toolTip">
<string>Afficher dans le peigne de contacts tous les contacts esclaves définis par le maître, même ceux qui ne sont pas encore reliés, dans l'ordre défini par le maître</string>
</property>
<property name="text">
<string>Afficher tous les esclaves définis par le maître</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="m_display_has_contacts_rb">
<property name="text">
@@ -291,6 +337,7 @@
<tabstop>m_slave_le</tabstop>
<tabstop>m_show_power_cb</tabstop>
<tabstop>m_show_terminal_name_cb</tabstop>
<tabstop>m_show_all_slaves_cb</tabstop>
<tabstop>m_power_prefix_le</tabstop>
<tabstop>m_delay_prefix_le</tabstop>
<tabstop>m_switch_prefix_le</tabstop>