Merge remote-tracking branch 'upstream/master' into review-635-fix

# Conflicts:
#	CMakeLists.txt
#	cmake/developer_options.cmake
#	cmake/qet_compilation_vars.cmake
#	sources/qetapp.cpp
This commit is contained in:
ispyisail
2026-09-22 23:10:34 +12:00
676 changed files with 150173 additions and 61935 deletions
+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">
+265
View File
@@ -0,0 +1,265 @@
/*
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>
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 conductors = dc.conductors(DiagramContent::AnyConductor);
if (conductors.isEmpty()) {
return;
}
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>
+180 -3
View File
@@ -31,6 +31,8 @@
#include "../titleblockpropertieswidget.h"
#include "../xrefpropertieswidget.h"
#include "guidespropertieswidget.h"
#include "../autoNum/numerotationcontext.h"
#include "../autoNum/ui/selectautonumw.h"
#include <QFont>
#include <QFontDialog>
#include <QSizePolicy>
@@ -101,6 +103,32 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
}
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) {
bpw -> setProperties (m_project -> defaultBorderProperties());
@@ -110,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);
@@ -127,6 +156,19 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
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);
@@ -138,7 +180,7 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
*/
NewDiagramPage::~NewDiagramPage()
{
disconnect(ipw,SIGNAL(openAutoNumFolioEditor(QString)),this,SLOT(changeToAutoFolioTab()));
disconnect(ipw, &TitleBlockPropertiesWidget::openAutoNumFolioEditor, this, &NewDiagramPage::changeToAutoFolioTab);
}
/**
@@ -214,7 +256,11 @@ void NewDiagramPage::applyConf()
rpw->toSettings(settings, "diagrameditor/defaultreport");
// default xref properties
QHash <QString, XRefProperties> hash_xrp = xrefpw -> properties();
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();
@@ -226,6 +272,9 @@ void NewDiagramPage::applyConf()
settings.setValue(QStringLiteral("color"), current_guides[i].color.name());
}
settings.endArray();
// save global auto-numbering defaults
persistAutonumSettings();
}
}
@@ -291,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
+21
View File
@@ -20,6 +20,7 @@
#include "configpage.h"
#include "../projectpropertiesdialog.h"
#include "../titleblockpropertieswidget.h"
#include "../autoNum/numerotationcontext.h"
#include <QDialog>
#include <QtWidgets>
@@ -32,6 +33,7 @@ class XRefPropertiesWidget;
class GuidesPropertiesWidget;
class QETProject;
class TitleBlockProperties;
class SelectAutonumW;
/**
@brief The NewDiagramPage class
@@ -48,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);
@@ -72,7 +75,25 @@ public slots:
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);
};
/**
@@ -40,9 +40,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,7 +62,7 @@ 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());
@@ -215,10 +212,8 @@ void GeneralConfigurationPage::applyConf()
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());
@@ -393,7 +388,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");
@@ -405,9 +400,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;
@@ -258,27 +258,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">
@@ -286,8 +265,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>
@@ -300,6 +279,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">
@@ -328,48 +328,6 @@
</property>
</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="5" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Répertoire des cartouches utilisateur</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QComboBox" name="m_custom_tbt_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="6" 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_23">
<property name="text">
@@ -391,6 +349,48 @@
</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>
</property>
</item>
<item>
<property name="text">
<string>Parcourir...</string>
</property>
</item>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_10">
<property name="text">
<string>Répertoire des cartouches utilisateur</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="m_company_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="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>
</layout>
</widget>
</item>
+244 -9
View File
@@ -23,6 +23,7 @@
#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"
@@ -290,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();
}
/**
@@ -349,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();
}
/**
@@ -360,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);
@@ -382,6 +407,9 @@ void ProjectAutoNumConfigPage::readValuesFromProject()
*/
void ProjectAutoNumConfigPage::adjustReadOnly()
{
if (m_import_pb && m_project) {
m_import_pb->setDisabled(m_project->isReadOnly());
}
}
/**
@@ -391,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);
}
/**
@@ -480,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
@@ -491,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());
}
/**
@@ -678,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();
}
@@ -691,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();
}
@@ -156,6 +156,7 @@ class ProjectAutoNumConfigPage : public ProjectConfigPage {
void updateContextElement(const QString&);//element
void saveContextElement();
void removeContextElement();
void importFromProject();
void applyAutoNum();
void applyManagement();
@@ -173,6 +174,7 @@ class ProjectAutoNumConfigPage : public ProjectConfigPage {
SelectAutonumW *m_saw_element;
FolioAutonumberingW *m_faw;
AutoNumberingManagementW *m_amw;
QPushButton *m_import_pb = nullptr;
};
+204 -46
View File
@@ -20,6 +20,7 @@
#include "../../qeticons.h"
#include "../../shortcutmanager.h"
#include <QComboBox>
#include <QFrame>
#include <QHBoxLayout>
#include <QHeaderView>
@@ -27,10 +28,33 @@
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QTableWidget>
#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
@@ -51,18 +75,33 @@ ShortcutsConfigPage::ShortcutsConfigPage(QWidget *parent) :
m_filter_edit = new QLineEdit(this);
m_filter_edit->setPlaceholderText(tr("Filtrer les raccourcis…"));
connect(m_filter_edit, &QLineEdit::textChanged, this, &ShortcutsConfigPage::filterRows);
vlayout->addWidget(m_filter_edit);
m_table = new QTableWidget(0, 4, this);
m_table->setHorizontalHeaderLabels({tr("Catégorie"), tr("Action"), tr("Raccourci"), QString()});
m_table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
m_table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
m_table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
m_table->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
m_table->verticalHeader()->setVisible(false);
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
m_table->setSelectionMode(QAbstractItemView::NoSelection);
vlayout->addWidget(m_table);
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);
@@ -75,6 +114,7 @@ ShortcutsConfigPage::ShortcutsConfigPage(QWidget *parent) :
setLayout(vlayout);
populateTable();
applyFilter();
}
ShortcutsConfigPage::~ShortcutsConfigPage()
@@ -83,8 +123,8 @@ ShortcutsConfigPage::~ShortcutsConfigPage()
/**
@brief ShortcutsConfigPage::populateTable
Fill the table with one row per shortcut known to ShortcutManager, sorted
by category then action name.
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()
{
@@ -97,33 +137,38 @@ void ShortcutsConfigPage::populateTable()
return a.description < b.description;
});
m_table->setRowCount(shortcuts.size());
m_tree->clear();
m_rows.clear();
m_rows.reserve(shortcuts.size());
for (int row = 0; row < shortcuts.size(); ++row) {
const ShortcutManager::ShortcutInfo &info = shortcuts.at(row);
QHash<QString, QTreeWidgetItem *> category_nodes;
auto *category_item = new QTableWidgetItem(info.category);
category_item->setFlags(category_item->flags() & ~Qt::ItemIsEditable);
m_table->setItem(row, 0, category_item);
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 *description_item = new QTableWidgetItem(info.description);
description_item->setFlags(description_item->flags() & ~Qt::ItemIsEditable);
m_table->setItem(row, 1, description_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_table);
auto *edit = new QKeySequenceEdit(info.current_sequence, m_tree);
connect(edit, &QKeySequenceEdit::editingFinished, this, &ShortcutsConfigPage::checkConflicts);
m_table->setCellWidget(row, 2, edit);
m_tree->setItemWidget(child, 1, edit);
auto *reset_button = new QToolButton(m_table);
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);
connect(reset_button, &QToolButton::clicked, this, [this, row]() { resetRow(row); });
m_table->setCellWidget(row, 3, reset_button);
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.default_sequence, edit};
m_rows << Row{info.id, info.category, info.description, info.default_sequence, edit, child, false};
}
checkConflicts();
@@ -131,17 +176,116 @@ void ShortcutsConfigPage::populateTable()
/**
@brief ShortcutsConfigPage::filterRows
Hide every row whose category or action name doesn't contain \a filter_text.
Re-run the combined text + quick filter whenever the search box changes.
*/
void ShortcutsConfigPage::filterRows(const QString &filter_text)
{
const QString needle = filter_text.trimmed();
for (int row = 0; row < m_table->rowCount(); ++row) {
const bool matches = needle.isEmpty()
|| m_table->item(row, 0)->text().contains(needle, Qt::CaseInsensitive)
|| m_table->item(row, 1)->text().contains(needle, Qt::CaseInsensitive);
m_table->setRowHidden(row, !matches);
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));
}
/**
@@ -160,27 +304,41 @@ void ShortcutsConfigPage::checkConflicts()
}
for (int row = 0; row < m_rows.size(); ++row) {
const Row &current_row = m_rows.at(row);
Row &current_row = m_rows[row];
const QString sequence_text = current_row.edit->keySequence().toString();
const QList<int> &conflicting_rows = sequence_to_rows.value(sequence_text);
const bool conflicted = !sequence_text.isEmpty() && conflicting_rows.size() > 1;
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;
QTableWidgetItem *description_item = m_table->item(row, 1);
if (conflicted) {
QStringList other_descriptions;
for (int other_row : conflicting_rows) {
if (other_row != row) {
other_descriptions << m_table->item(other_row, 1)->text();
other_descriptions << m_rows.at(other_row).description;
}
}
description_item->setBackground(QColor(255, 205, 205));
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 {
description_item->setBackground(Qt::NoBrush);
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();
}
}
/**
@@ -198,7 +356,7 @@ void ShortcutsConfigPage::resetRow(int row_index)
void ShortcutsConfigPage::resetAllRows()
{
for (const Row &row : qAsConst(m_rows)) {
for (const Row &row : std::as_const(m_rows)) {
row.edit->setKeySequence(row.default_sequence);
}
checkConflicts();
@@ -211,7 +369,7 @@ void ShortcutsConfigPage::resetAllRows()
*/
void ShortcutsConfigPage::applyConf()
{
for (const Row &row : qAsConst(m_rows)) {
for (const Row &row : std::as_const(m_rows)) {
ShortcutManager::instance().setSequence(row.id, row.edit->keySequence());
}
}
@@ -223,5 +381,5 @@ QString ShortcutsConfigPage::title() const
QIcon ShortcutsConfigPage::icon() const
{
return QET::Icons::ConfigureToolbars;
return QET::Icons::ConfigureShortcuts;
}
+20 -2
View File
@@ -22,9 +22,12 @@
#include <QKeySequence>
class QComboBox;
class QKeySequenceEdit;
class QLabel;
class QLineEdit;
class QTableWidget;
class QTreeWidget;
class QTreeWidgetItem;
/**
@brief The ShortcutsConfigPage class
@@ -47,21 +50,36 @@ class ShortcutsConfigPage : public ConfigPage
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;
QTableWidget *m_table;
QComboBox *m_quick_filter;
QLabel *m_count_label;
QTreeWidget *m_tree;
QList<Row> m_rows;
};
+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>
+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
+242
View File
@@ -0,0 +1,242 @@
/*
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 <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"));
m_is_system_color = true;
m_current = QApplication::palette().color(QPalette::Base);
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;
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;
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();
}
/**
@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);
}
+64
View File
@@ -0,0 +1,64 @@
/*
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 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>
+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>
+52 -10
View File
@@ -36,6 +36,7 @@
#include <QHash>
#include <QModelIndex>
#include <QStandardItem>
#include <QTimer>
#include <QUndoCommand>
static int src_txt_row = 0;
@@ -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
@@ -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
+19
View File
@@ -18,7 +18,9 @@
#include "elementinfopartwidget.h"
#include "../SearchAndReplace/searchandreplaceworker.h"
#include "../qetinformation.h"
#include "ui_elementinfopartwidget.h"
#include <QRegularExpressionValidator>
#include <utility>
@@ -43,6 +45,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 +77,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
+2 -2
View File
@@ -41,9 +41,9 @@ 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 setFocusTolineEdit();
+107 -1
View File
@@ -17,12 +17,14 @@
*/
#include "elementinfowidget.h"
#include <QCheckBox>
#include <QPushButton>
#include "../diagram.h"
#include "../qetapp.h"
#include "../qetgraphicsitem/element.h"
#include "../qetinformation.h"
#include "../ui_elementinfowidget.h"
#include "../undocommand/changeelementinformationcommand.h"
#include "customelementinfopartwidget.h"
#include "elementinfopartwidget.h"
/**
@@ -47,6 +49,7 @@ ElementInfoWidget::ElementInfoWidget(Element *elmt, QWidget *parent) :
ElementInfoWidget::~ElementInfoWidget()
{
qDeleteAll(m_eipw_list);
qDeleteAll(m_custom_eipw_list);
delete ui;
}
@@ -145,7 +148,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 +204,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 +251,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
@@ -271,6 +348,21 @@ void ElementInfoWidget::updateUi()
for (ElementInfoPartWidget *eipw : m_eipw_list) {
eipw -> setText (element_info[eipw->key()].toString());
}
// 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();
@@ -303,6 +395,9 @@ DiagramContext ElementInfoWidget::currentInfo() const
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 +409,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"));
+7
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,20 @@ class ElementInfoWidget : public AbstractElementPropertiesEditorWidget
private:
void buildInterface();
ElementInfoPartWidget *infoPartWidgetForKey(const QString &key) const;
QStringList predefinedKeys() const;
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;
+58 -5
View File
@@ -23,6 +23,7 @@
#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"
@@ -30,6 +31,7 @@
#include "masterpropertieswidget.h"
#include "plclinkwidget.h"
#include <QCheckBox>
#include <QLabel>
#include <QUndoStack>
#include <QVBoxLayout>
@@ -379,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());
@@ -396,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);
@@ -403,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"));
+81 -31
View File
@@ -17,7 +17,6 @@
*/
#include "linksingleelementwidget.h"
#include "contactgroupselectiondialog.h"
#include "../qetgraphicsitem/masterelement.h"
#include "../qetgraphicsitem/conductor.h"
#include "../diagram.h"
#include "../diagramposition.h"
@@ -26,10 +25,14 @@
#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>
/**
@@ -53,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, [=]()
{
@@ -250,6 +269,7 @@ void LinkSingleElementWidget::buildTree()
if (m_element->elementData().m_type == ElementData::Slave)
{
m_full_masters.clear();
for(const auto &elmt : elmt_vector)
{
@@ -289,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);
}
}
}
@@ -353,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();
@@ -410,11 +448,12 @@ QVector <QPointer<Element>> LinkSingleElementWidget::availableElements()
continue;
}
// If the master is full, we'll remove it from the list!
MasterElement *master = static_cast<MasterElement*>(elmt);
if (master->isFull()) {
elmt_vector.removeAt(i);
}
// 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;
@@ -447,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);
@@ -459,6 +500,7 @@ void LinkSingleElementWidget::clearTreeWidget()
m_qtwi_elmt_hash.clear();
m_qtwi_strl_hash.clear();
m_full_masters.clear();
}
void LinkSingleElementWidget::setUpHeaderLabels()
@@ -528,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()
@@ -667,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();
}
/**
@@ -680,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(
@@ -717,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);
}
@@ -726,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);
}
@@ -777,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);
}
+5
View File
@@ -26,6 +26,7 @@
#include <QComboBox>
class QTreeWidgetItem;
class QCheckBox;
class Element;
class QMenu;
@@ -71,6 +72,7 @@ class LinkSingleElementWidget : public AbstractElementPropertiesEditorWidget
void setUpCompleter();
void clearTreeWidget();
void setUpHeaderLabels();
void updateItemsVisibility();
private slots:
void diagramWasRemovedFromProject();
@@ -87,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;
@@ -103,6 +106,8 @@ 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};
+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/>
+42 -58
View File
@@ -154,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()))
{
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;
@@ -309,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;
}
}
}
@@ -397,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)
@@ -407,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);
}
/**
@@ -431,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);
}
/**
@@ -529,13 +534,13 @@ void MasterPropertiesWidget::updateUi()
// Table
m_plc_table = new QTableWidget(m_plc_widget);
m_plc_table->setColumnCount(5);
m_plc_table->setColumnCount(6);
m_plc_table->setHorizontalHeaderLabels({
tr("Type"), tr("Adresse"), tr("Fonction"),
tr("Commentaire"), tr("Réf. croisée")
tr("Commentaire"), tr("Réf. croisée"), tr("Bornes")
});
m_plc_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectRows);
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems);
m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
m_plc_table->setMinimumHeight(200);
@@ -596,6 +601,11 @@ void MasterPropertiesWidget::updateUi()
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);
@@ -810,23 +820,7 @@ void MasterPropertiesWidget::setCellFromValue(int row, int col, const QString &v
}
}
else if (col == 5) {
// Terminal count spinbox
auto *tc_sb = qobject_cast<QSpinBox*>(m_plc_table->cellWidget(row, col));
if (!tc_sb) {
tc_sb = new QSpinBox(m_plc_table);
tc_sb->setMinimum(1);
tc_sb->setMaximum(4);
m_plc_table->setCellWidget(row, col, tc_sb);
connect(tc_sb, QOverload<int>::of(&QSpinBox::valueChanged),
this, [this, row](int) { plcIOCellChanged(row, 5); });
}
bool ok;
int v = val.toInt(&ok);
if (ok && v >= 1 && v <= 4)
tc_sb->setValue(v);
}
else if (col == 6) {
// CrossRef - read-only
// Anschlüsse (read-only)
auto *item = new QTableWidgetItem(val);
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
m_plc_table->setItem(row, col, item);
@@ -864,17 +858,10 @@ void MasterPropertiesWidget::plcAddRow()
m_plc_table->setItem(row, 3, new QTableWidgetItem());
m_plc_table->setItem(row, 4, new QTableWidgetItem());
auto *tc_sb = new QSpinBox(m_plc_table);
tc_sb->setMinimum(1);
tc_sb->setMaximum(4);
tc_sb->setValue(1);
m_plc_table->setCellWidget(row, 5, tc_sb);
connect(tc_sb, QOverload<int>::of(&QSpinBox::valueChanged),
this, [this, row](int) { plcIOCellChanged(row, 5); });
auto *crossref_item = new QTableWidgetItem();
crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable);
m_plc_table->setItem(row, 6, crossref_item);
// 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);
}
/**
@@ -998,17 +985,11 @@ void MasterPropertiesWidget::plcUpdateDisplaySettings()
// Preserve existing display settings
ElementData ed = m_element->elementData();
ElementData::PlcMasterData plc_data = ed.plcMasterData();
const auto orig_plc = ed.plcMasterData();
ElementData::PlcMasterData plc_data = orig_plc;
plc_data.ios.clear();
// Build address -> original IO lookup to correctly reattach terminal data
// after row reorder (move up/down) or row removal
QHash<QString, int> addr_to_orig_idx;
for (int i = 0; i < ed.plcMasterData().ios.size(); ++i) {
addr_to_orig_idx[ed.plcMasterData().ios.at(i).address] = i;
}
// Read IOs from table, preserving terminal data from original IOs
// 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;
@@ -1032,9 +1013,12 @@ void MasterPropertiesWidget::plcUpdateDisplaySettings()
if (crossref_item)
io.crossRef = crossref_item->text();
// Preserve terminal data by looking up original IO via address
if (addr_to_orig_idx.contains(io.address)) {
const auto &orig_io = ed.plcMasterData().ios.at(addr_to_orig_idx.value(io.address));
// 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;
}
+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>
+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
+94 -35
View File
@@ -29,6 +29,7 @@
#include <QLineEdit>
#include <QPushButton>
#include <QLabel>
#include <QCheckBox>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
@@ -36,6 +37,7 @@
#include <QAction>
#include <QFont>
#include <QTimer>
#include <QSettings>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QGridLayout>
@@ -56,16 +58,20 @@ PlcLinkWidget::PlcLinkWidget(Element *elmt, QWidget *parent)
main_layout->addWidget(m_unlink_pb, 0, 1);
main_layout->addWidget(m_show_this_pb, 0, 2);
// Row 1: Search field
// 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, 1, 0, 1, 3);
main_layout->addWidget(m_search_field, 2, 0, 1, 3);
// Row 2: Tree widget
// 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("Anschlüsse")
tr("Fonction"), tr("Commentaire"), tr("Bornes")
});
m_tree_widget->setRootIsDecorated(true);
m_tree_widget->setIndentation(20);
@@ -78,9 +84,9 @@ PlcLinkWidget::PlcLinkWidget(Element *elmt, QWidget *parent)
m_tree_widget->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
m_tree_widget->header()->setSectionResizeMode(4, QHeaderView::ResizeToContents);
m_tree_widget->header()->setSectionResizeMode(5, QHeaderView::ResizeToContents);
main_layout->addWidget(m_tree_widget, 2, 0, 1, 3);
main_layout->addWidget(m_tree_widget, 3, 0, 1, 3);
// Row 3: Hidden masters note
// 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);
@@ -89,9 +95,9 @@ PlcLinkWidget::PlcLinkWidget(Element *elmt, QWidget *parent)
italic_font.setItalic(true);
m_hidden_masters_label->setFont(italic_font);
m_hidden_masters_label->hide();
main_layout->addWidget(m_hidden_masters_label, 3, 0, 1, 3);
main_layout->addWidget(m_hidden_masters_label, 4, 0, 1, 3);
main_layout->setRowStretch(2, 1);
main_layout->setRowStretch(3, 1);
setMinimumWidth(500);
@@ -104,6 +110,12 @@ PlcLinkWidget::PlcLinkWidget(Element *elmt, QWidget *parent)
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);
@@ -163,6 +175,7 @@ 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;
@@ -216,6 +229,7 @@ void PlcLinkWidget::buildPlcTree()
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);
@@ -225,44 +239,62 @@ void PlcLinkWidget::buildPlcTree()
child_item->setText(3, io.functionText);
child_item->setText(4, io.comment);
// Terminal count
child_item->setText(5, QString::number(io.terminalCount));
// 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, grey it out and strike through
// If this IO is already linked to a slave
if (used_io_indices.contains(i)) {
QFont strike_font = child_item->font(0);
strike_font.setStrikeOut(true);
child_item->setFont(0, strike_font);
child_item->setFont(1, strike_font);
child_item->setFont(2, strike_font);
child_item->setFont(3, strike_font);
child_item->setFont(4, strike_font);
child_item->setFont(5, strike_font);
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);
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;
// 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);
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();
@@ -296,15 +328,21 @@ void PlcLinkWidget::on_m_search_field_textEdited(const QString &text)
}
}
}
child->setHidden(!match);
if (match) any_child_visible = true;
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)) {
any_child_visible = true;
for (int j = 0; j < parent->childCount(); ++j)
parent->child(j)->setHidden(false);
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);
@@ -355,6 +393,20 @@ void PlcLinkWidget::on_m_unlink_pb_clicked()
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);
@@ -369,3 +421,10 @@ void PlcLinkWidget::on_m_show_this_pb_clicked()
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();
}
+5
View File
@@ -29,6 +29,7 @@ class QTreeWidget;
class QLineEdit;
class QPushButton;
class QLabel;
class QCheckBox;
class Element;
/**
@@ -58,17 +59,20 @@ class PlcLinkWidget : public AbstractElementPropertiesEditorWidget
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};
@@ -79,6 +83,7 @@ class PlcLinkWidget : public AbstractElementPropertiesEditorWidget
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;
+3 -3
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);
}
/**
@@ -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,14 +587,23 @@ 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);
}
}
}
@@ -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>
+94 -6
View File
@@ -5,18 +5,67 @@
#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 (info.value(QStringLiteral("auto_num_locked")).toString() == QLatin1String("true")) {
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 +95,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 +133,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 {
@@ -97,6 +174,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 +234,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
+13 -6
View File
@@ -37,9 +37,9 @@ 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_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 +49,9 @@ 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_type_cb, qOverload<int>(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::typeChanged);
disconnect(ui->m_snap_to_cb, qOverload<int>(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::enableOffsetSB);
delete ui;
}
@@ -111,6 +111,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();
}
@@ -139,6 +140,7 @@ 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.setPrefix("power", ui->m_power_prefix_le->text());
@@ -147,6 +149,7 @@ void XRefPropertiesWidget::saveProperties(int index) {
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);
}
@@ -177,6 +180,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);
@@ -191,6 +197,7 @@ 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_power_prefix_le-> setText(xrp.prefix("power"));
+50 -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>