mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-01 23:54:13 +02:00
Merge remote-tracking branch 'upstream/master' into proto-conductor-selection-properties
# Conflicts: # cmake/qet_compilation_vars.cmake # lang/qet_en.ts # sources/qetdiagrameditor.cpp
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "backupdialog.h"
|
||||
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
|
||||
/**
|
||||
@brief BackupDialog::BackupDialog
|
||||
@param parent parent widget
|
||||
*/
|
||||
BackupDialog::BackupDialog(QWidget *parent) :
|
||||
QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("Créer une copie de sauvegarde ?", "window title"));
|
||||
setFixedSize(450, 100);
|
||||
|
||||
auto main_layout = new QVBoxLayout(this);
|
||||
|
||||
auto label = new QLabel(
|
||||
tr("Souhaitez-vous créer une copie de sauvegarde ?",
|
||||
"dialog message"));
|
||||
label->setWordWrap(true);
|
||||
main_layout->addWidget(label);
|
||||
|
||||
main_layout->addStretch();
|
||||
|
||||
auto button_layout = new QHBoxLayout();
|
||||
button_layout->addStretch();
|
||||
|
||||
auto yes_button = new QPushButton(tr("Oui", "yes button"));
|
||||
auto no_button = new QPushButton(tr("Non", "no button"));
|
||||
|
||||
button_layout->addWidget(yes_button);
|
||||
button_layout->addWidget(no_button);
|
||||
main_layout->addLayout(button_layout);
|
||||
|
||||
connect(yes_button, &QPushButton::clicked, this, &QDialog::accept);
|
||||
connect(no_button, &QPushButton::clicked, this, &QDialog::reject);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief BackupDialog::~BackupDialog
|
||||
*/
|
||||
BackupDialog::~BackupDialog() = default;
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef BACKUPDIALOG_H
|
||||
#define BACKUPDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class BackupDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit BackupDialog(QWidget *parent = nullptr);
|
||||
~BackupDialog() override;
|
||||
};
|
||||
|
||||
#endif // BACKUPDIALOG_H
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -0,0 +1,277 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "conductorcolortoolbutton.h"
|
||||
|
||||
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
|
||||
#include "../diagram.h"
|
||||
#include "../diagramcontent.h"
|
||||
#include "../diagramview.h"
|
||||
#include "../lastusedstyle.h"
|
||||
#include "../qetdiagrameditor.h"
|
||||
#include "../projectview.h"
|
||||
#include "../qetgraphicsitem/conductor.h"
|
||||
#include "../conductorproperties.h"
|
||||
|
||||
#include <QColorDialog>
|
||||
#include <QMenu>
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
#include <QSet>
|
||||
|
||||
namespace {
|
||||
/**
|
||||
The colours an electrician reaches for, in the order the trade
|
||||
names them: the three phases, neutral, earth, then the ones used
|
||||
for control and extra-low-voltage circuits. Not a standard in
|
||||
itself -- IEC 60445 only fixes blue for neutral and green/yellow
|
||||
for protective earth -- but it covers the wiring a schematic
|
||||
actually shows, which is what makes the toolbar worth a click.
|
||||
*/
|
||||
struct NamedColor { const char *context_name; QColor color; };
|
||||
|
||||
QList<NamedColor> standardColors()
|
||||
{
|
||||
return {
|
||||
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Noir"), QColor(0x00, 0x00, 0x00)},
|
||||
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Marron"), QColor(0x7B, 0x3F, 0x00)},
|
||||
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Gris"), QColor(0x80, 0x80, 0x80)},
|
||||
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Bleu"), QColor(0x00, 0x00, 0xFF)},
|
||||
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Vert"), QColor(0x00, 0x80, 0x00)},
|
||||
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Rouge"), QColor(0xFF, 0x00, 0x00)},
|
||||
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Orange"), QColor(0xFF, 0x80, 0x00)},
|
||||
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Violet"), QColor(0x80, 0x00, 0x80)},
|
||||
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Blanc"), QColor(0xFF, 0xFF, 0xFF)},
|
||||
};
|
||||
}
|
||||
|
||||
const int MAX_RECENT = 6;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorColorToolButton::ConductorColorToolButton
|
||||
@param editor : the diagram editor this button acts on
|
||||
@param parent
|
||||
*/
|
||||
ConductorColorToolButton::ConductorColorToolButton(QETDiagramEditor *editor, QWidget *parent) :
|
||||
QToolButton(parent),
|
||||
m_editor(editor)
|
||||
{
|
||||
setPopupMode(QToolButton::InstantPopup);
|
||||
setToolTip(tr("Couleur de conducteur"));
|
||||
setStatusTip(tr("Applique une couleur aux conducteurs sélectionnés, et l'utilise pour les prochains conducteurs tracés",
|
||||
"status bar tip"));
|
||||
|
||||
m_current = LastUsedStyle::hasConductorColor() ? LastUsedStyle::conductorColor()
|
||||
: QColor(Qt::black);
|
||||
setMenu(new QMenu(this));
|
||||
rebuildMenu();
|
||||
setSwatch(m_current);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorColorToolButton::currentView
|
||||
@return the folio being edited, or nullptr when no project is open.
|
||||
Goes through the project view because QETDiagramEditor keeps its own
|
||||
currentDiagramView() private.
|
||||
*/
|
||||
DiagramView *ConductorColorToolButton::currentView() const
|
||||
{
|
||||
ProjectView *pv = m_editor ? m_editor->currentProjectView() : nullptr;
|
||||
return pv ? pv->currentDiagram() : nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorColorToolButton::updateEnabledState
|
||||
Greyed out when there is no folio to act on, or when the project is
|
||||
read-only -- the same rule the other conductor actions follow.
|
||||
*/
|
||||
void ConductorColorToolButton::updateEnabledState()
|
||||
{
|
||||
DiagramView *dv = currentView();
|
||||
setEnabled(dv && dv->diagram() && !dv->diagram()->isReadOnly());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorColorToolButton::rebuildMenu
|
||||
The standard colours never change; the recent ones do, so the menu is
|
||||
rebuilt rather than kept in sync entry by entry.
|
||||
*/
|
||||
void ConductorColorToolButton::rebuildMenu()
|
||||
{
|
||||
QMenu *m = menu();
|
||||
m->clear();
|
||||
|
||||
for (const auto &nc : standardColors())
|
||||
{
|
||||
const QColor c = nc.color;
|
||||
QAction *a = m->addAction(swatchIcon(c),
|
||||
tr(nc.context_name));
|
||||
connect(a, &QAction::triggered, this, [this, c]() { applyColor(c); });
|
||||
}
|
||||
|
||||
if (!m_recent.isEmpty())
|
||||
{
|
||||
m->addSeparator();
|
||||
QAction *title = m->addAction(tr("Récemment utilisées"));
|
||||
title->setEnabled(false);
|
||||
for (const QColor &c : std::as_const(m_recent))
|
||||
{
|
||||
QAction *a = m->addAction(swatchIcon(c), c.name());
|
||||
connect(a, &QAction::triggered, this, [this, c]() { applyColor(c); });
|
||||
}
|
||||
}
|
||||
|
||||
m->addSeparator();
|
||||
QAction *other = m->addAction(tr("Autre couleur…"));
|
||||
connect(other, &QAction::triggered, this, &ConductorColorToolButton::chooseOtherColor);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorColorToolButton::applyColor
|
||||
Recolour the selected conductors, and remember the colour for the next
|
||||
one drawn.
|
||||
@param color
|
||||
*/
|
||||
void ConductorColorToolButton::applyColor(const QColor &color)
|
||||
{
|
||||
if (!color.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Always: this is the half that works with nothing selected.
|
||||
LastUsedStyle::setConductorColor(color);
|
||||
rememberRecent(color);
|
||||
setSwatch(color);
|
||||
|
||||
DiagramView *dv = currentView();
|
||||
if (!dv) {
|
||||
return;
|
||||
}
|
||||
Diagram *diagram = dv->diagram();
|
||||
if (!diagram || diagram->isReadOnly()) {
|
||||
return;
|
||||
}
|
||||
|
||||
DiagramContent dc(diagram);
|
||||
const auto selected_conductors = dc.conductors(DiagramContent::AnyConductor);
|
||||
if (selected_conductors.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
//A wire drawn across a junction is several Conductor objects
|
||||
//sharing one electrical potential; selecting one segment must
|
||||
//still recolour the whole potential, the way the F2 dialog's
|
||||
//"apply to all" already does.
|
||||
QSet<Conductor *> conductors;
|
||||
for (Conductor *conductor : selected_conductors)
|
||||
{
|
||||
conductors << conductor;
|
||||
conductors += conductor->relatedPotentialConductors();
|
||||
}
|
||||
|
||||
QUndoCommand *undo = new QUndoCommand(tr("Modifier la couleur de %n conducteur(s)",
|
||||
"undo caption", conductors.count()));
|
||||
int changed = 0;
|
||||
for (Conductor *conductor : conductors)
|
||||
{
|
||||
ConductorProperties before = conductor->properties();
|
||||
if (before.color == color) {
|
||||
continue;
|
||||
}
|
||||
ConductorProperties after = before;
|
||||
after.color = color;
|
||||
|
||||
QVariant old_value, new_value;
|
||||
old_value.setValue(before);
|
||||
new_value.setValue(after);
|
||||
new QPropertyUndoCommand(conductor, "properties", old_value, new_value, undo);
|
||||
++changed;
|
||||
}
|
||||
|
||||
//Every selected conductor was already this colour: pushing an
|
||||
//empty command would put a no-op step in the undo stack.
|
||||
if (changed) {
|
||||
diagram->undoStack().push(undo);
|
||||
} else {
|
||||
delete undo;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorColorToolButton::chooseOtherColor
|
||||
*/
|
||||
void ConductorColorToolButton::chooseOtherColor()
|
||||
{
|
||||
const QColor c = QColorDialog::getColor(m_current, this,
|
||||
tr("Choisir une couleur de conducteur"));
|
||||
if (c.isValid()) {
|
||||
applyColor(c);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorColorToolButton::rememberRecent
|
||||
Most recent first, no duplicates, capped.
|
||||
@param color
|
||||
*/
|
||||
void ConductorColorToolButton::rememberRecent(const QColor &color)
|
||||
{
|
||||
//A colour that is already one row up in the standard list would
|
||||
//only appear twice, under a hex name it does not need.
|
||||
for (const auto &nc : standardColors()) {
|
||||
if (nc.color == color) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_recent.removeAll(color);
|
||||
m_recent.prepend(color);
|
||||
while (m_recent.size() > MAX_RECENT) {
|
||||
m_recent.removeLast();
|
||||
}
|
||||
rebuildMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorColorToolButton::setSwatch
|
||||
@param color
|
||||
*/
|
||||
void ConductorColorToolButton::setSwatch(const QColor &color)
|
||||
{
|
||||
m_current = color;
|
||||
setIcon(swatchIcon(color));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorColorToolButton::swatchIcon
|
||||
@param color
|
||||
@return a plain square of that colour, outlined so that white and very
|
||||
light colours are still visible against the toolbar.
|
||||
*/
|
||||
QIcon ConductorColorToolButton::swatchIcon(const QColor &color)
|
||||
{
|
||||
QPixmap pix(16, 16);
|
||||
pix.fill(Qt::transparent);
|
||||
QPainter p(&pix);
|
||||
p.setRenderHint(QPainter::Antialiasing, false);
|
||||
p.setBrush(color);
|
||||
p.setPen(QPen(QColor(0x40, 0x40, 0x40), 1));
|
||||
p.drawRect(0, 0, 15, 15);
|
||||
p.end();
|
||||
return QIcon(pix);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
|
||||
@@ -30,7 +30,9 @@
|
||||
#include "../reportpropertiewidget.h"
|
||||
#include "../titleblockpropertieswidget.h"
|
||||
#include "../xrefpropertieswidget.h"
|
||||
|
||||
#include "guidespropertieswidget.h"
|
||||
#include "../autoNum/numerotationcontext.h"
|
||||
#include "../autoNum/ui/selectautonumw.h"
|
||||
#include <QFont>
|
||||
#include <QFontDialog>
|
||||
#include <QSizePolicy>
|
||||
@@ -73,6 +75,59 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
|
||||
rpw = new ReportPropertieWidget(ReportProperties::defaultProperties());
|
||||
// default properties of xref
|
||||
xrefpw = new XRefPropertiesWidget(XRefProperties::defaultProperties(), this);
|
||||
// default guides properties
|
||||
m_gpw = new GuidesPropertiesWidget(this);
|
||||
|
||||
QSettings settings;
|
||||
QList<Diagram::Guide> loaded_guides;
|
||||
if (m_project) {
|
||||
for (const auto &pg : m_project->defaultGuides()) {
|
||||
Diagram::Guide g;
|
||||
g.orientation = static_cast<Diagram::Guide::Orientation>(pg.orientation);
|
||||
g.position = pg.position;
|
||||
g.color = pg.color;
|
||||
loaded_guides.append(g);
|
||||
}
|
||||
} else {
|
||||
QSettings settings;
|
||||
int size = settings.beginReadArray(QStringLiteral("diagrameditor/defaultguides"));
|
||||
for (int i = 0; i < size; ++i) {
|
||||
settings.setArrayIndex(i);
|
||||
Diagram::Guide g;
|
||||
g.orientation = static_cast<Diagram::Guide::Orientation>(settings.value(QStringLiteral("orientation"), 0).toInt());
|
||||
g.position = settings.value(QStringLiteral("position"), 0.0).toReal();
|
||||
g.color = QColor(settings.value(QStringLiteral("color"), QStringLiteral("#ff0000")).toString());
|
||||
loaded_guides.append(g);
|
||||
}
|
||||
settings.endArray();
|
||||
}
|
||||
m_gpw->setGuides(loaded_guides);
|
||||
|
||||
// global auto-numbering defaults (only when editing global settings, not a project)
|
||||
if (!m_project) {
|
||||
auto saw_conductor = new SelectAutonumW(1);
|
||||
auto saw_element = new SelectAutonumW(0);
|
||||
auto saw_folio = new SelectAutonumW(2);
|
||||
|
||||
initAutoNumTab(m_autonum_conductor, saw_conductor, QStringLiteral("autonum/conductor"));
|
||||
initAutoNumTab(m_autonum_element, saw_element, QStringLiteral("autonum/element"));
|
||||
initAutoNumTab(m_autonum_folio, saw_folio, QStringLiteral("autonum/folio"));
|
||||
|
||||
QSettings autonum_settings;
|
||||
loadAutoNumTab(m_autonum_conductor, autonum_settings);
|
||||
loadAutoNumTab(m_autonum_element, autonum_settings);
|
||||
loadAutoNumTab(m_autonum_folio, autonum_settings);
|
||||
|
||||
// Intercept Return key in the combo line edits so it doesn't
|
||||
// activate the dialog's default button (OK).
|
||||
for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) {
|
||||
if (QComboBox *combo = tab->widget->contextComboBox()) {
|
||||
if (combo->lineEdit()) {
|
||||
combo->lineEdit()->installEventFilter(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//If there is a project, we edit his properties
|
||||
if (m_project) {
|
||||
@@ -83,10 +138,11 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
|
||||
xrefpw -> setProperties (m_project -> defaultXRefProperties());
|
||||
}
|
||||
|
||||
connect(ipw,SIGNAL(openAutoNumFolioEditor(QString)),this,SLOT(changeToAutoFolioTab()));
|
||||
connect(ipw, &TitleBlockPropertiesWidget::openAutoNumFolioEditor, this, &NewDiagramPage::changeToAutoFolioTab);
|
||||
|
||||
// main tab widget
|
||||
QTabWidget *tab_widget = new QTabWidget(this);
|
||||
m_tab_widget = tab_widget;
|
||||
QWidget *diagram_widget = new QWidget();
|
||||
QVBoxLayout *diagram_layout = new QVBoxLayout(diagram_widget);
|
||||
|
||||
@@ -98,6 +154,20 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
|
||||
tab_widget -> addTab (m_cpw, tr("Conducteur"));
|
||||
tab_widget -> addTab (rpw, tr("Reports de folio"));
|
||||
tab_widget -> addTab (xrefpw, tr("Références croisées"));
|
||||
tab_widget -> addTab (m_gpw, tr("Guides"));
|
||||
|
||||
// add auto-numbering tab only for global settings (not per project)
|
||||
if (!m_project) {
|
||||
QWidget *autonum_widget = new QWidget();
|
||||
QVBoxLayout *autonum_layout = new QVBoxLayout(autonum_widget);
|
||||
autonum_layout->addWidget(new QLabel(tr("Définir les règles de numérotation automatique par défaut pour les nouveaux projets :")));
|
||||
QTabWidget *autonum_inner_tab = new QTabWidget();
|
||||
autonum_inner_tab->addTab(m_autonum_conductor.widget, tr("Conducteurs"));
|
||||
autonum_inner_tab->addTab(m_autonum_element.widget, tr("Eléments"));
|
||||
autonum_inner_tab->addTab(m_autonum_folio.widget, tr("Folios"));
|
||||
autonum_layout->addWidget(autonum_inner_tab);
|
||||
tab_widget -> addTab (autonum_widget, tr("Numérotation auto"));
|
||||
}
|
||||
|
||||
QVBoxLayout *vlayout1 = new QVBoxLayout();
|
||||
vlayout1->addWidget(tab_widget);
|
||||
@@ -110,7 +180,7 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
|
||||
*/
|
||||
NewDiagramPage::~NewDiagramPage()
|
||||
{
|
||||
disconnect(ipw,SIGNAL(openAutoNumFolioEditor(QString)),this,SLOT(changeToAutoFolioTab()));
|
||||
disconnect(ipw, &TitleBlockPropertiesWidget::openAutoNumFolioEditor, this, &NewDiagramPage::changeToAutoFolioTab);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -155,6 +225,17 @@ void NewDiagramPage::applyConf()
|
||||
modified_project = true;
|
||||
}
|
||||
|
||||
QList<GuideProperties> proj_guides;
|
||||
for (const auto &g : m_gpw->guides()) {
|
||||
GuideProperties pg;
|
||||
pg.orientation = static_cast<int>(g.orientation);
|
||||
pg.position = g.position;
|
||||
pg.color = g.color;
|
||||
proj_guides.append(pg);
|
||||
}
|
||||
m_project->setDefaultGuides(proj_guides);
|
||||
modified_project = true;
|
||||
|
||||
if (modified_project) {
|
||||
m_project -> setModified(modified_project);
|
||||
}
|
||||
@@ -175,14 +256,26 @@ void NewDiagramPage::applyConf()
|
||||
rpw->toSettings(settings, "diagrameditor/defaultreport");
|
||||
|
||||
// default xref properties
|
||||
QHash <QString, XRefProperties> hash_xrp = xrefpw -> properties();
|
||||
foreach (QString key, hash_xrp.keys()) {
|
||||
XRefProperties xrp = hash_xrp[key];
|
||||
QString str("diagrameditor/defaultxref");
|
||||
xrp.toSettings(settings, str += key);
|
||||
const QHash<QString, XRefProperties> hash_xrp = xrefpw->properties();
|
||||
for (auto it = hash_xrp.constBegin() ; it != hash_xrp.constEnd() ; ++it) {
|
||||
it.value().toSettings(settings,
|
||||
QStringLiteral("diagrameditor/defaultxref") % it.key());
|
||||
}
|
||||
}
|
||||
|
||||
// Global in QSettings speichern
|
||||
QList<Diagram::Guide> current_guides = m_gpw->guides();
|
||||
settings.beginWriteArray(QStringLiteral("diagrameditor/defaultguides"));
|
||||
for (int i = 0; i < current_guides.size(); ++i) {
|
||||
settings.setArrayIndex(i);
|
||||
settings.setValue(QStringLiteral("orientation"), static_cast<int>(current_guides[i].orientation));
|
||||
settings.setValue(QStringLiteral("position"), current_guides[i].position);
|
||||
settings.setValue(QStringLiteral("color"), current_guides[i].color.name());
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
// save global auto-numbering defaults
|
||||
persistAutonumSettings();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -247,6 +340,134 @@ void NewDiagramPage::loadSavedTbp()
|
||||
applyConf();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::isPlaceholder
|
||||
Return true if @a name matches the combo box's built-in placeholder text
|
||||
(first item). This is locale-independent because it reads the actual item text.
|
||||
*/
|
||||
bool NewDiagramPage::isPlaceholder(QComboBox *combo, const QString &name)
|
||||
{
|
||||
return !combo->count() || name == combo->itemText(0);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::initAutoNumTab
|
||||
Initialise an AutoNumTab struct and connect its signals.
|
||||
*/
|
||||
void NewDiagramPage::initAutoNumTab(AutoNumTab &tab, SelectAutonumW *w, const QString &prefix)
|
||||
{
|
||||
tab.widget = w;
|
||||
tab.prefix = prefix;
|
||||
|
||||
connect(w, &SelectAutonumW::applyPressed, this, [this, &tab]() { saveAutoNumContext(tab); });
|
||||
connect(w, &SelectAutonumW::removeClicked, this, [this, &tab]() { removeAutoNumContext(tab); });
|
||||
connect(w->contextComboBox(), &QComboBox::activated, this, [this, &tab](int index) {
|
||||
if (index >= 0) {
|
||||
QString name = tab.widget->contextComboBox()->itemText(index);
|
||||
if (tab.contexts.contains(name)) {
|
||||
tab.widget->setContext(tab.contexts.value(name));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::loadAutoNumTab
|
||||
Load saved rules from QSettings into an AutoNumTab.
|
||||
*/
|
||||
void NewDiagramPage::loadAutoNumTab(AutoNumTab &tab, QSettings &settings)
|
||||
{
|
||||
auto data = NumerotationContext::loadFromSettings(settings, tab.prefix);
|
||||
tab.contexts = data.first;
|
||||
for (auto it = tab.contexts.constBegin(); it != tab.contexts.constEnd(); ++it) {
|
||||
tab.widget->contextComboBox()->addItem(it.key());
|
||||
}
|
||||
if (!tab.contexts.isEmpty() && !data.second.isEmpty()
|
||||
&& tab.contexts.contains(data.second)) {
|
||||
tab.widget->contextComboBox()->setCurrentText(data.second);
|
||||
tab.widget->setContext(tab.contexts.value(data.second));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::saveAutoNumContext
|
||||
Save the current context from an AutoNumTab's widget into its hash and
|
||||
persist to QSettings immediately.
|
||||
*/
|
||||
void NewDiagramPage::saveAutoNumContext(AutoNumTab &tab)
|
||||
{
|
||||
QString name = tab.widget->contextComboBox()->currentText().trimmed();
|
||||
if (name.isEmpty() || isPlaceholder(tab.widget->contextComboBox(), name)) {
|
||||
return;
|
||||
}
|
||||
tab.contexts.insert(name, tab.widget->toNumContext());
|
||||
if (tab.widget->contextComboBox()->findText(name) == -1) {
|
||||
tab.widget->contextComboBox()->addItem(name);
|
||||
}
|
||||
persistAutonumSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::removeAutoNumContext
|
||||
Remove the current context from an AutoNumTab's hash and persist.
|
||||
*/
|
||||
void NewDiagramPage::removeAutoNumContext(AutoNumTab &tab)
|
||||
{
|
||||
QString name = tab.widget->contextComboBox()->currentText().trimmed();
|
||||
if (name.isEmpty() || isPlaceholder(tab.widget->contextComboBox(), name)) {
|
||||
return;
|
||||
}
|
||||
int idx = tab.widget->contextComboBox()->findText(name);
|
||||
if (idx == -1) return;
|
||||
tab.contexts.remove(name);
|
||||
tab.widget->contextComboBox()->removeItem(idx);
|
||||
tab.widget->contextComboBox()->setCurrentText(QString());
|
||||
tab.widget->setContext(NumerotationContext());
|
||||
persistAutonumSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::persistAutonumSettings
|
||||
Save all autonum contexts to QSettings immediately.
|
||||
*/
|
||||
void NewDiagramPage::persistAutonumSettings()
|
||||
{
|
||||
QSettings settings;
|
||||
for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) {
|
||||
QString current;
|
||||
QComboBox *combo = tab->widget->contextComboBox();
|
||||
if (!isPlaceholder(combo, combo->currentText().trimmed())
|
||||
&& tab->contexts.contains(combo->currentText().trimmed())) {
|
||||
current = combo->currentText().trimmed();
|
||||
}
|
||||
NumerotationContext::saveToSettings(tab->contexts, current,
|
||||
settings, tab->prefix);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::eventFilter
|
||||
Intercept Return/Enter in combo box line edits so it doesn't close the
|
||||
settings dialog.
|
||||
*/
|
||||
bool NewDiagramPage::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::KeyPress) {
|
||||
auto *ke = static_cast<QKeyEvent *>(event);
|
||||
if ((ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter)) {
|
||||
// Check if this is a line edit inside one of our autonum combos
|
||||
for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) {
|
||||
if (QComboBox *combo = tab->widget->contextComboBox()) {
|
||||
if (combo->lineEdit() && combo->lineEdit() == obj) {
|
||||
return true; // eat the event
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ConfigPage::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
/**
|
||||
Constructeur
|
||||
@param parent QWidget parent
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "configpage.h"
|
||||
#include "../projectpropertiesdialog.h"
|
||||
#include "../titleblockpropertieswidget.h"
|
||||
#include "../autoNum/numerotationcontext.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QtWidgets>
|
||||
@@ -29,8 +30,10 @@ class TitleBlockPropertiesWidget;
|
||||
class ExportPropertiesWidget;
|
||||
class ReportPropertieWidget;
|
||||
class XRefPropertiesWidget;
|
||||
class GuidesPropertiesWidget;
|
||||
class QETProject;
|
||||
class TitleBlockProperties;
|
||||
class SelectAutonumW;
|
||||
|
||||
/**
|
||||
@brief The NewDiagramPage class
|
||||
@@ -47,6 +50,7 @@ class NewDiagramPage : public ConfigPage {
|
||||
~NewDiagramPage() override;
|
||||
private:
|
||||
NewDiagramPage(const NewDiagramPage &);
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
public slots:
|
||||
void changeToAutoFolioTab();
|
||||
void setFolioAutonum(QString);
|
||||
@@ -69,8 +73,27 @@ public slots:
|
||||
ConductorPropertiesWidget *m_cpw; ///< Widget to edit default conductor properties
|
||||
ReportPropertieWidget *rpw; ///< Widget to edit default report label
|
||||
XRefPropertiesWidget *xrefpw; ///< Widget to edit default xref properties
|
||||
GuidesPropertiesWidget *m_gpw; ///< Widget to edit guides
|
||||
TitleBlockProperties savedTbp; ///< Used to save current TBP and retrieve later
|
||||
QTabWidget *m_tab_widget; ///< Main tab widget (stored for later access)
|
||||
|
||||
// auto-numbering tab data
|
||||
struct AutoNumTab {
|
||||
SelectAutonumW *widget = nullptr;
|
||||
QHash<QString, NumerotationContext> contexts;
|
||||
QString prefix;
|
||||
};
|
||||
AutoNumTab m_autonum_conductor;
|
||||
AutoNumTab m_autonum_element;
|
||||
AutoNumTab m_autonum_folio;
|
||||
|
||||
void initAutoNumTab(AutoNumTab &tab, SelectAutonumW *w, const QString &prefix);
|
||||
void loadAutoNumTab(AutoNumTab &tab, QSettings &settings);
|
||||
void saveAutoNumContext(AutoNumTab &tab);
|
||||
void removeAutoNumContext(AutoNumTab &tab);
|
||||
void persistAutonumSettings();
|
||||
|
||||
static bool isPlaceholder(QComboBox *combo, const QString &name);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -21,8 +21,10 @@
|
||||
#include "../../qeticons.h"
|
||||
#include "ui_generalconfigurationpage.h"
|
||||
#include "../../utils/qetsettings.h"
|
||||
#include "../../utils/qetutils.h"
|
||||
#include "../../qetmessagebox.h"
|
||||
|
||||
#include "../../textgrid.h"
|
||||
#include "../nokde/kcolorbutton.h"
|
||||
#include <QFileDialog>
|
||||
#include <QFontDialog>
|
||||
#include <QSettings>
|
||||
@@ -40,9 +42,6 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
|
||||
QSettings settings;
|
||||
|
||||
//Appearance tab
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ###Qt 6:remove
|
||||
ui->m_hdpi_round_policy_widget->setDisabled(true);
|
||||
#else
|
||||
ui->m_hdpi_round_policy_cb->addItem(tr("Arrondi supérieur pour 0.5 et plus"), QLatin1String("Round"));
|
||||
ui->m_hdpi_round_policy_cb->addItem(tr("Toujours arrondi supérieur"), QLatin1String("Ceil"));
|
||||
ui->m_hdpi_round_policy_cb->addItem(tr("Toujours arrondi inférieur"), QLatin1String("Floor"));
|
||||
@@ -65,9 +64,20 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
|
||||
ui->m_hdpi_round_policy_cb->setCurrentIndex(4);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
ui->grid_startup_cb->setChecked(settings.value("diagrameditor/grid_display_startup", true).toBool());
|
||||
ui->guides_startup_cb->setChecked(settings.value("diagrameditor/guides_display_startup", false).toBool());
|
||||
ui->DiagramEditor_xGrid_sb->setValue(settings.value("diagrameditor/Xgrid", 10).toInt());
|
||||
ui->DiagramEditor_yGrid_sb->setValue(settings.value("diagrameditor/Ygrid", 10).toInt());
|
||||
for (const qreal divisor : TextGrid::divisors)
|
||||
ui->DiagramEditor_textGrid_cb->addItem(
|
||||
divisor > 0 ? TextGrid::ratioLabel(divisor) : tr("Désactivée"),
|
||||
divisor);
|
||||
int text_grid_index = ui->DiagramEditor_textGrid_cb->findData(
|
||||
settings.value(TextGrid::settings_key, 1).toReal());
|
||||
if (text_grid_index < 0)
|
||||
text_grid_index = ui->DiagramEditor_textGrid_cb->findData(qreal(1));
|
||||
ui->DiagramEditor_textGrid_cb->setCurrentIndex(text_grid_index);
|
||||
ui->DiagramEditor_xKeyGrid_sb->setValue(settings.value("diagrameditor/key_Xgrid", 10).toInt());
|
||||
ui->DiagramEditor_yKeyGrid_sb->setValue(settings.value("diagrameditor/key_Ygrid", 10).toInt());
|
||||
ui->DiagramEditor_xKeyGridFine_sb->setValue(settings.value("diagrameditor/key_fine_Xgrid", 1).toInt());
|
||||
@@ -75,6 +85,12 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
|
||||
ui->DiagramEditor_Grid_PointSize_min_sb->setValue(settings.value("diagrameditor/grid_pointsize_min", 1).toInt());
|
||||
ui->DiagramEditor_Grid_PointSize_max_sb->setValue(settings.value("diagrameditor/grid_pointsize_max", 1).toInt());
|
||||
ui->m_use_system_color_cb->setChecked(settings.value("usesystemcolors", "true").toBool());
|
||||
bool sysColors = ui->m_use_system_color_cb->isChecked();
|
||||
ui->m_custom_app_color_kpb->setEnabled(!sysColors);
|
||||
if (settings.contains("customapplicationcolor"))
|
||||
ui->m_custom_app_color_kpb->setColor(QColor(settings.value("customapplicationcolor").toString()));
|
||||
else
|
||||
ui->m_custom_app_color_kpb->setColor(QApplication::palette().color(QPalette::Window));
|
||||
bool tabbed = settings.value("diagrameditor/viewmode", "tabbed") == "tabbed";
|
||||
if(tabbed)
|
||||
ui->m_use_tab_mode_rb->setChecked(true);
|
||||
@@ -83,6 +99,25 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
|
||||
ui->m_zoom_out_beyond_folio->setChecked(settings.value("diagrameditor/zoom-out-beyond-of-folio", false).toBool());
|
||||
ui->m_use_gesture_trackpad->setChecked(settings.value("diagramview/gestures", false).toBool());
|
||||
ui->m_save_label_paste->setChecked(settings.value("diagramcommands/erase-label-on-copy", true).toBool());
|
||||
ui->m_enable_scripting->setChecked(QetSettings::scriptingEnabled());
|
||||
#ifdef QET_HAS_SCRIPTING
|
||||
if (QetSettings::scriptingForcedByEnvironment()) {
|
||||
//QET_ENABLE_SCRIPTING wins over the stored value, so let the box
|
||||
//say so rather than offer a tick that changes nothing.
|
||||
ui->m_enable_scripting->setEnabled(false);
|
||||
ui->m_enable_scripting->setToolTip(
|
||||
tr("Activé par la variable d'environnement "
|
||||
"QET_ENABLE_SCRIPTING ; ce réglage est sans effet "
|
||||
"tant qu'elle est définie."));
|
||||
}
|
||||
#else
|
||||
//Built without Qt Qml: there is no scripting to allow. Disabled as
|
||||
//well as hidden, so applyConf() leaves the stored value alone --
|
||||
//a hidden box still reports its state, and writing it here would
|
||||
//quietly clear a preference set on a build that does have Qml.
|
||||
ui->m_enable_scripting->setVisible(false);
|
||||
ui->m_enable_scripting->setEnabled(false);
|
||||
#endif
|
||||
ui->m_use_folio_label->setChecked(settings.value("genericpanel/folio", true).toBool());
|
||||
ui->m_border_0->setChecked(settings.value("border-columns_0", false).toBool());
|
||||
ui->m_autosave_sb->setValue(settings.value("diagrameditor/autosave-interval", 0).toInt());
|
||||
@@ -99,7 +134,7 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
|
||||
if (settings.contains("diagrameditor/dynamic_text_font"))
|
||||
{
|
||||
QFont font;
|
||||
font.fromString(settings.value("diagrameditor/dynamic_text_font").toString());
|
||||
QETUtils::fontFromString(font, settings.value("diagrameditor/dynamic_text_font").toString());
|
||||
|
||||
QString fontInfos = font.family() + " " +
|
||||
QString::number(font.pointSize()) + " (" +
|
||||
@@ -112,7 +147,7 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
|
||||
if (settings.contains("diagrameditor/independent_text_font"))
|
||||
{
|
||||
QFont font;
|
||||
font.fromString(settings.value("diagrameditor/independent_text_font").toString());
|
||||
QETUtils::fontFromString(font, settings.value("diagrameditor/independent_text_font").toString());
|
||||
|
||||
QString fontInfos = font.family() + " " +
|
||||
QString::number(font.pointSize()) + " (" +
|
||||
@@ -207,16 +242,24 @@ void GeneralConfigurationPage::applyConf()
|
||||
bool must_use_system_colors = ui->m_use_system_color_cb->isChecked();
|
||||
settings.setValue("usesystemcolors", must_use_system_colors);
|
||||
if (was_using_system_colors != must_use_system_colors) {
|
||||
QETApp::instance()->useSystemPalette(must_use_system_colors);
|
||||
if (must_use_system_colors) {
|
||||
QETApp::instance()->useSystemPalette(true);
|
||||
} else {
|
||||
QColor custom_color = ui->m_custom_app_color_kpb->color();
|
||||
settings.setValue("customapplicationcolor", custom_color.name());
|
||||
QETApp::instance()->useCustomPalette(custom_color);
|
||||
}
|
||||
} else if (!must_use_system_colors) {
|
||||
QColor custom_color = ui->m_custom_app_color_kpb->color();
|
||||
settings.setValue("customapplicationcolor", custom_color.name());
|
||||
QETApp::instance()->useCustomPalette(custom_color);
|
||||
}
|
||||
settings.setValue("border-columns_0",ui->m_border_0->isChecked());
|
||||
settings.setValue("lang", ui->m_lang_cb->itemData(ui->m_lang_cb->currentIndex()).toString());
|
||||
|
||||
//hdpi
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
QetSettings::setHdpiScaleFactorRoundingPolicy(ui->m_hdpi_round_policy_cb->currentData().toString());
|
||||
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFactorRoundingPolicy());
|
||||
#endif
|
||||
|
||||
//ELEMENT EDITOR
|
||||
settings.setValue("elementeditor/default-informations", ui->m_default_elements_info->toPlainText());
|
||||
@@ -230,6 +273,14 @@ void GeneralConfigurationPage::applyConf()
|
||||
//DIAGRAM COMMAND
|
||||
settings.setValue("diagramcommands/erase-label-on-copy", ui->m_save_label_paste->isChecked());
|
||||
|
||||
//SCRIPTING
|
||||
//Left alone while the environment forces it on: the box is disabled
|
||||
//in that case and writing its state would silently clear the user's
|
||||
//real preference the first time this dialog is accepted.
|
||||
if (ui->m_enable_scripting->isEnabled()) {
|
||||
QetSettings::setScriptingEnabled(ui->m_enable_scripting->isChecked());
|
||||
}
|
||||
|
||||
//GENERIC PANEL
|
||||
settings.setValue("genericpanel/folio",ui->m_use_folio_label->isChecked());
|
||||
|
||||
@@ -240,9 +291,13 @@ void GeneralConfigurationPage::applyConf()
|
||||
settings.setValue("diagrameditor/highlight-integrated-elements", ui->m_highlight_integrated_elements->isChecked());
|
||||
settings.setValue("diagrameditor/zoom-out-beyond-of-folio", ui->m_zoom_out_beyond_folio->isChecked());
|
||||
settings.setValue("diagrameditor/autosave-interval", ui->m_autosave_sb->value());
|
||||
|
||||
settings.setValue("diagrameditor/grid_display_startup", ui->grid_startup_cb->isChecked());
|
||||
settings.setValue("diagrameditor/guides_display_startup", ui->guides_startup_cb->isChecked());
|
||||
//Grid step and key navigation
|
||||
settings.setValue("diagrameditor/Xgrid", ui->DiagramEditor_xGrid_sb->value());
|
||||
settings.setValue("diagrameditor/Ygrid", ui->DiagramEditor_yGrid_sb->value());
|
||||
settings.setValue(TextGrid::settings_key, ui->DiagramEditor_textGrid_cb->currentData());
|
||||
settings.setValue("diagrameditor/key_Xgrid", ui->DiagramEditor_xKeyGrid_sb->value());
|
||||
settings.setValue("diagrameditor/key_Ygrid", ui->DiagramEditor_yKeyGrid_sb->value());
|
||||
settings.setValue("diagrameditor/key_fine_Xgrid", ui->DiagramEditor_xKeyGridFine_sb->value());
|
||||
@@ -388,7 +443,7 @@ void GeneralConfigurationPage::fillLang()
|
||||
ui->m_lang_cb->addItem(QET::Icons::hr, tr("Croate"), "hr");
|
||||
ui->m_lang_cb->addItem(QET::Icons::it, tr("Italien"), "it");
|
||||
ui->m_lang_cb->addItem(QET::Icons::jp, tr("Japonais"), "ja");
|
||||
ui->m_lang_cb->addItem(QET::Icons::ko, tr("Coréen"), "ko");
|
||||
ui->m_lang_cb->addItem(QET::Icons::ko, tr("Coréen"), "ko");
|
||||
ui->m_lang_cb->addItem(QET::Icons::pl, tr("Polonais"), "pl");
|
||||
ui->m_lang_cb->addItem(QET::Icons::pt, tr("Portugais"), "pt");
|
||||
ui->m_lang_cb->addItem(QET::Icons::ro, tr("Roumains"), "ro");
|
||||
@@ -400,9 +455,9 @@ void GeneralConfigurationPage::fillLang()
|
||||
ui->m_lang_cb->addItem(QET::Icons::tr, tr("Turc"), "tr");
|
||||
ui->m_lang_cb->addItem(QET::Icons::hu, tr("Hongrois"), "hu");
|
||||
ui->m_lang_cb->addItem(QET::Icons::mn, tr("Mongol"), "mn");
|
||||
ui->m_lang_cb->addItem(QET::Icons::uk, tr("Ukrainien"), "uk");
|
||||
ui->m_lang_cb->addItem(QET::Icons::zh, tr("Chinois"), "zh");
|
||||
ui->m_lang_cb->addItem(QET::Icons::se, tr("Suédois"), "sv");
|
||||
ui->m_lang_cb->addItem(QET::Icons::uk, tr("Ukrainien"), "uk");
|
||||
ui->m_lang_cb->addItem(QET::Icons::zh, tr("Chinois"), "zh");
|
||||
ui->m_lang_cb->addItem(QET::Icons::se, tr("Suédois"), "sv");
|
||||
//set current index to the lang found in setting file
|
||||
//if lang doesn't exist set to system
|
||||
QSettings settings;
|
||||
@@ -451,11 +506,11 @@ void GeneralConfigurationPage::on_m_dyn_text_font_pb_clicked()
|
||||
bool ok;
|
||||
QSettings settings;
|
||||
QFont curFont;
|
||||
curFont.fromString(settings.value("diagrameditor/dynamic_text_font", "Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular").toString());
|
||||
QETUtils::fontFromString(curFont, settings.value("diagrameditor/dynamic_text_font", "Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular").toString());
|
||||
QFont font = QFontDialog::getFont(&ok, curFont, this);
|
||||
if (ok)
|
||||
{
|
||||
settings.setValue("diagrameditor/dynamic_text_font", font.toString());
|
||||
settings.setValue("diagrameditor/dynamic_text_font", QETUtils::fontToString(font));
|
||||
QString fontInfos = font.family() + " " +
|
||||
QString::number(font.pointSize()) + " (" +
|
||||
font.styleName() + ")";
|
||||
@@ -555,11 +610,11 @@ void GeneralConfigurationPage::on_m_indi_text_font_pb_clicked()
|
||||
bool ok;
|
||||
QSettings settings;
|
||||
QFont curFont;
|
||||
curFont.fromString(settings.value("diagrameditor/independent_text_font", "Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular").toString());
|
||||
QETUtils::fontFromString(curFont, settings.value("diagrameditor/independent_text_font", "Liberation Sans,9,-1,5,50,0,0,0,0,0,Regular").toString());
|
||||
QFont font = QFontDialog::getFont(&ok, curFont, this);
|
||||
if (ok)
|
||||
{
|
||||
settings.setValue("diagrameditor/independent_text_font", font.toString());
|
||||
settings.setValue("diagrameditor/independent_text_font", QETUtils::fontToString(font));
|
||||
QString fontInfos = font.family() + " " +
|
||||
QString::number(font.pointSize()) + " (" +
|
||||
font.styleName() + ")";
|
||||
@@ -625,3 +680,14 @@ void GeneralConfigurationPage::on_m_hdpi_round_cb_clicked(bool checked)
|
||||
ui->m_hdpi_round_policy_cb->setEnabled(checked);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief GeneralConfigurationPage::on_m_use_system_color_cb_toggled
|
||||
Enable/disable the custom color picker when the system color
|
||||
checkbox is toggled.
|
||||
@param checked
|
||||
*/
|
||||
void GeneralConfigurationPage::on_m_use_system_color_cb_toggled(bool checked)
|
||||
{
|
||||
ui->m_custom_app_color_kpb->setEnabled(!checked);
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ class GeneralConfigurationPage : public ConfigPage
|
||||
void on_ElementEditor_Grid_PointSize_min_sb_valueChanged(int value);
|
||||
|
||||
void on_m_hdpi_round_cb_clicked(bool checked);
|
||||
void on_m_use_system_color_cb_toggled(bool checked);
|
||||
|
||||
private:
|
||||
void fillLang();
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>955</width>
|
||||
<height>556</height>
|
||||
<height>570</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
@@ -17,21 +17,32 @@
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>2</number>
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="tab_3">
|
||||
<attribute name="title">
|
||||
<string>Apparence</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="m_use_system_color_cb">
|
||||
<property name="text">
|
||||
<string>Utiliser les couleurs du système</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="m_system_color_layout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="m_use_system_color_cb">
|
||||
<property name="text">
|
||||
<string>Utiliser les couleurs du système</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="KColorButton" name="m_custom_app_color_kpb">
|
||||
<property name="toolTip">
|
||||
<string>Couleur de l'application</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
@@ -59,6 +70,27 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="grid_startup_cb">
|
||||
<property name="text">
|
||||
<string>Afficher la grille par défaut (appliqué au prochain lancement)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="guides_startup_cb">
|
||||
<property name="text">
|
||||
<string>Afficher les guides par défaut (appliqué au prochain lancement)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="Line" name="line_5">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="m_use_windows_mode_rb">
|
||||
<property name="text">
|
||||
@@ -186,6 +218,16 @@
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QCheckBox" name="m_enable_scripting">
|
||||
<property name="text">
|
||||
<string>Autoriser l'exécution de scripts JavaScript (Projet > Exécuter un script, et --run)</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Un script s'exécute avec vos droits : il peut lire et modifier le projet ouvert et écrire des fichiers. Désactivé par défaut ; n'exécutez que des scripts dont vous connaissez l'origine.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<spacer name="verticalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Vertical</enum>
|
||||
@@ -237,27 +279,6 @@
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>Répertoire de la collection commune</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="m_common_elmt_path_cb">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Par defaut</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Parcourir...</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_16">
|
||||
<property name="text">
|
||||
@@ -265,8 +286,8 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="m_company_elmt_path_cb">
|
||||
<item row="5" column="1">
|
||||
<widget class="QComboBox" name="m_custom_tbt_path_cb">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Par defaut</string>
|
||||
@@ -279,6 +300,27 @@
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QComboBox" name="m_custom_elmt_path_cb">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Par defaut</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Parcourir...</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>Répertoire de la collection commune</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_17">
|
||||
<property name="text">
|
||||
@@ -307,8 +349,29 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QComboBox" name="m_custom_elmt_path_cb">
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="label_23">
|
||||
<property name="text">
|
||||
<string>Répertoire des Macros utilisateur</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QComboBox" name="m_user_macros_path_cb">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Par defaut</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Parcourir...</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="m_common_elmt_path_cb">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Par defaut</string>
|
||||
@@ -328,8 +391,8 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QComboBox" name="m_custom_tbt_path_cb">
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="m_company_elmt_path_cb">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Par defaut</string>
|
||||
@@ -342,34 +405,13 @@
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<item row="8" column="1">
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="text">
|
||||
<string>(Recharger les collections d'éléments pour appliquer les changements)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="label_11">
|
||||
<property name="text">
|
||||
<string>Répertoire des Macros utilisateur</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="1">
|
||||
<widget class="QComboBox" name="m_user_macros_path_cb">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Par defaut</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Parcourir...</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -742,6 +784,26 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="Label_Diagram_textGrid">
|
||||
<property name="text">
|
||||
<string>Grille des textes déplacés à la souris</string>
|
||||
</property>
|
||||
<property name="toolTip">
|
||||
<string>Fraction de la grille des folios. Maintenir Ctrl pendant le déplacement pour placer librement.</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="2">
|
||||
<widget class="QComboBox" name="DiagramEditor_textGrid_cb">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>80</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -911,30 +973,10 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
|
||||
<string>Affichage Grille</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_5">
|
||||
<item row="0" column="1">
|
||||
<spacer name="horizontalSpacer_10">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>555</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="0" column="4">
|
||||
<widget class="QLabel" name="label_18">
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="label_19">
|
||||
<property name="text">
|
||||
<string>max:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="Label_Diagram_Grid_PointSize">
|
||||
<property name="text">
|
||||
<string>Taille des points de la grille de Diagram-Editor : 1 - 5</string>
|
||||
<string>min:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -954,10 +996,39 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="2">
|
||||
<widget class="QLabel" name="label_19">
|
||||
<item row="0" column="1">
|
||||
<spacer name="horizontalSpacer_10">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>555</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item row="1" column="3">
|
||||
<widget class="QSpinBox" name="ElementEditor_Grid_PointSize_min_sb">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>60</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>5</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="Label_Diagram_Grid_PointSize">
|
||||
<property name="text">
|
||||
<string>min:</string>
|
||||
<string>Taille des points de la grille de Diagram-Editor : 1 - 5</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -977,13 +1048,6 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_20">
|
||||
<property name="text">
|
||||
<string>Taille des points de la grille de l'éditeur d'éléments : 1 - 5</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="5">
|
||||
<widget class="QSpinBox" name="ElementEditor_Grid_PointSize_max_sb">
|
||||
<property name="minimumSize">
|
||||
@@ -1007,19 +1071,10 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="3">
|
||||
<widget class="QSpinBox" name="ElementEditor_Grid_PointSize_min_sb">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>60</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>5</number>
|
||||
<item row="0" column="4">
|
||||
<widget class="QLabel" name="label_18">
|
||||
<property name="text">
|
||||
<string>max:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -1030,6 +1085,13 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_20">
|
||||
<property name="text">
|
||||
<string>Taille des points de la grille de l'éditeur d'éléments : 1 - 5</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -1110,7 +1172,6 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>tabWidget</tabstop>
|
||||
<tabstop>m_use_system_color_cb</tabstop>
|
||||
<tabstop>m_use_gesture_trackpad</tabstop>
|
||||
<tabstop>m_zoom_out_beyond_folio</tabstop>
|
||||
@@ -1141,4 +1202,11 @@ Vous pouvez spécifier ici la valeur par défaut de ce champ pour les éléments
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections/>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>KColorButton</class>
|
||||
<extends>QPushButton</extends>
|
||||
<header>nokde/kcolorbutton.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
</ui>
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
#include "guidespropertieswidget.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QPushButton>
|
||||
#include <QTableWidget>
|
||||
#include <QHeaderView>
|
||||
#include <QComboBox>
|
||||
#include <QDoubleSpinBox>
|
||||
#include <QColorDialog>
|
||||
|
||||
GuidesPropertiesWidget::GuidesPropertiesWidget(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
setupUi();
|
||||
}
|
||||
|
||||
GuidesPropertiesWidget::~GuidesPropertiesWidget() {}
|
||||
|
||||
void GuidesPropertiesWidget::setupUi() {
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
|
||||
m_table = new QTableWidget(0, 3, this);
|
||||
m_table->setHorizontalHeaderLabels({tr("Orientation"), tr("Position"), tr("Couleur")});
|
||||
m_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
||||
m_table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
|
||||
m_add_btn = new QPushButton(tr("Ajouter"), this);
|
||||
m_remove_btn = new QPushButton(tr("Supprimer"), this);
|
||||
|
||||
QHBoxLayout *btn_layout = new QHBoxLayout();
|
||||
btn_layout->addWidget(m_add_btn);
|
||||
btn_layout->addWidget(m_remove_btn);
|
||||
btn_layout->addStretch();
|
||||
|
||||
main_layout->addWidget(m_table);
|
||||
main_layout->addLayout(btn_layout);
|
||||
|
||||
connect(m_add_btn, &QPushButton::clicked, this, &GuidesPropertiesWidget::addGuide);
|
||||
connect(m_remove_btn, &QPushButton::clicked, this, &GuidesPropertiesWidget::removeGuide);
|
||||
}
|
||||
|
||||
QList<Diagram::Guide> GuidesPropertiesWidget::guides() const {
|
||||
QList<Diagram::Guide> list;
|
||||
for (int row = 0; row < m_table->rowCount(); ++row) {
|
||||
QComboBox *combo = qobject_cast<QComboBox*>(m_table->cellWidget(row, 0));
|
||||
QDoubleSpinBox *spin = qobject_cast<QDoubleSpinBox*>(m_table->cellWidget(row, 1));
|
||||
QPushButton *colorBtn = qobject_cast<QPushButton*>(m_table->cellWidget(row, 2));
|
||||
|
||||
if (combo && spin && colorBtn) {
|
||||
Diagram::Guide g;
|
||||
g.orientation = (combo->currentIndex() == 0) ? Diagram::Guide::Horizontal : Diagram::Guide::Vertical;
|
||||
g.position = spin->value();
|
||||
g.color = colorBtn->property("color").value<QColor>();
|
||||
list.append(g);
|
||||
}
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
void GuidesPropertiesWidget::setGuides(const QList<Diagram::Guide> &guides) {
|
||||
m_table->setRowCount(0);
|
||||
for (const auto &g : guides) {
|
||||
addGuide();
|
||||
int row = m_table->rowCount() - 1;
|
||||
|
||||
QComboBox *combo = qobject_cast<QComboBox*>(m_table->cellWidget(row, 0));
|
||||
QDoubleSpinBox *spin = qobject_cast<QDoubleSpinBox*>(m_table->cellWidget(row, 1));
|
||||
QPushButton *colorBtn = qobject_cast<QPushButton*>(m_table->cellWidget(row, 2));
|
||||
|
||||
if (combo && spin && colorBtn) {
|
||||
combo->setCurrentIndex(g.orientation == Diagram::Guide::Horizontal ? 0 : 1);
|
||||
spin->setValue(g.position);
|
||||
colorBtn->setProperty("color", g.color);
|
||||
colorBtn->setStyleSheet(QString("background-color: %1; color: white; font-weight: bold;").arg(g.color.name()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void GuidesPropertiesWidget::addGuide() {
|
||||
int row = m_table->rowCount();
|
||||
m_table->insertRow(row);
|
||||
|
||||
QComboBox *combo = new QComboBox(this);
|
||||
combo->addItems({tr("Horizontal"), tr("Vertical")});
|
||||
m_table->setCellWidget(row, 0, combo);
|
||||
|
||||
QDoubleSpinBox *spin = new QDoubleSpinBox(this);
|
||||
spin->setRange(-10000.0, 10000.0);
|
||||
spin->setDecimals(2);
|
||||
spin->setValue(100.0);
|
||||
m_table->setCellWidget(row, 1, spin);
|
||||
|
||||
QPushButton *colorBtn = new QPushButton(tr("Couleur"), this);
|
||||
QColor defaultColor = Qt::lightGray;
|
||||
colorBtn->setProperty("color", defaultColor);
|
||||
colorBtn->setStyleSheet(QString("background-color: %1; color: white; font-weight: bold;").arg(defaultColor.name()));
|
||||
|
||||
connect(colorBtn, &QPushButton::clicked, [this, colorBtn]() {
|
||||
QColor c = QColorDialog::getColor(colorBtn->property("color").value<QColor>(), this);
|
||||
if (c.isValid()) {
|
||||
colorBtn->setProperty("color", c);
|
||||
colorBtn->setStyleSheet(QString("background-color: %1; color: white; font-weight: bold;").arg(c.name()));
|
||||
}
|
||||
});
|
||||
m_table->setCellWidget(row, 2, colorBtn);
|
||||
}
|
||||
|
||||
void GuidesPropertiesWidget::removeGuide() {
|
||||
int row = m_table->currentRow();
|
||||
if (row >= 0) {
|
||||
m_table->removeRow(row);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#ifndef GUIDESPROPERTIESWIDGET_H
|
||||
#define GUIDESPROPERTIESWIDGET_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QList>
|
||||
#include "../../diagram.h"
|
||||
|
||||
class QTableWidget;
|
||||
class QPushButton;
|
||||
|
||||
class GuidesPropertiesWidget : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit GuidesPropertiesWidget(QWidget *parent = nullptr);
|
||||
~GuidesPropertiesWidget() override;
|
||||
|
||||
QList<Diagram::Guide> guides() const;
|
||||
void setGuides(const QList<Diagram::Guide> &guides);
|
||||
|
||||
private slots:
|
||||
void addGuide();
|
||||
void removeGuide();
|
||||
|
||||
private:
|
||||
void setupUi();
|
||||
|
||||
QTableWidget *m_table;
|
||||
QPushButton *m_add_btn;
|
||||
QPushButton *m_remove_btn;
|
||||
};
|
||||
|
||||
#endif // GUIDESPROPERTIESWIDGET_H
|
||||
@@ -22,6 +22,8 @@
|
||||
#include "../autoNum/ui/folioautonumbering.h"
|
||||
#include "../autoNum/ui/formulaautonumberingw.h"
|
||||
#include "../autoNum/ui/selectautonumw.h"
|
||||
#include "../project/projectpropertieshandler.h"
|
||||
#include "../qet.h"
|
||||
#include "../qeticons.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../borderpropertieswidget.h"
|
||||
@@ -161,6 +163,13 @@ void ProjectMainConfigPage::applyProjectConf()
|
||||
m_project -> setProjectProperties(new_properties);
|
||||
modified_project = true;
|
||||
}
|
||||
|
||||
ProjectUsageTracker &usage_tracker = m_project -> projectPropertiesHandler().usageTracker();
|
||||
if (usage_tracker.isEnabled() != usage_enabled_cb_ -> isChecked()) {
|
||||
usage_tracker.setEnabled(usage_enabled_cb_ -> isChecked());
|
||||
modified_project = true;
|
||||
}
|
||||
|
||||
if (modified_project) {
|
||||
m_project -> setModified(true);
|
||||
}
|
||||
@@ -191,6 +200,12 @@ void ProjectMainConfigPage::initWidgets()
|
||||
project_variables_label_ -> setWordWrap(true);
|
||||
project_variables_ = new DiagramContextWidget();
|
||||
project_variables_ -> setContext(DiagramContext());
|
||||
|
||||
usage_label_ = new QLabel(tr("Temps passé sur ce projet :", "label when configuring"));
|
||||
usage_value_ = new QLabel();
|
||||
usage_enabled_cb_ = new QCheckBox(tr("Suivre le temps passé sur ce projet (uniquement enregistré localement dans ce fichier)", "checkbox label"));
|
||||
usage_reset_pb_ = new QPushButton(tr("Réinitialiser", "button label"));
|
||||
connect(usage_reset_pb_, &QPushButton::clicked, this, &ProjectMainConfigPage::resetUsageTracker);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -207,6 +222,16 @@ void ProjectMainConfigPage::initLayout()
|
||||
main_layout0 -> addSpacing(10);
|
||||
main_layout0 -> addWidget(project_variables_label_);
|
||||
main_layout0 -> addWidget(project_variables_);
|
||||
main_layout0 -> addSpacing(10);
|
||||
|
||||
QHBoxLayout *usage_layout0 = new QHBoxLayout();
|
||||
usage_layout0 -> addWidget(usage_label_);
|
||||
usage_layout0 -> addWidget(usage_value_);
|
||||
usage_layout0 -> addStretch();
|
||||
usage_layout0 -> addWidget(usage_reset_pb_);
|
||||
main_layout0 -> addLayout(usage_layout0);
|
||||
main_layout0 -> addWidget(usage_enabled_cb_);
|
||||
|
||||
setLayout(main_layout0);
|
||||
this -> setMinimumWidth(680);
|
||||
|
||||
@@ -219,6 +244,27 @@ void ProjectMainConfigPage::readValuesFromProject()
|
||||
{
|
||||
title_value_ -> setText(m_project -> title());
|
||||
project_variables_ -> setContext(m_project -> projectProperties());
|
||||
|
||||
const ProjectUsageTracker &usage_tracker = m_project -> projectPropertiesHandler().usageTracker();
|
||||
const qint64 total_seconds = usage_tracker.secondsSpent();
|
||||
usage_value_ -> setText(tr("%1 h %2 min", "hours and minutes of time spent on a project")
|
||||
.arg(total_seconds / 3600)
|
||||
.arg((total_seconds % 3600) / 60));
|
||||
usage_enabled_cb_ -> setChecked(usage_tracker.isEnabled());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ProjectMainConfigPage::resetUsageTracker
|
||||
Reset the accumulated "time spent on this project" counter to zero and
|
||||
refresh its displayed value. Applies immediately (not staged behind
|
||||
OK/Cancel like the other fields on this page), since it isn't
|
||||
destructive to any actual project content.
|
||||
*/
|
||||
void ProjectMainConfigPage::resetUsageTracker()
|
||||
{
|
||||
m_project -> projectPropertiesHandler().usageTracker().resetSecondsSpent();
|
||||
m_project -> setModified(true);
|
||||
usage_value_ -> setText(tr("%1 h %2 min", "hours and minutes of time spent on a project").arg(0).arg(0));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -229,6 +275,8 @@ void ProjectMainConfigPage::adjustReadOnly()
|
||||
{
|
||||
bool is_read_only = m_project -> isReadOnly();
|
||||
title_value_ -> setReadOnly(is_read_only);
|
||||
usage_enabled_cb_ -> setDisabled(is_read_only);
|
||||
usage_reset_pb_ -> setDisabled(is_read_only);
|
||||
}
|
||||
|
||||
//######################################################################################//
|
||||
@@ -243,9 +291,14 @@ ProjectAutoNumConfigPage::ProjectAutoNumConfigPage (QETProject *project,
|
||||
QWidget *parent) :
|
||||
ProjectConfigPage(project, parent)
|
||||
{
|
||||
initWidgets();
|
||||
buildConnections();
|
||||
readValuesFromProject();
|
||||
// Follow the same contract ProjectMainConfigPage's constructor does --
|
||||
// init() is documented to be the thing a subclass constructor calls
|
||||
// (see ProjectConfigPage::init()'s doc comment). Calling the pieces
|
||||
// individually here used to skip both initLayout() and adjustReadOnly(),
|
||||
// and called readValuesFromProject() even when m_project was null.
|
||||
// buildConnections() itself is invoked from the end of initWidgets()
|
||||
// below, in the same relative position it held here.
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -302,9 +355,22 @@ void ProjectAutoNumConfigPage::initWidgets()
|
||||
m_faw = new FolioAutonumberingW(project());
|
||||
tab_widget->addTab(m_faw, tr("Numérotation auto des folios"));
|
||||
|
||||
QHBoxLayout *main_layout = new QHBoxLayout();
|
||||
m_import_pb = new QPushButton(
|
||||
tr("Importer depuis un autre projet..."), this);
|
||||
m_import_pb->setToolTip(
|
||||
tr("Reprendre les numérotations automatiques "
|
||||
"enregistrées dans un autre projet"));
|
||||
|
||||
QHBoxLayout *button_layout = new QHBoxLayout();
|
||||
button_layout->addStretch();
|
||||
button_layout->addWidget(m_import_pb);
|
||||
|
||||
QVBoxLayout *main_layout = new QVBoxLayout();
|
||||
main_layout->addWidget(tab_widget);
|
||||
main_layout->addLayout(button_layout);
|
||||
setLayout(main_layout);
|
||||
|
||||
buildConnections();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -313,6 +379,12 @@ void ProjectAutoNumConfigPage::initWidgets()
|
||||
*/
|
||||
void ProjectAutoNumConfigPage::readValuesFromProject()
|
||||
{
|
||||
// This is called again after an import, so start from an empty
|
||||
// combo box instead of appending a second copy of every name.
|
||||
m_saw_conductor->contextComboBox()->clear();
|
||||
m_saw_element->contextComboBox()->clear();
|
||||
m_saw_folio->contextComboBox()->clear();
|
||||
|
||||
//Conductor Tab
|
||||
const QStringList strlc(m_project->conductorAutoNum().keys());
|
||||
m_saw_conductor->contextComboBox()->addItems(strlc);
|
||||
@@ -335,6 +407,9 @@ void ProjectAutoNumConfigPage::readValuesFromProject()
|
||||
*/
|
||||
void ProjectAutoNumConfigPage::adjustReadOnly()
|
||||
{
|
||||
if (m_import_pb && m_project) {
|
||||
m_import_pb->setDisabled(m_project->isReadOnly());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -344,25 +419,28 @@ void ProjectAutoNumConfigPage::adjustReadOnly()
|
||||
void ProjectAutoNumConfigPage::buildConnections()
|
||||
{
|
||||
//Management Tab
|
||||
connect (m_amw, SIGNAL(applyPressed()), this, SLOT(applyManagement()));
|
||||
connect(m_amw, &AutoNumberingManagementW::applyPressed, this, &ProjectAutoNumConfigPage::applyManagement);
|
||||
|
||||
//Conductor Tab
|
||||
connect(m_saw_conductor, &SelectAutonumW::applyPressed, this, &ProjectAutoNumConfigPage::saveContextConductor);
|
||||
connect(m_saw_conductor, &SelectAutonumW::removeClicked, this, &ProjectAutoNumConfigPage::removeContextConductor);
|
||||
connect(m_saw_conductor->contextComboBox(), SIGNAL(currentIndexChanged(QString)), this, SLOT(updateContextConductor(QString)));
|
||||
connect(m_saw_conductor->contextComboBox(), &QComboBox::textActivated, this, &ProjectAutoNumConfigPage::updateContextConductor);
|
||||
|
||||
//Element Tab
|
||||
connect(m_saw_element, &SelectAutonumW::applyPressed, this, &ProjectAutoNumConfigPage::saveContextElement);
|
||||
connect(m_saw_element, &SelectAutonumW::removeClicked, this, &ProjectAutoNumConfigPage::removeContextElement);
|
||||
connect(m_saw_element->contextComboBox(), SIGNAL(currentIndexChanged(QString)), this, SLOT(updateContextElement(QString)));
|
||||
connect(m_saw_element->contextComboBox(), &QComboBox::textActivated, this, &ProjectAutoNumConfigPage::updateContextElement);
|
||||
|
||||
//Folio Tab
|
||||
connect(m_saw_folio, &SelectAutonumW::applyPressed, this, &ProjectAutoNumConfigPage::saveContextFolio);
|
||||
connect(m_saw_folio, &SelectAutonumW::removeClicked, this, &ProjectAutoNumConfigPage::removeContextFolio);
|
||||
connect(m_saw_folio->contextComboBox(), SIGNAL(currentIndexChanged(QString)), this, SLOT(updateContextFolio(QString)));
|
||||
connect(m_saw_folio->contextComboBox(), &QComboBox::textActivated, this, &ProjectAutoNumConfigPage::updateContextFolio);
|
||||
|
||||
// Auto Folio Numbering
|
||||
connect (m_faw, SIGNAL (applyPressed()), this, SLOT (applyAutoNum()));
|
||||
connect(m_faw, &FolioAutonumberingW::applyPressed, this, &ProjectAutoNumConfigPage::applyAutoNum);
|
||||
|
||||
//Import from another project
|
||||
connect(m_import_pb, &QPushButton::clicked, this, &ProjectAutoNumConfigPage::importFromProject);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -419,7 +497,7 @@ void ProjectAutoNumConfigPage::saveContextElement()
|
||||
m_saw_element->contextComboBox()->addItem(tr("Sans nom"));
|
||||
}
|
||||
// If the text isn't yet to the autonum of the project, add this new item to the combo box.
|
||||
else if ( !m_project -> elementAutoNum().keys().contains( m_saw_element->contextComboBox()->currentText()))
|
||||
else if ( !m_project -> elementAutoNum().contains( m_saw_element->contextComboBox()->currentText()))
|
||||
{
|
||||
m_project->addElementAutoNum(m_saw_element->contextComboBox()->currentText(), m_saw_element->toNumContext());
|
||||
m_project->setCurrrentElementAutonum(m_saw_element->contextComboBox()->currentText());
|
||||
@@ -433,6 +511,198 @@ void ProjectAutoNumConfigPage::saveContextElement()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ProjectAutoNumConfigPage::importFromProject
|
||||
Read the automatic numbering rules stored in another .qet project and
|
||||
copy the ones the user selects into this project.
|
||||
|
||||
The source file is parsed as plain XML rather than opened as a
|
||||
QETProject: opening it would run the whole load path, including the
|
||||
modal dialog raised when the file was written by a different version
|
||||
of QElectroTech.
|
||||
*/
|
||||
void ProjectAutoNumConfigPage::importFromProject()
|
||||
{
|
||||
if (!m_project || m_project->isReadOnly()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString path = QFileDialog::getOpenFileName(
|
||||
this,
|
||||
tr("Importer les numérotations d'un projet"),
|
||||
m_project->currentDir(),
|
||||
tr("Projet QElectroTech (*.qet)"));
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
QMessageBox::warning(this, tr("Import impossible"),
|
||||
tr("Impossible d'ouvrir %1").arg(path));
|
||||
return;
|
||||
}
|
||||
|
||||
QDomDocument doc;
|
||||
if (!doc.setContent(&file)) {
|
||||
QMessageBox::warning(this, tr("Import impossible"),
|
||||
tr("%1 n'est pas un projet QElectroTech valide.")
|
||||
.arg(QFileInfo(path).fileName()));
|
||||
return;
|
||||
}
|
||||
file.close();
|
||||
|
||||
const QDomNodeList newdiagrams =
|
||||
doc.elementsByTagName(QStringLiteral("newdiagrams"));
|
||||
if (newdiagrams.isEmpty()) {
|
||||
QMessageBox::information(this, tr("Aucune numérotation"),
|
||||
tr("Ce projet ne contient aucune numérotation automatique."));
|
||||
return;
|
||||
}
|
||||
const QDomElement root = newdiagrams.at(0).toElement();
|
||||
|
||||
// tag of the group, tag of one entry, label shown to the user
|
||||
struct Category {
|
||||
QString group_tag;
|
||||
QString item_tag;
|
||||
QString label;
|
||||
};
|
||||
const QList<Category> categories {
|
||||
{QStringLiteral("conductors_autonums"),
|
||||
QStringLiteral("conductor_autonum"), tr("Conducteurs")},
|
||||
{QStringLiteral("element_autonums"),
|
||||
QStringLiteral("element_autonum"), tr("Eléments")},
|
||||
{QStringLiteral("folio_autonums"),
|
||||
QStringLiteral("folio_autonum"), tr("Folios")}
|
||||
};
|
||||
|
||||
QDialog dialog(this);
|
||||
dialog.setWindowTitle(tr("Numérotations à importer"));
|
||||
QVBoxLayout *layout = new QVBoxLayout(&dialog);
|
||||
layout->addWidget(new QLabel(
|
||||
tr("Numérotations trouvées dans %1 :")
|
||||
.arg(QFileInfo(path).fileName()), &dialog));
|
||||
|
||||
QListWidget *list = new QListWidget(&dialog);
|
||||
layout->addWidget(list);
|
||||
|
||||
// NumerotationContext is not a QVariant type, so the list item
|
||||
// carries an index into this instead of the context itself.
|
||||
QList<NumerotationContext> contexts;
|
||||
for (int i = 0 ; i < categories.count() ; ++i)
|
||||
{
|
||||
const Category &category = categories.at(i);
|
||||
QDomElement group;
|
||||
for (QDomNode n = root.firstChild() ; !n.isNull() ; n = n.nextSibling()) {
|
||||
if (n.toElement().tagName() == category.group_tag) {
|
||||
group = n.toElement();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (group.isNull()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (QDomElement entry : QET::findInDomElement(group, category.item_tag))
|
||||
{
|
||||
const QString title = entry.attribute(QStringLiteral("title"));
|
||||
if (title.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool exists = false;
|
||||
switch (i) {
|
||||
case 0: exists = m_project->conductorAutoNum().contains(title); break;
|
||||
case 1: exists = m_project->elementAutoNum().contains(title); break;
|
||||
default: exists = m_project->folioAutoNum().contains(title); break;
|
||||
}
|
||||
|
||||
QListWidgetItem *item = new QListWidgetItem(
|
||||
exists ? tr("%1 : %2 (existe déjà)")
|
||||
.arg(category.label, title)
|
||||
: QStringLiteral("%1 : %2")
|
||||
.arg(category.label, title),
|
||||
list);
|
||||
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||||
item->setCheckState(exists ? Qt::Unchecked : Qt::Checked);
|
||||
item->setData(Qt::UserRole, i);
|
||||
item->setData(Qt::UserRole + 1, title);
|
||||
|
||||
NumerotationContext nc;
|
||||
nc.fromXml(entry);
|
||||
item->setData(Qt::UserRole + 2, contexts.count());
|
||||
contexts << nc;
|
||||
}
|
||||
}
|
||||
|
||||
if (contexts.isEmpty()) {
|
||||
QMessageBox::information(this, tr("Aucune numérotation"),
|
||||
tr("Ce projet ne contient aucune numérotation automatique."));
|
||||
return;
|
||||
}
|
||||
|
||||
QCheckBox *overwrite_cb = new QCheckBox(
|
||||
tr("Remplacer les numérotations de même nom"), &dialog);
|
||||
layout->addWidget(overwrite_cb);
|
||||
|
||||
QDialogButtonBox *buttons = new QDialogButtonBox(
|
||||
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog);
|
||||
layout->addWidget(buttons);
|
||||
connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||
|
||||
if (dialog.exec() != QDialog::Accepted) {
|
||||
return;
|
||||
}
|
||||
|
||||
int imported = 0, skipped = 0, conductors = 0;
|
||||
for (int row = 0 ; row < list->count() ; ++row)
|
||||
{
|
||||
QListWidgetItem *item = list->item(row);
|
||||
if (item->checkState() != Qt::Checked) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int category = item->data(Qt::UserRole).toInt();
|
||||
const QString title = item->data(Qt::UserRole + 1).toString();
|
||||
|
||||
bool exists = false;
|
||||
switch (category) {
|
||||
case 0: exists = m_project->conductorAutoNum().contains(title); break;
|
||||
case 1: exists = m_project->elementAutoNum().contains(title); break;
|
||||
default: exists = m_project->folioAutoNum().contains(title); break;
|
||||
}
|
||||
if (exists && !overwrite_cb->isChecked()) {
|
||||
++skipped;
|
||||
continue;
|
||||
}
|
||||
|
||||
const NumerotationContext &nc =
|
||||
contexts.at(item->data(Qt::UserRole + 2).toInt());
|
||||
switch (category) {
|
||||
case 0:
|
||||
m_project->addConductorAutoNum(title, nc);
|
||||
++conductors;
|
||||
break;
|
||||
case 1: m_project->addElementAutoNum(title, nc); break;
|
||||
default: m_project->addFolioAutoNum(title, nc); break;
|
||||
}
|
||||
++imported;
|
||||
}
|
||||
|
||||
readValuesFromProject();
|
||||
if (conductors) {
|
||||
m_project->conductorAutoNumAdded();
|
||||
}
|
||||
|
||||
QMessageBox::information(
|
||||
this, tr("Import terminé"),
|
||||
skipped ? tr("%1 numérotation(s) importée(s), "
|
||||
"%2 conservée(s) telles quelles.")
|
||||
.arg(imported).arg(skipped)
|
||||
: tr("%1 numérotation(s) importée(s).").arg(imported));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ProjectAutoNumConfigPage::removeContextElement
|
||||
Remove from project the current element numerotation context
|
||||
@@ -444,6 +714,10 @@ void ProjectAutoNumConfigPage::removeContextElement()
|
||||
return;
|
||||
m_project->removeElementAutoNum (m_saw_element->contextComboBox()->currentText());
|
||||
m_saw_element->contextComboBox()->removeItem (m_saw_element->contextComboBox()->currentIndex());
|
||||
// removeItem() removes the current selection programmatically but
|
||||
// textActivated() does not react to (by design, see buildConnections()).
|
||||
// Refresh the displayed pattern explicitly so it matches the new selection.
|
||||
updateContextElement(m_saw_element->contextComboBox()->currentText());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -461,7 +735,7 @@ void ProjectAutoNumConfigPage::saveContextConductor()
|
||||
m_saw_conductor->contextComboBox()-> addItem(tr("Sans nom"));
|
||||
}
|
||||
// If the text isn't yet to the autonum of the project, add this new item to the combo box.
|
||||
else if ( !m_project -> conductorAutoNum().keys().contains( m_saw_conductor->contextComboBox()->currentText()))
|
||||
else if ( !m_project -> conductorAutoNum().contains( m_saw_conductor->contextComboBox()->currentText()))
|
||||
{
|
||||
project()->addConductorAutoNum(m_saw_conductor->contextComboBox()->currentText(), m_saw_conductor->toNumContext());
|
||||
project()->setCurrentConductorAutoNum(m_saw_conductor->contextComboBox()->currentText());
|
||||
@@ -489,7 +763,7 @@ void ProjectAutoNumConfigPage::saveContextFolio()
|
||||
m_saw_folio->contextComboBox() -> addItem(tr("Sans nom"));
|
||||
}
|
||||
// If the text isn't yet to the autonum of the project, add this new item to the combo box.
|
||||
else if ( !m_project -> folioAutoNum().keys().contains( m_saw_folio->contextComboBox()->currentText())) {
|
||||
else if ( !m_project -> folioAutoNum().contains( m_saw_folio->contextComboBox()->currentText())) {
|
||||
project()->addFolioAutoNum(m_saw_folio->contextComboBox()->currentText(), m_saw_folio->toNumContext());
|
||||
m_saw_folio->contextComboBox() -> addItem(m_saw_folio->contextComboBox()->currentText());
|
||||
}
|
||||
@@ -631,6 +905,10 @@ void ProjectAutoNumConfigPage::removeContextConductor()
|
||||
if ( m_saw_conductor->contextComboBox()-> currentText() == tr("Nom de la nouvelle numérotation") ) return;
|
||||
m_project -> removeConductorAutoNum (m_saw_conductor->contextComboBox()-> currentText() );
|
||||
m_saw_conductor->contextComboBox()-> removeItem (m_saw_conductor->contextComboBox()-> currentIndex() );
|
||||
// removeItem() removes the current selection programmatically but
|
||||
// textActivated() does not react to (by design, see buildConnections()).
|
||||
// Refresh the displayed pattern explicitly so it matches the new selection.
|
||||
updateContextConductor(m_saw_conductor->contextComboBox()->currentText());
|
||||
project()->conductorAutoNumRemoved();
|
||||
}
|
||||
|
||||
@@ -644,6 +922,10 @@ void ProjectAutoNumConfigPage::removeContextFolio()
|
||||
if ( m_saw_folio->contextComboBox() -> currentText() == tr("Nom de la nouvelle numérotation") ) return;
|
||||
m_project -> removeFolioAutoNum (m_saw_folio->contextComboBox() -> currentText() );
|
||||
m_saw_folio->contextComboBox() -> removeItem (m_saw_folio->contextComboBox() -> currentIndex() );
|
||||
// removeItem() removes the current selection programmatically but
|
||||
// textActivated() does not react to (by design, see buildConnections()).
|
||||
// Refresh the displayed pattern explicitly so it matches the new selection.
|
||||
updateContextFolio(m_saw_folio->contextComboBox()->currentText());
|
||||
project()->folioAutoNumRemoved();
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QCheckBox;
|
||||
class QPushButton;
|
||||
class QETProject;
|
||||
class BorderPropertiesWidget;
|
||||
class ConductorPropertiesWidget;
|
||||
@@ -108,7 +110,10 @@ class ProjectMainConfigPage : public ProjectConfigPage {
|
||||
void initLayout() override;
|
||||
void readValuesFromProject() override;
|
||||
void adjustReadOnly() override;
|
||||
|
||||
|
||||
private slots:
|
||||
void resetUsageTracker();
|
||||
|
||||
// attributes
|
||||
protected:
|
||||
QLabel *title_label_;
|
||||
@@ -116,6 +121,10 @@ class ProjectMainConfigPage : public ProjectConfigPage {
|
||||
QLabel *title_information_;
|
||||
QLabel *project_variables_label_;
|
||||
DiagramContextWidget *project_variables_;
|
||||
QLabel *usage_label_;
|
||||
QLabel *usage_value_;
|
||||
QCheckBox *usage_enabled_cb_;
|
||||
QPushButton *usage_reset_pb_;
|
||||
};
|
||||
|
||||
class ProjectAutoNumConfigPage : public ProjectConfigPage {
|
||||
@@ -147,6 +156,7 @@ class ProjectAutoNumConfigPage : public ProjectConfigPage {
|
||||
void updateContextElement(const QString&);//element
|
||||
void saveContextElement();
|
||||
void removeContextElement();
|
||||
void importFromProject();
|
||||
|
||||
void applyAutoNum();
|
||||
void applyManagement();
|
||||
@@ -164,6 +174,7 @@ class ProjectAutoNumConfigPage : public ProjectConfigPage {
|
||||
SelectAutonumW *m_saw_element;
|
||||
FolioAutonumberingW *m_faw;
|
||||
AutoNumberingManagementW *m_amw;
|
||||
QPushButton *m_import_pb = nullptr;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,385 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "shortcutsconfigpage.h"
|
||||
|
||||
#include "../../qeticons.h"
|
||||
#include "../../shortcutmanager.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QKeySequenceEdit>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QRegularExpression>
|
||||
#include <QToolButton>
|
||||
#include <QTreeWidget>
|
||||
#include <QTreeWidgetItem>
|
||||
#include <QVBoxLayout>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
@brief normalizedForSearch
|
||||
Decompose \a text and strip every non-spacing combining mark, then fold the
|
||||
case. Both the query terms and the row's searchable text are run through
|
||||
this, so "general" matches "Général" and "Ctrl+S" matches "ctrl+s"
|
||||
regardless of the keyboard layout the query was typed on.
|
||||
*/
|
||||
static QString normalizedForSearch(const QString &text)
|
||||
{
|
||||
const QString decomposed = text.normalized(QString::NormalizationForm_D);
|
||||
QString stripped;
|
||||
stripped.reserve(decomposed.size());
|
||||
for (const QChar &c : decomposed) {
|
||||
if (c.category() != QChar::Mark_NonSpacing) {
|
||||
stripped.append(c);
|
||||
}
|
||||
}
|
||||
return stripped.toCaseFolded();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ShortcutsConfigPage::ShortcutsConfigPage
|
||||
@param parent
|
||||
*/
|
||||
ShortcutsConfigPage::ShortcutsConfigPage(QWidget *parent) :
|
||||
ConfigPage(parent)
|
||||
{
|
||||
auto *vlayout = new QVBoxLayout();
|
||||
|
||||
QLabel *title_label = new QLabel(this->title());
|
||||
vlayout->addWidget(title_label);
|
||||
|
||||
QFrame *horiz_line = new QFrame();
|
||||
horiz_line->setFrameShape(QFrame::HLine);
|
||||
vlayout->addWidget(horiz_line);
|
||||
|
||||
m_filter_edit = new QLineEdit(this);
|
||||
m_filter_edit->setPlaceholderText(tr("Filtrer les raccourcis…"));
|
||||
connect(m_filter_edit, &QLineEdit::textChanged, this, &ShortcutsConfigPage::filterRows);
|
||||
|
||||
m_quick_filter = new QComboBox(this);
|
||||
m_quick_filter->setObjectName(QStringLiteral("quickFilterCombo"));
|
||||
m_quick_filter->addItem(tr("Tous"));
|
||||
m_quick_filter->addItem(tr("Attribués uniquement"));
|
||||
m_quick_filter->addItem(tr("Non attribués uniquement"));
|
||||
m_quick_filter->addItem(tr("Conflits uniquement"));
|
||||
connect(m_quick_filter, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &ShortcutsConfigPage::quickFilterChanged);
|
||||
|
||||
m_count_label = new QLabel(this);
|
||||
m_count_label->setObjectName(QStringLiteral("shortcutCountLabel"));
|
||||
|
||||
auto *filter_layout = new QHBoxLayout();
|
||||
filter_layout->addWidget(m_filter_edit, 1);
|
||||
filter_layout->addWidget(m_quick_filter);
|
||||
filter_layout->addWidget(m_count_label);
|
||||
vlayout->addLayout(filter_layout);
|
||||
|
||||
m_tree = new QTreeWidget(this);
|
||||
m_tree->setHeaderLabels({tr("Action"), tr("Raccourci"), QString()});
|
||||
m_tree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
m_tree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
|
||||
m_tree->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
||||
m_tree->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_tree->setSelectionMode(QAbstractItemView::NoSelection);
|
||||
vlayout->addWidget(m_tree);
|
||||
|
||||
auto *reset_all_button = new QPushButton(tr("Tout réinitialiser"), this);
|
||||
connect(reset_all_button, &QPushButton::clicked, this, &ShortcutsConfigPage::resetAllRows);
|
||||
|
||||
auto *bottom_layout = new QHBoxLayout();
|
||||
bottom_layout->addStretch();
|
||||
bottom_layout->addWidget(reset_all_button);
|
||||
vlayout->addLayout(bottom_layout);
|
||||
|
||||
setLayout(vlayout);
|
||||
|
||||
populateTable();
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
ShortcutsConfigPage::~ShortcutsConfigPage()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ShortcutsConfigPage::populateTable
|
||||
Fill the tree with one collapsible top-level node per category and one child
|
||||
per shortcut known to ShortcutManager, sorted by category then action name.
|
||||
*/
|
||||
void ShortcutsConfigPage::populateTable()
|
||||
{
|
||||
QList<ShortcutManager::ShortcutInfo> shortcuts = ShortcutManager::instance().allShortcuts();
|
||||
std::sort(shortcuts.begin(), shortcuts.end(),
|
||||
[](const ShortcutManager::ShortcutInfo &a, const ShortcutManager::ShortcutInfo &b) {
|
||||
if (a.category != b.category) {
|
||||
return a.category < b.category;
|
||||
}
|
||||
return a.description < b.description;
|
||||
});
|
||||
|
||||
m_tree->clear();
|
||||
m_rows.clear();
|
||||
m_rows.reserve(shortcuts.size());
|
||||
|
||||
QHash<QString, QTreeWidgetItem *> category_nodes;
|
||||
|
||||
for (const ShortcutManager::ShortcutInfo &info : shortcuts) {
|
||||
QTreeWidgetItem *category_item = category_nodes.value(info.category, nullptr);
|
||||
if (!category_item) {
|
||||
category_item = new QTreeWidgetItem(m_tree);
|
||||
category_item->setText(0, info.category);
|
||||
category_item->setFlags(category_item->flags() & ~Qt::ItemIsEditable);
|
||||
category_nodes.insert(info.category, category_item);
|
||||
}
|
||||
|
||||
auto *child = new QTreeWidgetItem(category_item);
|
||||
child->setText(0, info.description);
|
||||
child->setFlags(child->flags() & ~Qt::ItemIsEditable);
|
||||
|
||||
auto *edit = new QKeySequenceEdit(info.current_sequence, m_tree);
|
||||
connect(edit, &QKeySequenceEdit::editingFinished, this, &ShortcutsConfigPage::checkConflicts);
|
||||
m_tree->setItemWidget(child, 1, edit);
|
||||
|
||||
auto *reset_button = new QToolButton(m_tree);
|
||||
reset_button->setIcon(QET::Icons::EditUndo);
|
||||
reset_button->setToolTip(tr("Réinitialiser ce raccourci"));
|
||||
reset_button->setAutoRaise(true);
|
||||
const int row_index = m_rows.size();
|
||||
connect(reset_button, &QToolButton::clicked, this, [this, row_index]() { resetRow(row_index); });
|
||||
m_tree->setItemWidget(child, 2, reset_button);
|
||||
|
||||
m_rows << Row{info.id, info.category, info.description, info.default_sequence, edit, child, false};
|
||||
}
|
||||
|
||||
checkConflicts();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ShortcutsConfigPage::filterRows
|
||||
Re-run the combined text + quick filter whenever the search box changes.
|
||||
*/
|
||||
void ShortcutsConfigPage::filterRows(const QString &filter_text)
|
||||
{
|
||||
Q_UNUSED(filter_text)
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ShortcutsConfigPage::quickFilterChanged
|
||||
Re-run the combined text + quick filter whenever the quick filter changes.
|
||||
*/
|
||||
void ShortcutsConfigPage::quickFilterChanged(int index)
|
||||
{
|
||||
Q_UNUSED(index)
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ShortcutsConfigPage::applyFilter
|
||||
Hide every row that doesn't match both the search box and the quick filter.
|
||||
The text query is split on whitespace and every term must match the category,
|
||||
action name or current sequence (accent- and case-insensitively). Matching
|
||||
category nodes are expanded so hits are not hidden inside collapsed groups,
|
||||
and the "N actions" label tracks how many actions remain visible.
|
||||
*/
|
||||
void ShortcutsConfigPage::applyFilter()
|
||||
{
|
||||
const QString needle = m_filter_edit->text().trimmed();
|
||||
const QStringList terms = needle.isEmpty()
|
||||
? QStringList()
|
||||
: needle.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
|
||||
|
||||
const int quick_filter = m_quick_filter->currentIndex();
|
||||
|
||||
int visible_actions = 0;
|
||||
for (const Row &row : std::as_const(m_rows)) {
|
||||
// Text is matched by substring, but the key sequence is matched exactly:
|
||||
// a substring match would let "Ctrl+S" also hit "Ctrl+Shift+F" (the "S"
|
||||
// of "Shift"), which is precisely the kind of false positive that hides
|
||||
// the one binding the user is looking for.
|
||||
const QString text_haystack = normalizedForSearch(
|
||||
row.category + QLatin1Char(' ') + row.description);
|
||||
const QString sequence_text = normalizedForSearch(row.edit->keySequence().toString());
|
||||
|
||||
bool matches = true;
|
||||
for (const QString &term : terms) {
|
||||
const QString t = normalizedForSearch(term);
|
||||
if (!text_haystack.contains(t) && sequence_text != t) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
switch (quick_filter) {
|
||||
case BoundOnly:
|
||||
matches = !row.edit->keySequence().isEmpty();
|
||||
break;
|
||||
case UnboundOnly:
|
||||
matches = row.edit->keySequence().isEmpty();
|
||||
break;
|
||||
case ConflictsOnly:
|
||||
matches = row.conflicted;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
row.item->setHidden(!matches);
|
||||
if (matches) {
|
||||
++visible_actions;
|
||||
}
|
||||
}
|
||||
|
||||
const bool filtering = !needle.isEmpty() || quick_filter != ShowAll;
|
||||
for (int i = 0; i < m_tree->topLevelItemCount(); ++i) {
|
||||
QTreeWidgetItem *top = m_tree->topLevelItem(i);
|
||||
bool any_visible = false;
|
||||
for (int j = 0; j < top->childCount(); ++j) {
|
||||
if (!top->child(j)->isHidden()) {
|
||||
any_visible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
top->setHidden(!any_visible);
|
||||
top->setExpanded(filtering && any_visible);
|
||||
}
|
||||
|
||||
m_count_label->setText(tr("%n action(s)", nullptr, visible_actions));
|
||||
}
|
||||
|
||||
// IDs use stable context prefixes; translated display categories are not scopes.
|
||||
// Main-window actions are shared by the editors, and depth actions by the
|
||||
// diagram and element editors. Panel-specific shortcuts retain their own scope.
|
||||
static bool shortcutScopesOverlap(const QString &first_id, const QString &second_id)
|
||||
{
|
||||
const QString first = first_id.section(QLatin1Char('.'), 0, 0);
|
||||
const QString second = second_id.section(QLatin1Char('.'), 0, 0);
|
||||
if (first == second || first == QLatin1String("mainwindow")
|
||||
|| second == QLatin1String("mainwindow"))
|
||||
return true;
|
||||
|
||||
const auto has_depth = [](const QString &scope) {
|
||||
return scope == QLatin1String("diagrameditor")
|
||||
|| scope == QLatin1String("elementeditor");
|
||||
};
|
||||
return (first == QLatin1String("depth") && has_depth(second))
|
||||
|| (second == QLatin1String("depth") && has_depth(first));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ShortcutsConfigPage::checkConflicts
|
||||
Highlight every row whose currently-edited sequence is shared, non-empty,
|
||||
with another row, and explain the conflict in the shortcut editor's tooltip.
|
||||
*/
|
||||
void ShortcutsConfigPage::checkConflicts()
|
||||
{
|
||||
QHash<QString, QList<int>> sequence_to_rows;
|
||||
for (int row = 0; row < m_rows.size(); ++row) {
|
||||
const QString sequence_text = m_rows.at(row).edit->keySequence().toString();
|
||||
if (!sequence_text.isEmpty()) {
|
||||
sequence_to_rows[sequence_text] << row;
|
||||
}
|
||||
}
|
||||
|
||||
for (int row = 0; row < m_rows.size(); ++row) {
|
||||
Row ¤t_row = m_rows[row];
|
||||
const QString sequence_text = current_row.edit->keySequence().toString();
|
||||
QList<int> conflicting_rows;
|
||||
for (int other : sequence_to_rows.value(sequence_text)) {
|
||||
if (other != row && shortcutScopesOverlap(current_row.id, m_rows.at(other).id))
|
||||
conflicting_rows << other;
|
||||
}
|
||||
const bool conflicted = !conflicting_rows.isEmpty();
|
||||
|
||||
current_row.conflicted = conflicted;
|
||||
|
||||
if (conflicted) {
|
||||
QStringList other_descriptions;
|
||||
for (int other_row : conflicting_rows) {
|
||||
if (other_row != row) {
|
||||
other_descriptions << m_rows.at(other_row).description;
|
||||
}
|
||||
}
|
||||
current_row.item->setBackground(0, QColor(255, 205, 205));
|
||||
current_row.edit->setToolTip(
|
||||
tr("Ce raccourci est aussi utilisé par : %1").arg(other_descriptions.join(QStringLiteral(", "))));
|
||||
} else {
|
||||
current_row.item->setBackground(0, QBrush());
|
||||
current_row.edit->setToolTip(QString());
|
||||
}
|
||||
}
|
||||
|
||||
// A sequence edit can change while a filter is active (search-by-key or the
|
||||
// conflicts-only quick filter); refresh the visible set so the list doesn't
|
||||
// show stale results.
|
||||
const bool filtering = !m_filter_edit->text().trimmed().isEmpty()
|
||||
|| m_quick_filter->currentIndex() != ShowAll;
|
||||
if (filtering) {
|
||||
applyFilter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ShortcutsConfigPage::resetRow
|
||||
Reset the shortcut editor at \a row_index to its default sequence.
|
||||
*/
|
||||
void ShortcutsConfigPage::resetRow(int row_index)
|
||||
{
|
||||
if (row_index < 0 || row_index >= m_rows.size()) {
|
||||
return;
|
||||
}
|
||||
m_rows.at(row_index).edit->setKeySequence(m_rows.at(row_index).default_sequence);
|
||||
checkConflicts();
|
||||
}
|
||||
|
||||
void ShortcutsConfigPage::resetAllRows()
|
||||
{
|
||||
for (const Row &row : std::as_const(m_rows)) {
|
||||
row.edit->setKeySequence(row.default_sequence);
|
||||
}
|
||||
checkConflicts();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ShortcutsConfigPage::applyConf
|
||||
Persist every row's shortcut edit through ShortcutManager, which also
|
||||
applies it immediately to every currently live QAction sharing that id.
|
||||
*/
|
||||
void ShortcutsConfigPage::applyConf()
|
||||
{
|
||||
for (const Row &row : std::as_const(m_rows)) {
|
||||
ShortcutManager::instance().setSequence(row.id, row.edit->keySequence());
|
||||
}
|
||||
}
|
||||
|
||||
QString ShortcutsConfigPage::title() const
|
||||
{
|
||||
return tr("Raccourcis", "configuration page title");
|
||||
}
|
||||
|
||||
QIcon ShortcutsConfigPage::icon() const
|
||||
{
|
||||
return QET::Icons::ConfigureShortcuts;
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef SHORTCUTSCONFIGPAGE_H
|
||||
#define SHORTCUTSCONFIGPAGE_H
|
||||
|
||||
#include "configpage.h"
|
||||
|
||||
#include <QKeySequence>
|
||||
|
||||
class QComboBox;
|
||||
class QKeySequenceEdit;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
|
||||
/**
|
||||
@brief The ShortcutsConfigPage class
|
||||
Configuration page listing every shortcut registered with ShortcutManager,
|
||||
letting the user rebind, reset and search them. Bindings are only
|
||||
persisted (via ShortcutManager::setSequence()) when applyConf() runs, i.e.
|
||||
when the user validates the surrounding ConfigDialog.
|
||||
*/
|
||||
class ShortcutsConfigPage : public ConfigPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit ShortcutsConfigPage(QWidget *parent = nullptr);
|
||||
~ShortcutsConfigPage() override;
|
||||
|
||||
void applyConf() override;
|
||||
QString title() const override;
|
||||
QIcon icon() const override;
|
||||
|
||||
private slots:
|
||||
void filterRows(const QString &filter_text);
|
||||
void quickFilterChanged(int index);
|
||||
void checkConflicts();
|
||||
void resetAllRows();
|
||||
|
||||
private:
|
||||
enum QuickFilter {
|
||||
ShowAll = 0,
|
||||
BoundOnly,
|
||||
UnboundOnly,
|
||||
ConflictsOnly
|
||||
};
|
||||
|
||||
struct Row {
|
||||
QString id;
|
||||
QString category;
|
||||
QString description;
|
||||
QKeySequence default_sequence;
|
||||
QKeySequenceEdit *edit;
|
||||
QTreeWidgetItem *item;
|
||||
bool conflicted;
|
||||
};
|
||||
|
||||
void populateTable();
|
||||
void applyFilter();
|
||||
void resetRow(int row_index);
|
||||
|
||||
QLineEdit *m_filter_edit;
|
||||
QComboBox *m_quick_filter;
|
||||
QLabel *m_count_label;
|
||||
QTreeWidget *m_tree;
|
||||
QList<Row> m_rows;
|
||||
};
|
||||
|
||||
#endif // SHORTCUTSCONFIGPAGE_H
|
||||
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "spacemouseconfigpage.h"
|
||||
|
||||
#include "../../qeticons.h"
|
||||
#include "../../shortcutmanager.h"
|
||||
#include "../../spacemouse/spacemousebuttonmap.h"
|
||||
#include "../../spacemouse/spacemousemotion.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QFormLayout>
|
||||
#include <QFrame>
|
||||
#include <QGroupBox>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QSpinBox>
|
||||
#include <QTableWidget>
|
||||
#include <QToolButton>
|
||||
#include <QVBoxLayout>
|
||||
#include <algorithm>
|
||||
|
||||
namespace {
|
||||
constexpr int COL_BUTTON = 0;
|
||||
constexpr int COL_ACTION = 1;
|
||||
constexpr int COL_REMOVE = 2;
|
||||
|
||||
//The combo box's first entry: no action bound, i.e. this row does
|
||||
//nothing. Real action ids are never empty (ShortcutManager assigns
|
||||
//them from source, not user input), so an empty string can never
|
||||
//collide with a real one.
|
||||
const QString UNBOUND_LABEL = QObject::tr("(Aucune action)", "spacemouse config: unbound button");
|
||||
}
|
||||
|
||||
/**
|
||||
@brief SpaceMouseConfigPage::SpaceMouseConfigPage
|
||||
@param parent
|
||||
*/
|
||||
SpaceMouseConfigPage::SpaceMouseConfigPage(QWidget *parent) :
|
||||
ConfigPage(parent)
|
||||
{
|
||||
auto *vlayout = new QVBoxLayout();
|
||||
|
||||
QLabel *title_label = new QLabel(this->title());
|
||||
vlayout->addWidget(title_label);
|
||||
|
||||
QFrame *horiz_line = new QFrame();
|
||||
horiz_line->setFrameShape(QFrame::HLine);
|
||||
vlayout->addWidget(horiz_line);
|
||||
|
||||
vlayout->addWidget(buildMotionGroup());
|
||||
|
||||
auto *intro_label = new QLabel(
|
||||
tr("Associez un numéro de bouton de votre souris 3D (SpaceMouse, SpacePilot...) "
|
||||
"à une action de QElectroTech. Le numéro de bouton dépend de votre appareil "
|
||||
"et de son pilote -- reportez-vous à sa documentation, ou essayez successivement "
|
||||
"les valeurs à partir de 0.",
|
||||
"spacemouse config page intro"));
|
||||
intro_label->setWordWrap(true);
|
||||
|
||||
auto *buttons_layout = new QVBoxLayout();
|
||||
buttons_layout->addWidget(intro_label);
|
||||
|
||||
m_table = new QTableWidget(0, 3, this);
|
||||
m_table->setHorizontalHeaderLabels({tr("N° bouton"), tr("Action"), QString()});
|
||||
m_table->horizontalHeader()->setSectionResizeMode(COL_BUTTON, QHeaderView::ResizeToContents);
|
||||
m_table->horizontalHeader()->setSectionResizeMode(COL_ACTION, QHeaderView::Stretch);
|
||||
m_table->horizontalHeader()->setSectionResizeMode(COL_REMOVE, QHeaderView::ResizeToContents);
|
||||
m_table->verticalHeader()->setVisible(false);
|
||||
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_table->setSelectionMode(QAbstractItemView::NoSelection);
|
||||
buttons_layout->addWidget(m_table);
|
||||
|
||||
auto *add_button = new QPushButton(QET::Icons::Add, tr("Ajouter une association"), this);
|
||||
connect(add_button, &QPushButton::clicked, this, &SpaceMouseConfigPage::addRow);
|
||||
|
||||
auto *bottom_layout = new QHBoxLayout();
|
||||
bottom_layout->addWidget(add_button);
|
||||
bottom_layout->addStretch();
|
||||
buttons_layout->addLayout(bottom_layout);
|
||||
|
||||
auto *buttons_group = new QGroupBox(tr("Boutons"), this);
|
||||
buttons_group->setLayout(buttons_layout);
|
||||
vlayout->addWidget(buttons_group);
|
||||
|
||||
setLayout(vlayout);
|
||||
|
||||
populateTable();
|
||||
}
|
||||
|
||||
SpaceMouseConfigPage::~SpaceMouseConfigPage()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
@brief SpaceMouseConfigPage::buildMotionGroup
|
||||
@return the "motion" part of the page, filled from the saved
|
||||
SpaceMouseSettings
|
||||
*/
|
||||
QGroupBox *SpaceMouseConfigPage::buildMotionGroup()
|
||||
{
|
||||
const SpaceMouseSettings settings = SpaceMouseSettings::load();
|
||||
|
||||
auto speed_box = [this](int value) {
|
||||
auto *spin = new QSpinBox(this);
|
||||
spin->setRange(10, 400);
|
||||
spin->setSingleStep(10);
|
||||
spin->setSuffix(QStringLiteral(" %"));
|
||||
spin->setValue(value);
|
||||
return spin;
|
||||
};
|
||||
m_pan_speed = speed_box(settings.pan_speed);
|
||||
m_zoom_speed = speed_box(settings.zoom_speed);
|
||||
|
||||
m_dead_zone = new QSpinBox(this);
|
||||
m_dead_zone->setRange(0, 200);
|
||||
m_dead_zone->setValue(settings.dead_zone);
|
||||
m_dead_zone->setToolTip(tr("Les petits mouvements en dessous de ce seuil sont ignorés. "
|
||||
"Augmentez-le si la vue dérive quand vous ne touchez pas la souris."));
|
||||
|
||||
m_zoom_axis = new QComboBox(this);
|
||||
m_zoom_axis->addItem(tr("Pousser / tirer le capuchon"),
|
||||
static_cast<int>(SpaceMouseSettings::ZoomAxis::PushPull));
|
||||
m_zoom_axis->addItem(tr("Tourner le capuchon"),
|
||||
static_cast<int>(SpaceMouseSettings::ZoomAxis::Twist));
|
||||
m_zoom_axis->setCurrentIndex(
|
||||
m_zoom_axis->findData(static_cast<int>(settings.zoom_axis)));
|
||||
|
||||
m_invert_pan_x = new QCheckBox(tr("Inverser le déplacement horizontal"), this);
|
||||
m_invert_pan_x->setChecked(settings.invert_pan_x);
|
||||
m_invert_pan_y = new QCheckBox(tr("Inverser le déplacement vertical"), this);
|
||||
m_invert_pan_y->setChecked(settings.invert_pan_y);
|
||||
m_invert_zoom = new QCheckBox(tr("Inverser le zoom"), this);
|
||||
m_invert_zoom->setChecked(settings.invert_zoom);
|
||||
|
||||
auto *form = new QFormLayout();
|
||||
form->addRow(tr("Vitesse de déplacement :"), m_pan_speed);
|
||||
form->addRow(tr("Vitesse du zoom :"), m_zoom_speed);
|
||||
form->addRow(tr("Zoomer en :"), m_zoom_axis);
|
||||
form->addRow(tr("Zone morte :"), m_dead_zone);
|
||||
form->addRow(m_invert_pan_x);
|
||||
form->addRow(m_invert_pan_y);
|
||||
form->addRow(m_invert_zoom);
|
||||
|
||||
auto *group = new QGroupBox(tr("Mouvement"), this);
|
||||
group->setLayout(form);
|
||||
return group;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief SpaceMouseConfigPage::populateTable
|
||||
Fill the table with one row per binding currently persisted in
|
||||
SpaceMouseButtonMap.
|
||||
*/
|
||||
void SpaceMouseConfigPage::populateTable()
|
||||
{
|
||||
const QMap<int, QString> bindings = SpaceMouseButtonMap::allBindings();
|
||||
for (auto it = bindings.constBegin(); it != bindings.constEnd(); ++it) {
|
||||
appendRow(it.key(), it.value());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief SpaceMouseConfigPage::appendRow
|
||||
Add one row bound to \a button / \a action_id (either or both may be
|
||||
the "unset" value -- 0 and an empty string respectively -- for a fresh
|
||||
row added via the "+" button).
|
||||
@param button
|
||||
@param action_id
|
||||
*/
|
||||
void SpaceMouseConfigPage::appendRow(int button, const QString &action_id)
|
||||
{
|
||||
const int row = m_table->rowCount();
|
||||
m_table->setRowCount(row + 1);
|
||||
|
||||
auto *button_spin = new QSpinBox(m_table);
|
||||
button_spin->setRange(0, 999);
|
||||
button_spin->setValue(button);
|
||||
m_table->setCellWidget(row, COL_BUTTON, button_spin);
|
||||
|
||||
auto *action_combo = new QComboBox(m_table);
|
||||
action_combo->addItem(UNBOUND_LABEL, QString());
|
||||
|
||||
QList<ShortcutManager::ShortcutInfo> shortcuts = ShortcutManager::instance().allShortcuts();
|
||||
std::sort(shortcuts.begin(), shortcuts.end(),
|
||||
[](const ShortcutManager::ShortcutInfo &a, const ShortcutManager::ShortcutInfo &b) {
|
||||
if (a.category != b.category) {
|
||||
return a.category < b.category;
|
||||
}
|
||||
return a.description < b.description;
|
||||
});
|
||||
for (const ShortcutManager::ShortcutInfo &info : shortcuts) {
|
||||
action_combo->addItem(
|
||||
QStringLiteral("%1 — %2").arg(info.category, info.description), info.id);
|
||||
}
|
||||
|
||||
int index_for_current = action_combo->findData(action_id);
|
||||
action_combo->setCurrentIndex(index_for_current >= 0 ? index_for_current : 0);
|
||||
m_table->setCellWidget(row, COL_ACTION, action_combo);
|
||||
|
||||
auto *remove_button = new QToolButton(m_table);
|
||||
remove_button->setIcon(QET::Icons::EditTableDeleteRow);
|
||||
remove_button->setToolTip(tr("Supprimer cette association"));
|
||||
connect(remove_button, &QToolButton::clicked, this, &SpaceMouseConfigPage::removeSelectedRow);
|
||||
m_table->setCellWidget(row, COL_REMOVE, remove_button);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief SpaceMouseConfigPage::addRow
|
||||
Add a fresh, unbound row for the user to fill in.
|
||||
*/
|
||||
void SpaceMouseConfigPage::addRow()
|
||||
{
|
||||
appendRow(0, QString());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief SpaceMouseConfigPage::removeSelectedRow
|
||||
Remove whichever row owns the remove button that was clicked. Found by
|
||||
looking the sender up in the button column rather than tracked
|
||||
per-button state, so nothing has to be kept in sync as rows are added
|
||||
and removed around it.
|
||||
*/
|
||||
void SpaceMouseConfigPage::removeSelectedRow()
|
||||
{
|
||||
auto *button = qobject_cast<QToolButton *>(sender());
|
||||
if (!button) {
|
||||
return;
|
||||
}
|
||||
for (int row = 0; row < m_table->rowCount(); ++row) {
|
||||
if (m_table->cellWidget(row, COL_REMOVE) == button) {
|
||||
m_table->removeRow(row);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief SpaceMouseConfigPage::applyConf
|
||||
Save the motion settings, then persist exactly what the table
|
||||
currently shows: every previously saved binding is cleared first,
|
||||
then every row still in the table (after any
|
||||
adds/edits/removals) is written back. Simpler and less error-prone than
|
||||
tracking which individual rows changed, and cheap -- this runs once,
|
||||
when the user validates the surrounding ConfigDialog, not on every
|
||||
keystroke.
|
||||
*/
|
||||
void SpaceMouseConfigPage::applyConf()
|
||||
{
|
||||
SpaceMouseSettings settings;
|
||||
settings.pan_speed = m_pan_speed->value();
|
||||
settings.zoom_speed = m_zoom_speed->value();
|
||||
settings.dead_zone = m_dead_zone->value();
|
||||
settings.zoom_axis = static_cast<SpaceMouseSettings::ZoomAxis>(
|
||||
m_zoom_axis->currentData().toInt());
|
||||
settings.invert_pan_x = m_invert_pan_x->isChecked();
|
||||
settings.invert_pan_y = m_invert_pan_y->isChecked();
|
||||
settings.invert_zoom = m_invert_zoom->isChecked();
|
||||
settings.save();
|
||||
|
||||
const QMap<int, QString> previous = SpaceMouseButtonMap::allBindings();
|
||||
for (auto it = previous.constBegin(); it != previous.constEnd(); ++it) {
|
||||
SpaceMouseButtonMap::setActionId(it.key(), QString());
|
||||
}
|
||||
|
||||
for (int row = 0; row < m_table->rowCount(); ++row)
|
||||
{
|
||||
auto *button_spin = qobject_cast<QSpinBox *>(m_table->cellWidget(row, COL_BUTTON));
|
||||
auto *action_combo = qobject_cast<QComboBox *>(m_table->cellWidget(row, COL_ACTION));
|
||||
if (!button_spin || !action_combo) {
|
||||
continue;
|
||||
}
|
||||
SpaceMouseButtonMap::setActionId(button_spin->value(), action_combo->currentData().toString());
|
||||
}
|
||||
}
|
||||
|
||||
QString SpaceMouseConfigPage::title() const
|
||||
{
|
||||
return tr("Souris 3D", "configuration page title");
|
||||
}
|
||||
|
||||
QIcon SpaceMouseConfigPage::icon() const
|
||||
{
|
||||
return QET::Icons::ConfigureToolbars;
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef SPACEMOUSECONFIGPAGE_H
|
||||
#define SPACEMOUSECONFIGPAGE_H
|
||||
|
||||
#include "configpage.h"
|
||||
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
class QGroupBox;
|
||||
class QSpinBox;
|
||||
class QTableWidget;
|
||||
|
||||
/**
|
||||
@brief The SpaceMouseConfigPage class
|
||||
Configuration page for the 3D mouse: how motion pans and zooms the view
|
||||
(SpaceMouseSettings: speeds, dead zone, which axes are inverted, and
|
||||
whether zoom follows push/pull or twist), then every
|
||||
device-button-to-action binding (SpaceMouseButtonMap), letting the
|
||||
user add, edit and remove them.
|
||||
Modelled directly on ShortcutsConfigPage, one row per binding instead
|
||||
of one row per shortcut, since the two are close cousins: both end in
|
||||
the same place -- ShortcutManager::trigger()/registerAction() -- just
|
||||
from a different input source. Bindings are only persisted (via
|
||||
SpaceMouseButtonMap::setActionId()) when applyConf() runs, i.e. when
|
||||
the user validates the surrounding ConfigDialog.
|
||||
|
||||
Only compiled in, and only added to the Configuration dialog, when
|
||||
QET_SPACEMOUSE_SUPPORT is defined -- see qetapp.cpp.
|
||||
|
||||
Button numbering is intentionally not fixed to any particular device:
|
||||
it is exactly whatever integer SpaceMouseBackend::buttonPressed()
|
||||
reports for the button pressed, which is backend- and device-specific.
|
||||
Rows are added/removed freely rather than the page assuming a button
|
||||
count, since that varies from 2 (SpaceMouse Compact) to over 30
|
||||
(SpacePilot Pro).
|
||||
*/
|
||||
class SpaceMouseConfigPage : public ConfigPage
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SpaceMouseConfigPage(QWidget *parent = nullptr);
|
||||
~SpaceMouseConfigPage() override;
|
||||
|
||||
void applyConf() override;
|
||||
QString title() const override;
|
||||
QIcon icon() const override;
|
||||
|
||||
private slots:
|
||||
void addRow();
|
||||
void removeSelectedRow();
|
||||
|
||||
private:
|
||||
QGroupBox *buildMotionGroup();
|
||||
void populateTable();
|
||||
void appendRow(int button, const QString &action_id);
|
||||
|
||||
QSpinBox *m_pan_speed;
|
||||
QSpinBox *m_zoom_speed;
|
||||
QSpinBox *m_dead_zone;
|
||||
QComboBox *m_zoom_axis;
|
||||
QCheckBox *m_invert_pan_x;
|
||||
QCheckBox *m_invert_pan_y;
|
||||
QCheckBox *m_invert_zoom;
|
||||
QTableWidget *m_table;
|
||||
};
|
||||
|
||||
#endif // SPACEMOUSECONFIGPAGE_H
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "contactgroupselectiondialog.h"
|
||||
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QTableWidget>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QHeaderView>
|
||||
#include <QItemSelectionModel>
|
||||
|
||||
ContactGroupSelectionDialog::ContactGroupSelectionDialog(
|
||||
const QVector<ElementData::SlaveContactGroup> &groups,
|
||||
const QSet<int> &usedGroupIndices,
|
||||
const ElementData &slaveData,
|
||||
QWidget *parent)
|
||||
: QDialog(parent)
|
||||
{
|
||||
m_used_indices = usedGroupIndices;
|
||||
setWindowTitle(tr("Sélectionner un groupe de contacts"));
|
||||
|
||||
auto *main_layout = new QVBoxLayout(this);
|
||||
|
||||
auto *info_label = new QLabel(
|
||||
tr("Sélectionnez le groupe de contacts à assigner à cet élément esclave :"));
|
||||
main_layout->addWidget(info_label);
|
||||
|
||||
// Determine max terminal count for dynamic columns
|
||||
int max_terminals = 0;
|
||||
for (const auto &g : groups) {
|
||||
if (g.terminalCount > max_terminals)
|
||||
max_terminals = g.terminalCount;
|
||||
}
|
||||
|
||||
// Build column headers: #, Type, Sous-type, Contacts, Bornes, T1, T2, ...
|
||||
QStringList headers;
|
||||
headers << tr("#")
|
||||
<< tr("Type")
|
||||
<< tr("Sous-type")
|
||||
<< tr("Contacts")
|
||||
<< tr("Bornes");
|
||||
for (int t = 0; t < max_terminals; ++t) {
|
||||
headers << tr("T%1").arg(t + 1);
|
||||
}
|
||||
|
||||
m_table = new QTableWidget(groups.size(), headers.size(), this);
|
||||
m_table->setHorizontalHeaderLabels(headers);
|
||||
m_table->setSelectionBehavior(QTableWidget::SelectRows);
|
||||
m_table->setSelectionMode(QTableWidget::SingleSelection);
|
||||
m_table->setEditTriggers(QTableWidget::NoEditTriggers);
|
||||
m_table->verticalHeader()->setVisible(false);
|
||||
m_table->horizontalHeader()->setSectionResizeMode(QHeaderView::ResizeToContents);
|
||||
m_table->horizontalHeader()->setStretchLastSection(false);
|
||||
m_table->verticalHeader()->setDefaultSectionSize(24);
|
||||
|
||||
// Populate table rows
|
||||
for (int row = 0; row < groups.size(); ++row) {
|
||||
const auto &g = groups.at(row);
|
||||
|
||||
auto *num_item = new QTableWidgetItem(QString::number(row + 1));
|
||||
num_item->setTextAlignment(Qt::AlignCenter);
|
||||
m_table->setItem(row, 0, num_item);
|
||||
|
||||
m_table->setItem(row, 1, new QTableWidgetItem(typeToString(g.type)));
|
||||
m_table->setItem(row, 2, new QTableWidgetItem(subtypeToString(g.subtype)));
|
||||
|
||||
auto *ctc_item = new QTableWidgetItem(QString::number(g.contactCount));
|
||||
ctc_item->setTextAlignment(Qt::AlignCenter);
|
||||
m_table->setItem(row, 3, ctc_item);
|
||||
|
||||
auto *term_item = new QTableWidgetItem(QString::number(g.terminalCount));
|
||||
term_item->setTextAlignment(Qt::AlignCenter);
|
||||
m_table->setItem(row, 4, term_item);
|
||||
|
||||
// Fill T1..TN label columns
|
||||
for (int t = 0; t < max_terminals; ++t) {
|
||||
int col = 5 + t;
|
||||
QString label;
|
||||
if (t < g.labels.size()) {
|
||||
label = g.labels.at(t);
|
||||
} else if (t < g.terminalCount) {
|
||||
label = tr("T%1").arg(t + 1);
|
||||
}
|
||||
auto *label_item = new QTableWidgetItem(label);
|
||||
label_item->setTextAlignment(Qt::AlignCenter);
|
||||
m_table->setItem(row, col, label_item);
|
||||
}
|
||||
}
|
||||
|
||||
// Mark used group rows as disabled (grayed out)
|
||||
QFont disabled_font;
|
||||
disabled_font.setStrikeOut(true);
|
||||
QColor disabled_color(Qt::gray);
|
||||
|
||||
for (int row = 0; row < groups.size(); ++row) {
|
||||
bool disabled = false;
|
||||
QString reason;
|
||||
|
||||
if (m_used_indices.contains(row)) {
|
||||
disabled = true;
|
||||
reason = tr("(déjà assigné)");
|
||||
} else {
|
||||
const auto &g = groups.at(row);
|
||||
if (g.type != slaveData.m_slave_state) {
|
||||
disabled = true;
|
||||
reason = tr("(état ne correspond pas)");
|
||||
} else if (g.subtype != slaveData.m_slave_type) {
|
||||
disabled = true;
|
||||
reason = tr("(sous-type ne correspond pas)");
|
||||
} else if (g.contactCount != slaveData.m_contact_count) {
|
||||
disabled = true;
|
||||
reason = tr("(nombre de contacts ne correspond pas)");
|
||||
}
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
m_disabled_rows.insert(row);
|
||||
for (int col = 0; col < m_table->columnCount(); ++col) {
|
||||
auto *item = m_table->item(row, col);
|
||||
if (item) {
|
||||
item->setForeground(disabled_color);
|
||||
item->setFont(disabled_font);
|
||||
item->setFlags(item->flags() & ~Qt::ItemIsSelectable);
|
||||
item->setToolTip(reason);
|
||||
}
|
||||
}
|
||||
// Show circled "?" as a widget in the first column
|
||||
auto *num_item = m_table->item(row, 0);
|
||||
if (num_item) {
|
||||
num_item->setToolTip(reason);
|
||||
auto *question_label = new QLabel(this);
|
||||
question_label->setText("?");
|
||||
question_label->setAlignment(Qt::AlignCenter);
|
||||
question_label->setStyleSheet(
|
||||
"QLabel {"
|
||||
" color: #1a73e8;"
|
||||
" font-weight: bold;"
|
||||
" font-size: 8px;"
|
||||
" border: 1.5px solid #1a73e8;"
|
||||
" border-radius: 7px;"
|
||||
" min-width: 12px; max-width: 12px;"
|
||||
" min-height: 12px; max-height: 12px;"
|
||||
"}");
|
||||
question_label->setToolTip(reason);
|
||||
m_table->setCellWidget(row, 0, question_label);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Select first available (non-disabled) row
|
||||
int first_available = 0;
|
||||
for (int row = 0; row < groups.size(); ++row) {
|
||||
if (!m_disabled_rows.contains(row)) {
|
||||
first_available = row;
|
||||
break;
|
||||
}
|
||||
}
|
||||
m_table->selectRow(first_available);
|
||||
main_layout->addWidget(m_table);
|
||||
|
||||
// Calculate width based on column count: base + ~60px per column
|
||||
int table_width = 50 + headers.size() * 65;
|
||||
int table_height = 60 + groups.size() * 26;
|
||||
m_table->setMinimumWidth(table_width);
|
||||
m_table->setMinimumHeight(table_height);
|
||||
|
||||
// Buttons
|
||||
auto *button_layout = new QHBoxLayout();
|
||||
button_layout->addStretch();
|
||||
|
||||
m_ok_button = new QPushButton(tr("OK"), this);
|
||||
button_layout->addWidget(m_ok_button);
|
||||
|
||||
auto *cancel_button = new QPushButton(tr("Annuler"), this);
|
||||
button_layout->addWidget(cancel_button);
|
||||
|
||||
main_layout->addLayout(button_layout);
|
||||
|
||||
// Connections
|
||||
connect(m_ok_button, &QPushButton::clicked, this, [this]() {
|
||||
auto *item = m_table->currentItem();
|
||||
if (item && !m_disabled_rows.contains(item->row())) {
|
||||
m_selected_index = item->row();
|
||||
accept();
|
||||
}
|
||||
});
|
||||
connect(cancel_button, &QPushButton::clicked, this, &QDialog::reject);
|
||||
connect(m_table, &QTableWidget::cellDoubleClicked, this, [this](int row, int) {
|
||||
if (!m_disabled_rows.contains(row)) {
|
||||
m_selected_index = row;
|
||||
accept();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
int ContactGroupSelectionDialog::selectedIndex() const
|
||||
{
|
||||
return m_selected_index;
|
||||
}
|
||||
|
||||
QString ContactGroupSelectionDialog::typeToString(ElementData::SlaveState type)
|
||||
{
|
||||
switch (type) {
|
||||
case ElementData::NO: return tr("Normalement ouvert");
|
||||
case ElementData::NC: return tr("Normalement fermé");
|
||||
case ElementData::SW: return tr("Inverseur");
|
||||
case ElementData::Other: return tr("Autre");
|
||||
default: return tr("Inconnu");
|
||||
}
|
||||
}
|
||||
|
||||
QString ContactGroupSelectionDialog::subtypeToString(ElementData::SlaveType subtype)
|
||||
{
|
||||
switch (subtype) {
|
||||
case ElementData::SSimple: return tr("Simple");
|
||||
case ElementData::Power: return tr("Puissance");
|
||||
case ElementData::DelayOn: return tr("Temporisé travail");
|
||||
case ElementData::DelayOff: return tr("Temporisé repos");
|
||||
case ElementData::delayOnOff: return tr("Temporisé travail & repos");
|
||||
default: return tr("Inconnu");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef CONTACTGROUPSELECTIONDIALOG_H
|
||||
#define CONTACTGROUPSELECTIONDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QVector>
|
||||
#include <QSet>
|
||||
|
||||
#include "../properties/elementdata.h"
|
||||
|
||||
class QTableWidget;
|
||||
class QTableWidgetItem;
|
||||
class QPushButton;
|
||||
|
||||
/**
|
||||
@brief The ContactGroupSelectionDialog class
|
||||
A dialog that displays all slave contact groups defined by a master element
|
||||
in a table format. The user can select one group to assign to a slave element.
|
||||
Columns: #, Type, Subtype, Contacts, Terminals, T1, T2, ..., TN
|
||||
*/
|
||||
class ContactGroupSelectionDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
ContactGroupSelectionDialog(
|
||||
const QVector<ElementData::SlaveContactGroup> &groups,
|
||||
const QSet<int> &usedGroupIndices,
|
||||
const ElementData &slaveData,
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
int selectedIndex() const;
|
||||
|
||||
private:
|
||||
QTableWidget *m_table = nullptr;
|
||||
QPushButton *m_ok_button = nullptr;
|
||||
int m_selected_index = -1;
|
||||
QSet<int> m_used_indices;
|
||||
QSet<int> m_disabled_rows;
|
||||
|
||||
static QString typeToString(ElementData::SlaveState type);
|
||||
static QString subtypeToString(ElementData::SlaveType subtype);
|
||||
};
|
||||
|
||||
#endif // CONTACTGROUPSELECTIONDIALOG_H
|
||||
@@ -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;"));
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,281 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "diagrambgcolorbutton.h"
|
||||
|
||||
#include "../diagram.h"
|
||||
#include "../diagramview.h"
|
||||
#include "../palettegraphicsview.h"
|
||||
#include "../qetdiagrameditor.h"
|
||||
#include "../projectview.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../utils/qetsettings.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QColorDialog>
|
||||
#include <QMenu>
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
|
||||
namespace {
|
||||
struct NamedColor { const char *context_name; QColor color; };
|
||||
|
||||
QList<NamedColor> standardColors()
|
||||
{
|
||||
return {
|
||||
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Blanc"), QColor(0xFF, 0xFF, 0xFF)},
|
||||
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Blanc cassé"), QColor(0xFD, 0xFB, 0xF5)},
|
||||
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Gris clair"), QColor(0xE0, 0xE0, 0xE0)},
|
||||
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Gris"), QColor(0x80, 0x80, 0x80)},
|
||||
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Gris foncé"), QColor(0x40, 0x40, 0x40)},
|
||||
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Noir"), QColor(0x00, 0x00, 0x00)},
|
||||
};
|
||||
}
|
||||
|
||||
const int MAX_RECENT = 6;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::DiagramBgColorToolButton
|
||||
@param editor : the diagram editor this button acts on
|
||||
@param parent
|
||||
*/
|
||||
DiagramBgColorToolButton::DiagramBgColorToolButton(QETDiagramEditor *editor, QWidget *parent) :
|
||||
QToolButton(parent),
|
||||
m_editor(editor)
|
||||
{
|
||||
setPopupMode(QToolButton::InstantPopup);
|
||||
setToolTip(tr("Couleur de fond du folio"));
|
||||
setStatusTip(tr("Choisir la couleur de fond du folio",
|
||||
"status bar tip"));
|
||||
|
||||
//main() already restored the stored choice onto the global
|
||||
//Diagram::background_color and PaletteGraphicsView flag; mirror it
|
||||
//here so the button opens on what the editor actually draws, rather
|
||||
//than always claiming "system colour" as it used to.
|
||||
if (PaletteGraphicsView::customBackgroundColor()) {
|
||||
m_is_system_color = false;
|
||||
m_current = Diagram::background_color;
|
||||
} else {
|
||||
m_is_system_color = true;
|
||||
m_current = QApplication::palette().color(QPalette::Base);
|
||||
}
|
||||
for (const QString &name : QetSettings::sheetBackgroundRecentColors())
|
||||
{
|
||||
const QColor c(name);
|
||||
if (c.isValid()) {
|
||||
m_recent.append(c);
|
||||
}
|
||||
}
|
||||
while (m_recent.size() > MAX_RECENT) {
|
||||
m_recent.removeLast();
|
||||
}
|
||||
|
||||
setMenu(new QMenu(this));
|
||||
rebuildMenu();
|
||||
setSwatch(m_current);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::rebuildMenu
|
||||
*/
|
||||
void DiagramBgColorToolButton::rebuildMenu()
|
||||
{
|
||||
QMenu *m = menu();
|
||||
m->clear();
|
||||
|
||||
QAction *sys = m->addAction(tr("Couleur système"));
|
||||
connect(sys, &QAction::triggered, this, &DiagramBgColorToolButton::applySystemColor);
|
||||
m->addSeparator();
|
||||
|
||||
for (const auto &nc : standardColors())
|
||||
{
|
||||
const QColor c = nc.color;
|
||||
QAction *a = m->addAction(swatchIcon(c),
|
||||
tr(nc.context_name));
|
||||
connect(a, &QAction::triggered, this, [this, c]() { applyColor(c); });
|
||||
}
|
||||
|
||||
if (!m_recent.isEmpty())
|
||||
{
|
||||
m->addSeparator();
|
||||
QAction *title = m->addAction(tr("Récemment utilisées"));
|
||||
title->setEnabled(false);
|
||||
for (const QColor &c : std::as_const(m_recent))
|
||||
{
|
||||
QAction *a = m->addAction(swatchIcon(c), c.name());
|
||||
connect(a, &QAction::triggered, this, [this, c]() { applyColor(c); });
|
||||
}
|
||||
}
|
||||
|
||||
m->addSeparator();
|
||||
QAction *other = m->addAction(tr("Autre couleur…"));
|
||||
connect(other, &QAction::triggered, this, &DiagramBgColorToolButton::chooseOtherColor);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::applyColor
|
||||
Set a custom background color on all open diagrams.
|
||||
@param color
|
||||
*/
|
||||
void DiagramBgColorToolButton::applyColor(const QColor &color)
|
||||
{
|
||||
if (!color.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_is_system_color = false;
|
||||
rememberRecent(color);
|
||||
setSwatch(color);
|
||||
|
||||
PaletteGraphicsView::setCustomBackgroundColor(true);
|
||||
Diagram::background_color = color;
|
||||
QetSettings::setSheetBackground(QetSettings::SheetBackground{color, true});
|
||||
|
||||
QETDiagramEditor *editor = m_editor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
for (ProjectView *pv : editor->openedProjects())
|
||||
for (Diagram *d : pv->project()->diagrams())
|
||||
d->update();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::applySystemColor
|
||||
Restore the system-default background and re-enable dark-mode
|
||||
inversion.
|
||||
*/
|
||||
void DiagramBgColorToolButton::applySystemColor()
|
||||
{
|
||||
m_is_system_color = true;
|
||||
m_current = QApplication::palette().color(QPalette::Base);
|
||||
setSwatch(m_current);
|
||||
|
||||
PaletteGraphicsView::setCustomBackgroundColor(false);
|
||||
Diagram::background_color = Qt::white;
|
||||
QetSettings::setSheetBackground(
|
||||
QetSettings::SheetBackground{QColor(Qt::white), false});
|
||||
|
||||
QETDiagramEditor *editor = m_editor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
for (ProjectView *pv : editor->openedProjects())
|
||||
for (Diagram *d : pv->project()->diagrams())
|
||||
d->update();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::chooseOtherColor
|
||||
*/
|
||||
void DiagramBgColorToolButton::chooseOtherColor()
|
||||
{
|
||||
const QColor c = QColorDialog::getColor(m_current, this,
|
||||
tr("Choisir une couleur de fond"));
|
||||
if (c.isValid()) {
|
||||
applyColor(c);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::rememberRecent
|
||||
Most recent first, no duplicates, capped.
|
||||
@param color
|
||||
*/
|
||||
void DiagramBgColorToolButton::rememberRecent(const QColor &color)
|
||||
{
|
||||
for (const auto &nc : standardColors()) {
|
||||
if (nc.color == color) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_recent.removeAll(color);
|
||||
m_recent.prepend(color);
|
||||
while (m_recent.size() > MAX_RECENT) {
|
||||
m_recent.removeLast();
|
||||
}
|
||||
rebuildMenu();
|
||||
persistRecent();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::persistRecent
|
||||
Write the "recently used" colours back to the settings, so they are
|
||||
still there on the next start instead of starting out empty.
|
||||
*/
|
||||
void DiagramBgColorToolButton::persistRecent()
|
||||
{
|
||||
QStringList names;
|
||||
for (const QColor &c : std::as_const(m_recent)) {
|
||||
names << c.name();
|
||||
}
|
||||
QetSettings::setSheetBackgroundRecentColors(names);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::setSwatch
|
||||
@param color
|
||||
*/
|
||||
void DiagramBgColorToolButton::setSwatch(const QColor &color)
|
||||
{
|
||||
m_current = color;
|
||||
setIcon(swatchIcon(color));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::syncFromDiagram
|
||||
Sync the button swatch to whatever Diagram::background_color is
|
||||
currently set to. Called when the active folio changes.
|
||||
*/
|
||||
void DiagramBgColorToolButton::syncFromDiagram()
|
||||
{
|
||||
if (m_is_system_color) {
|
||||
m_current = QApplication::palette().color(QPalette::Base);
|
||||
} else {
|
||||
m_current = Diagram::background_color;
|
||||
}
|
||||
setSwatch(m_current);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::updateEnabledState
|
||||
*/
|
||||
void DiagramBgColorToolButton::updateEnabledState()
|
||||
{
|
||||
setEnabled(m_editor && m_editor->currentProjectView());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::swatchIcon
|
||||
@param color
|
||||
@return a plain square of that colour, outlined so that white and very
|
||||
light colours are still visible against the toolbar.
|
||||
*/
|
||||
QIcon DiagramBgColorToolButton::swatchIcon(const QColor &color)
|
||||
{
|
||||
QPixmap pix(16, 16);
|
||||
pix.fill(Qt::transparent);
|
||||
QPainter p(&pix);
|
||||
p.setRenderHint(QPainter::Antialiasing, false);
|
||||
p.setBrush(color);
|
||||
p.setPen(QPen(QColor(0x40, 0x40, 0x40), 1));
|
||||
p.drawRect(0, 0, 15, 15);
|
||||
p.end();
|
||||
return QIcon(pix);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef DIAGRAMBGCOLORTOOLBUTTON_H
|
||||
#define DIAGRAMBGCOLORTOOLBUTTON_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QList>
|
||||
#include <QToolButton>
|
||||
|
||||
class QETDiagramEditor;
|
||||
|
||||
/**
|
||||
@brief The DiagramBgColorToolButton class
|
||||
Color picker for the diagram sheet background, placed in the
|
||||
"Affichage" toolbar. Mirrors the ConductorColorToolButton UX:
|
||||
preset colors in the dropdown, "Autre couleur..." at the bottom,
|
||||
and a swatch icon on the button itself.
|
||||
|
||||
Picking "Couleur système" clears any custom colour and lets the
|
||||
dark-mode inversion handle the background as before.
|
||||
*/
|
||||
class DiagramBgColorToolButton : public QToolButton
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DiagramBgColorToolButton(QETDiagramEditor *editor,
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
public slots:
|
||||
void updateEnabledState();
|
||||
void syncFromDiagram();
|
||||
|
||||
private:
|
||||
void rebuildMenu();
|
||||
void applyColor(const QColor &color);
|
||||
void applySystemColor();
|
||||
void chooseOtherColor();
|
||||
void rememberRecent(const QColor &color);
|
||||
void persistRecent();
|
||||
void setSwatch(const QColor &color);
|
||||
static QIcon swatchIcon(const QColor &color);
|
||||
|
||||
QETDiagramEditor *m_editor = nullptr;
|
||||
QList<QColor> m_recent;
|
||||
QColor m_current;
|
||||
bool m_is_system_color = false;
|
||||
};
|
||||
|
||||
#endif // DIAGRAMBGCOLORTOOLBUTTON_H
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "duplicateoffsetdialog.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QSettings>
|
||||
#include <QSpinBox>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
namespace {
|
||||
// Stored as one integer per axis, in grid steps -- not pixels, so the
|
||||
// remembered offset still makes sense if the grid size ever changes.
|
||||
const QString kOffsetXKey = QStringLiteral("diagrameditor/duplicate_offset_x");
|
||||
const QString kOffsetYKey = QStringLiteral("diagrameditor/duplicate_offset_y");
|
||||
}
|
||||
|
||||
DuplicateOffsetDialog::DuplicateOffsetDialog(QWidget *parent) :
|
||||
QDialog(parent)
|
||||
{
|
||||
setWindowTitle(tr("Dupliquer"));
|
||||
|
||||
auto *form = new QFormLayout;
|
||||
|
||||
m_spacing = new QSpinBox(this);
|
||||
m_spacing->setRange(1, 1000);
|
||||
m_spacing->setSuffix(tr(" pas de grille"));
|
||||
form->addRow(tr("Espacement :"), m_spacing);
|
||||
|
||||
m_direction = new QComboBox(this);
|
||||
// Order matches the Direction enum, so currentIndex() can be used
|
||||
// directly wherever Direction is needed.
|
||||
m_direction->addItem(tr("Haut"));
|
||||
m_direction->addItem(tr("Bas"));
|
||||
m_direction->addItem(tr("Gauche"));
|
||||
m_direction->addItem(tr("Droite"));
|
||||
form->addRow(tr("Direction :"), m_direction);
|
||||
|
||||
const QPoint saved = savedStepOffset();
|
||||
// The saved value is a signed (dx, dy) pair, not itself a
|
||||
// spacing+direction pair, so it has to be decomposed back into the
|
||||
// two the dialog shows. Exactly one axis is ever non-zero (see
|
||||
// stepOffset()), so whichever one is picks the direction; a value
|
||||
// that somehow has neither (only possible if QSettings was hand-
|
||||
// edited) falls back to the same default stepOffset() would.
|
||||
int spacing = 1;
|
||||
Direction direction = Right;
|
||||
if (saved.x() > 0) { direction = Right; spacing = saved.x(); }
|
||||
else if (saved.x() < 0) { direction = Left; spacing = -saved.x(); }
|
||||
else if (saved.y() > 0) { direction = Down; spacing = saved.y(); }
|
||||
else if (saved.y() < 0) { direction = Up; spacing = -saved.y(); }
|
||||
m_spacing->setValue(spacing);
|
||||
m_direction->setCurrentIndex(static_cast<int>(direction));
|
||||
|
||||
auto *buttons = new QDialogButtonBox(
|
||||
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addLayout(form);
|
||||
layout->addWidget(buttons);
|
||||
}
|
||||
|
||||
QPoint DuplicateOffsetDialog::stepOffset() const
|
||||
{
|
||||
const int spacing = m_spacing->value();
|
||||
switch (static_cast<Direction>(m_direction->currentIndex())) {
|
||||
case Up: return QPoint(0, -spacing);
|
||||
case Down: return QPoint(0, spacing);
|
||||
case Left: return QPoint(-spacing, 0);
|
||||
case Right: return QPoint(spacing, 0);
|
||||
}
|
||||
return QPoint(spacing, 0); // unreachable; keeps -Wreturn-type quiet
|
||||
}
|
||||
|
||||
QPoint DuplicateOffsetDialog::savedStepOffset()
|
||||
{
|
||||
QSettings settings;
|
||||
if (!hasSavedStepOffset()) {
|
||||
return QPoint(1, 0); // default: one grid step to the right
|
||||
}
|
||||
return QPoint(settings.value(kOffsetXKey).toInt(),
|
||||
settings.value(kOffsetYKey).toInt());
|
||||
}
|
||||
|
||||
void DuplicateOffsetDialog::saveStepOffset(const QPoint &steps)
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue(kOffsetXKey, steps.x());
|
||||
settings.setValue(kOffsetYKey, steps.y());
|
||||
}
|
||||
|
||||
bool DuplicateOffsetDialog::hasSavedStepOffset()
|
||||
{
|
||||
QSettings settings;
|
||||
return settings.contains(kOffsetXKey) && settings.contains(kOffsetYKey);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef DUPLICATEOFFSETDIALOG_H
|
||||
#define DUPLICATEOFFSETDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPoint>
|
||||
|
||||
class QSpinBox;
|
||||
class QComboBox;
|
||||
|
||||
/**
|
||||
@brief The DuplicateOffsetDialog class
|
||||
Asks how far, and in which of the four cardinal directions, Ctrl+D
|
||||
(DiagramView::duplicate(), bugtracker #991) should offset a copy from
|
||||
its source. Shown once, then remembered: the answer is stored in
|
||||
QSettings and reused by every later Ctrl+D press without asking
|
||||
again, until this dialog is reopened deliberately.
|
||||
*/
|
||||
class DuplicateOffsetDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
enum Direction { Up, Down, Left, Right };
|
||||
|
||||
explicit DuplicateOffsetDialog(QWidget *parent = nullptr);
|
||||
|
||||
/// The offset in grid steps, positive along X to the right
|
||||
/// and positive along Y downward -- QET's own scene axes,
|
||||
/// matching the sign convention setPos() already uses
|
||||
/// everywhere else in this codebase.
|
||||
QPoint stepOffset() const;
|
||||
|
||||
/// Reads the last-confirmed spacing/direction from QSettings,
|
||||
/// or the default (1 step, right) if none was ever set.
|
||||
static QPoint savedStepOffset();
|
||||
/// Writes @p steps to QSettings, in the same X/Y convention
|
||||
/// as stepOffset().
|
||||
static void saveStepOffset(const QPoint &steps);
|
||||
/// Whether a direction/spacing has already been confirmed
|
||||
/// once, i.e. whether Ctrl+D can skip the dialog.
|
||||
static bool hasSavedStepOffset();
|
||||
|
||||
private:
|
||||
QSpinBox *m_spacing = nullptr;
|
||||
QComboBox *m_direction = nullptr;
|
||||
};
|
||||
|
||||
#endif // DUPLICATEOFFSETDIALOG_H
|
||||
@@ -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>
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
#include <QHash>
|
||||
#include <QModelIndex>
|
||||
#include <QStandardItem>
|
||||
#include <QTimer>
|
||||
#include <QUndoCommand>
|
||||
|
||||
static int src_txt_row = 0;
|
||||
@@ -130,7 +131,7 @@ QList<QStandardItem *> DynamicElementTextModel::itemsForText(
|
||||
{
|
||||
QList <QStandardItem *> qsi_list;
|
||||
|
||||
if(m_texts_list.keys().contains(deti))
|
||||
if(m_texts_list.contains(deti))
|
||||
return qsi_list;
|
||||
|
||||
QStandardItem *qsi = new QStandardItem(deti->toPlainText());
|
||||
@@ -250,8 +251,10 @@ QList<QStandardItem *> DynamicElementTextModel::itemsForText(
|
||||
QStandardItem *color = new QStandardItem(tr("Couleur"));
|
||||
color->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
|
||||
// Shown as a swatch next to the value, not as the text color: black
|
||||
// text (the default) is unreadable on a dark palette.
|
||||
QStandardItem *colora = new QStandardItem;
|
||||
colora->setData(deti->color(), Qt::ForegroundRole);
|
||||
colora->setData(deti->color(), Qt::DecorationRole);
|
||||
colora->setData(deti->color(), Qt::EditRole);
|
||||
colora->setData(DynamicElementTextModel::color, Qt::UserRole+1);
|
||||
colora->setFlags(Qt::ItemIsSelectable
|
||||
@@ -718,7 +721,7 @@ QUndoCommand *DynamicElementTextModel::undoForEditedGroup(
|
||||
*/
|
||||
void DynamicElementTextModel::addGroup(ElementTextItemGroup *group)
|
||||
{
|
||||
if(m_groups_list.keys().contains(group))
|
||||
if(m_groups_list.contains(group))
|
||||
return;
|
||||
|
||||
//Group
|
||||
@@ -869,7 +872,7 @@ void DynamicElementTextModel::addGroup(ElementTextItemGroup *group)
|
||||
*/
|
||||
void DynamicElementTextModel::removeGroup(ElementTextItemGroup *group)
|
||||
{
|
||||
if(m_groups_list.keys().contains(group))
|
||||
if(m_groups_list.contains(group))
|
||||
{
|
||||
QModelIndex group_index = m_groups_list.value(group)->index();
|
||||
this->removeRow(group_index.row(), group_index.parent());
|
||||
@@ -896,7 +899,7 @@ void DynamicElementTextModel::removeTextFromGroup(DynamicElementTextItem *deti,
|
||||
{
|
||||
Q_UNUSED(group)
|
||||
|
||||
if(m_texts_list.keys().contains(deti))
|
||||
if(m_texts_list.contains(deti))
|
||||
{
|
||||
QStandardItem *text_item = m_texts_list.value(deti);
|
||||
QModelIndex text_index = indexFromItem(text_item);
|
||||
@@ -961,7 +964,7 @@ ElementTextItemGroup *DynamicElementTextModel::groupFromItem(
|
||||
QModelIndex DynamicElementTextModel::indexFromGroup(
|
||||
ElementTextItemGroup *group) const
|
||||
{
|
||||
if(m_groups_list.keys().contains(group))
|
||||
if(m_groups_list.contains(group))
|
||||
return m_groups_list.value(group)->index();
|
||||
else
|
||||
return QModelIndex();
|
||||
@@ -1371,7 +1374,7 @@ void DynamicElementTextModel::setConnection(DynamicElementTextItem *deti, bool s
|
||||
{
|
||||
if(set)
|
||||
{
|
||||
if(m_hash_text_connect.keys().contains(deti))
|
||||
if(m_hash_text_connect.contains(deti))
|
||||
return;
|
||||
|
||||
QList<QMetaObject::Connection> connection_list;
|
||||
@@ -1390,7 +1393,7 @@ void DynamicElementTextModel::setConnection(DynamicElementTextItem *deti, bool s
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!m_hash_text_connect.keys().contains(deti))
|
||||
if(!m_hash_text_connect.contains(deti))
|
||||
return;
|
||||
|
||||
for (const QMetaObject::Connection& con : m_hash_text_connect.value(deti))
|
||||
@@ -1412,7 +1415,7 @@ void DynamicElementTextModel::setConnection(ElementTextItemGroup *group, bool se
|
||||
{
|
||||
if(set)
|
||||
{
|
||||
if(m_hash_group_connect.keys().contains(group))
|
||||
if(m_hash_group_connect.contains(group))
|
||||
return;
|
||||
|
||||
QList<QMetaObject::Connection> connection_list;
|
||||
@@ -1429,7 +1432,7 @@ void DynamicElementTextModel::setConnection(ElementTextItemGroup *group, bool se
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!m_hash_group_connect.keys().contains(group))
|
||||
if(!m_hash_group_connect.contains(group))
|
||||
return;
|
||||
|
||||
for (const QMetaObject::Connection& con : m_hash_group_connect.value(group))
|
||||
@@ -1499,7 +1502,7 @@ void DynamicElementTextModel::updateDataFromText(DynamicElementTextItem *deti,
|
||||
case color:
|
||||
{
|
||||
qsi->child(color_txt_row,1)->setData(deti->color(), Qt::EditRole);
|
||||
qsi->child(color_txt_row,1)->setData(deti->color(), Qt::ForegroundRole);
|
||||
qsi->child(color_txt_row,1)->setData(deti->color(), Qt::DecorationRole);
|
||||
break;
|
||||
}
|
||||
case pos:
|
||||
@@ -1595,6 +1598,15 @@ DynamicTextItemDelegate::DynamicTextItemDelegate(QObject *parent) :
|
||||
QStyledItemDelegate(parent)
|
||||
{}
|
||||
|
||||
void DynamicTextItemDelegate::commitAndCloseDeferred(QWidget *editor) const
|
||||
{
|
||||
auto *self = const_cast<DynamicTextItemDelegate *>(this);
|
||||
QTimer::singleShot(0, self, [self, editor]() {
|
||||
emit self->commitData(editor);
|
||||
emit self->closeEditor(editor);
|
||||
});
|
||||
}
|
||||
|
||||
QWidget *DynamicTextItemDelegate::createEditor(
|
||||
QWidget *parent,
|
||||
const QStyleOptionViewItem &option,
|
||||
@@ -1682,13 +1694,43 @@ QWidget *DynamicTextItemDelegate::createEditor(
|
||||
w->setProperty("ok", ok);
|
||||
}
|
||||
w->setObjectName("font_dialog");
|
||||
commitAndCloseDeferred(w);
|
||||
return w;
|
||||
}
|
||||
case DynamicElementTextModel::color:
|
||||
{
|
||||
QColorDialog *cd = new QColorDialog(index.data(Qt::EditRole).value<QColor>(), parent);
|
||||
cd->setObjectName("color_dialog");
|
||||
return cd;
|
||||
/* Like the font case above: run the dialog synchronously via
|
||||
* its static convenience function and stash the result on a
|
||||
* plain placeholder widget, rather than handing back the
|
||||
* QColorDialog itself as the item view's "editor".
|
||||
*
|
||||
* The item view's own Enter/Escape handling (the base
|
||||
* QStyledItemDelegate::eventFilter(), since "color_dialog"
|
||||
* isn't one of the objectNames special-cased in this
|
||||
* delegate's own eventFilter() above) treats whatever
|
||||
* createEditor() returned as a small inline editor it
|
||||
* commits and destroys directly on a key press. A QColorDialog
|
||||
* is not that: on Windows it can hand off to the native
|
||||
* color picker, whose own accept/close path then races the
|
||||
* view's -- pressing Enter fired both, one tearing down an
|
||||
* object the other was still using (case #323 on the bug
|
||||
* tracker: "QObject::installEventFilter(): Cannot filter
|
||||
* events for objects in a different thread" immediately
|
||||
* followed by a segfault; clicking the dialog's own OK
|
||||
* button with the mouse didn't reach the view's key
|
||||
* handling, so it didn't crash). Resolving the dialog
|
||||
* before returning removes the second, competing teardown
|
||||
* path entirely. */
|
||||
QColor color = QColorDialog::getColor(index.data(Qt::EditRole).value<QColor>(), parent);
|
||||
QWidget *w = new QWidget(parent);
|
||||
if (color.isValid())
|
||||
{
|
||||
w->setProperty("color", color);
|
||||
w->setProperty("ok", true);
|
||||
}
|
||||
w->setObjectName("color_dialog");
|
||||
commitAndCloseDeferred(w);
|
||||
return w;
|
||||
}
|
||||
case DynamicElementTextModel::pos:
|
||||
{
|
||||
@@ -1788,15 +1830,15 @@ void DynamicTextItemDelegate::setModelData(
|
||||
{
|
||||
if(QStandardItem *qsi = qsim->itemFromIndex(index))
|
||||
{
|
||||
QColorDialog *cd = static_cast<QColorDialog *> (editor);
|
||||
if (cd->result() == QDialog::Accepted)
|
||||
if (editor->property("ok").toBool() == true)
|
||||
{
|
||||
qsi->setData(cd->selectedColor(), Qt::EditRole);
|
||||
qsi->setData(cd->selectedColor(), Qt::ForegroundRole);
|
||||
QColor color = editor->property("color").value<QColor>();
|
||||
qsi->setData(color, Qt::EditRole);
|
||||
qsi->setData(color, Qt::DecorationRole);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
else if (editor->objectName() == "info_text")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,7 +18,11 @@
|
||||
#include "elementinfopartwidget.h"
|
||||
|
||||
#include "../SearchAndReplace/searchandreplaceworker.h"
|
||||
#include "../qetinformation.h"
|
||||
#include "ui_elementinfopartwidget.h"
|
||||
#include <QCompleter>
|
||||
#include <QRegularExpressionValidator>
|
||||
#include <QStringListModel>
|
||||
|
||||
#include <utility>
|
||||
|
||||
@@ -43,6 +47,14 @@ ElementInfoPartWidget::ElementInfoPartWidget(
|
||||
ui->label_->setText(translated_key);
|
||||
ui->m_erase_text->setVisible(false);
|
||||
|
||||
if (key_ == QETInformation::ELMT_WIDTH || key_ == QETInformation::ELMT_HEIGHT || key_ == QETInformation::ELMT_DEPTH)
|
||||
{
|
||||
auto *validator = new QETInformation::NumericInfoValidator(ui->line_edit);
|
||||
ui->line_edit->setValidator(validator);
|
||||
ui->line_edit->setPlaceholderText(tr("ex. 80.5"));
|
||||
ui->line_edit->setToolTip(tr("Nombre décimal avec un point comme séparateur (ex. 80.5)"));
|
||||
}
|
||||
|
||||
connect(ui->line_edit, &QLineEdit::textEdited,
|
||||
this, &ElementInfoPartWidget::textEdited);
|
||||
connect(ui->line_edit, &QLineEdit::textChanged,
|
||||
@@ -67,6 +79,15 @@ QString ElementInfoPartWidget::text() const
|
||||
return (ui->line_edit->text());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoPartWidget::hasAcceptableInput
|
||||
@return whether the line edit's current text satisfies its validator
|
||||
*/
|
||||
bool ElementInfoPartWidget::hasAcceptableInput() const
|
||||
{
|
||||
return ui->line_edit->hasAcceptableInput();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoPartWidget::setText
|
||||
Set text to line edit
|
||||
@@ -90,6 +111,30 @@ void ElementInfoPartWidget::setPlaceHolderText(const QString &text)
|
||||
ui->line_edit->setPlaceholderText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoPartWidget::setSuggestions
|
||||
Offer suggestions as a drop-down list while typing in the line edit,
|
||||
matching anywhere in the text and ignoring case.
|
||||
An empty list removes the drop-down.
|
||||
@param suggestions
|
||||
*/
|
||||
void ElementInfoPartWidget::setSuggestions(const QStringList &suggestions)
|
||||
{
|
||||
if (suggestions.isEmpty()) {
|
||||
ui->line_edit->setCompleter(nullptr);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_completer) {
|
||||
m_suggestions_model = new QStringListModel(this);
|
||||
m_completer = new QCompleter(m_suggestions_model, this);
|
||||
m_completer->setCaseSensitivity(Qt::CaseInsensitive);
|
||||
m_completer->setFilterMode(Qt::MatchContains);
|
||||
}
|
||||
m_suggestions_model->setStringList(suggestions);
|
||||
ui->line_edit->setCompleter(m_completer);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoPartWidget::setFocusTolineEdit
|
||||
Set the focus to the line edit
|
||||
|
||||
@@ -20,6 +20,9 @@
|
||||
|
||||
#include <QWidget>
|
||||
|
||||
class QCompleter;
|
||||
class QStringListModel;
|
||||
|
||||
namespace Ui {
|
||||
class ElementInfoPartWidget;
|
||||
}
|
||||
@@ -41,11 +44,12 @@ class ElementInfoPartWidget : public QWidget
|
||||
QWidget *parent = nullptr);
|
||||
~ElementInfoPartWidget() override;
|
||||
|
||||
QString key () const
|
||||
{return key_;}
|
||||
QString key () const {return key_;}
|
||||
QString text () const;
|
||||
bool hasAcceptableInput() const;
|
||||
void setText (const QString &);
|
||||
void setPlaceHolderText (const QString &text);
|
||||
void setSuggestions (const QStringList &suggestions);
|
||||
void setFocusTolineEdit();
|
||||
void setEnabled(bool e);
|
||||
void setDisabled(bool d);
|
||||
@@ -65,6 +69,8 @@ class ElementInfoPartWidget : public QWidget
|
||||
Ui::ElementInfoPartWidget *ui;
|
||||
QString key_;
|
||||
bool m_show_erase = false;
|
||||
QCompleter *m_completer = nullptr;
|
||||
QStringListModel *m_suggestions_model = nullptr;
|
||||
};
|
||||
|
||||
#endif // ELEMENTINFOPARTWIDGET_H
|
||||
|
||||
@@ -16,13 +16,18 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "elementinfowidget.h"
|
||||
#include "../qet.h"
|
||||
#include <QCheckBox>
|
||||
#include <QPushButton>
|
||||
#include "../diagram.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "../dataBase/projectdatabase.h"
|
||||
#include "../qetinformation.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../ui_elementinfowidget.h"
|
||||
#include "../undocommand/changeelementinformationcommand.h"
|
||||
#include "customelementinfopartwidget.h"
|
||||
#include "elementinfopartwidget.h"
|
||||
|
||||
/**
|
||||
@@ -47,6 +52,7 @@ ElementInfoWidget::ElementInfoWidget(Element *elmt, QWidget *parent) :
|
||||
ElementInfoWidget::~ElementInfoWidget()
|
||||
{
|
||||
qDeleteAll(m_eipw_list);
|
||||
qDeleteAll(m_custom_eipw_list);
|
||||
delete ui;
|
||||
}
|
||||
|
||||
@@ -145,7 +151,7 @@ bool ElementInfoWidget::event(QEvent *event)
|
||||
{
|
||||
if (event -> type() == QEvent::WindowActivate || event -> type() == QEvent::Show)
|
||||
{
|
||||
QTimer::singleShot(250, this, SLOT(firstActivated()));
|
||||
QTimer::singleShot(250, this, &ElementInfoWidget::firstActivated);
|
||||
m_first_activation = false;
|
||||
}
|
||||
}
|
||||
@@ -201,12 +207,25 @@ void ElementInfoWidget::buildInterface()
|
||||
keys = QETInformation::elementInfoKeys();
|
||||
}
|
||||
|
||||
//"exclude_from_bom" is part of elementInfoKeys() because the project
|
||||
//database builds the element_info table from that list, but it is not
|
||||
//a free-text property: it already has its own check box below. Without
|
||||
//this it also gets a generic edit row, and since translatedInfoKey()
|
||||
//has no entry for it that row carries no label at all - an anonymous
|
||||
//line that currentInfo() then fills with "true"/"false".
|
||||
keys.removeAll(QStringLiteral("exclude_from_bom"));
|
||||
|
||||
for (auto str : keys)
|
||||
{
|
||||
ElementInfoPartWidget *eipw = new ElementInfoPartWidget(str, QETInformation::translatedInfoKey(str), this);
|
||||
ui->scroll_vlayout->addWidget(eipw);
|
||||
m_eipw_list << eipw;
|
||||
}
|
||||
|
||||
m_add_custom_property_btn = new QPushButton(tr("Ajouter une propriété personnalisée"), this);
|
||||
connect(m_add_custom_property_btn, &QPushButton::clicked, this, [this]() { addCustomProperty(); });
|
||||
ui->scroll_vlayout->addWidget(m_add_custom_property_btn);
|
||||
|
||||
ui->scroll_vlayout->addStretch();
|
||||
|
||||
// Existing potential isolating checkbox
|
||||
@@ -235,6 +254,67 @@ void ElementInfoWidget::buildInterface()
|
||||
m_potential_isolating_cb->setVisible(false);
|
||||
}
|
||||
}
|
||||
/**
|
||||
@brief ElementInfoWidget::predefinedKeys
|
||||
@return every key this widget already exposes a dedicated row for,
|
||||
whether through ElementInfoPartWidget (the ~40 ELMT_* keys) or one
|
||||
of the standalone checkboxes. Anything present in the element's
|
||||
informations but absent from this list is a user-defined custom
|
||||
property.
|
||||
*/
|
||||
QStringList ElementInfoWidget::predefinedKeys() const
|
||||
{
|
||||
QStringList keys = (m_element.data()->elementData().m_type == ElementData::Terminal)
|
||||
? QETInformation::terminalElementInfoKeys()
|
||||
: QETInformation::elementInfoKeys();
|
||||
|
||||
keys << QStringLiteral("auto_num_locked")
|
||||
<< QStringLiteral("potential_isolating")
|
||||
<< QStringLiteral("exclude_from_bom");
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoWidget::addCustomProperty
|
||||
Append a new user-defined key/value row to the widget.
|
||||
@param key initial key, left empty for a freshly added row
|
||||
@param value initial value
|
||||
*/
|
||||
void ElementInfoWidget::addCustomProperty(const QString &key, const QString &value)
|
||||
{
|
||||
auto *widget = new CustomElementInfoPartWidget(key, value, this);
|
||||
|
||||
const int insert_index = ui->scroll_vlayout->indexOf(m_add_custom_property_btn);
|
||||
ui->scroll_vlayout->insertWidget(insert_index >= 0 ? insert_index : ui->scroll_vlayout->count(), widget);
|
||||
m_custom_eipw_list << widget;
|
||||
|
||||
connect(widget, &CustomElementInfoPartWidget::removeRequested, this, &ElementInfoWidget::removeCustomProperty);
|
||||
connect(widget, &CustomElementInfoPartWidget::changed, this, [this]() {
|
||||
if (m_live_edit) apply();
|
||||
});
|
||||
|
||||
if (key.isEmpty()) {
|
||||
widget->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoWidget::removeCustomProperty
|
||||
Remove a user-defined key/value row.
|
||||
@param widget the row to remove
|
||||
*/
|
||||
void ElementInfoWidget::removeCustomProperty(CustomElementInfoPartWidget *widget)
|
||||
{
|
||||
if (!m_custom_eipw_list.removeOne(widget))
|
||||
return;
|
||||
|
||||
ui->scroll_vlayout->removeWidget(widget);
|
||||
widget->deleteLater();
|
||||
|
||||
if (m_live_edit) apply();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoWidget::infoPartWidgetForKey
|
||||
@param key
|
||||
@@ -243,7 +323,7 @@ void ElementInfoWidget::buildInterface()
|
||||
*/
|
||||
ElementInfoPartWidget *ElementInfoWidget::infoPartWidgetForKey(const QString &key) const
|
||||
{
|
||||
for (const auto &eipw : qAsConst(m_eipw_list))
|
||||
for (const auto &eipw : std::as_const(m_eipw_list))
|
||||
{
|
||||
if (eipw->key() == key)
|
||||
return eipw;
|
||||
@@ -252,6 +332,55 @@ ElementInfoPartWidget *ElementInfoWidget::infoPartWidgetForKey(const QString &ke
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoWidget::updateSuggestions
|
||||
Offer, for each information, the values already used by the other
|
||||
elements of the project (supplier, manufacturer...), so they can be
|
||||
picked instead of typed again.
|
||||
The values come from the project database rather than from the
|
||||
diagrams, which already holds them in the element_info table.
|
||||
*/
|
||||
void ElementInfoWidget::updateSuggestions()
|
||||
{
|
||||
Diagram *diagram = m_element ? m_element->diagram() : nullptr;
|
||||
QETProject *project = diagram ? diagram->project() : nullptr;
|
||||
if (!project || !project->dataBase()) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Only a column of element_info can be queried. The key is checked
|
||||
//against that list rather than trusted, because it becomes part of
|
||||
//the SQL text.
|
||||
const QStringList columns = QETInformation::elementInfoKeys();
|
||||
const QString uuid = m_element->uuid().toString();
|
||||
|
||||
for (ElementInfoPartWidget *eipw : m_eipw_list)
|
||||
{
|
||||
const QString key = eipw->key();
|
||||
//A label identifies one element, suggesting the others is noise
|
||||
if (key == QETInformation::ELMT_LABEL || !columns.contains(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//"Schneider" and "schneider" are offered once, spelled the way
|
||||
//most elements spell it: SQLite takes the bare column v from
|
||||
//the row that holds MAX(n).
|
||||
QStringList values;
|
||||
auto query = project->dataBase()->newQuery(QStringLiteral(
|
||||
"SELECT v, MAX(n) FROM ("
|
||||
"SELECT \"%1\" AS v, COUNT(*) AS n FROM element_info "
|
||||
"WHERE \"%1\" IS NOT NULL AND \"%1\" != '' "
|
||||
"AND element_uuid != '%2' "
|
||||
"GROUP BY \"%1\") "
|
||||
"GROUP BY v COLLATE NOCASE "
|
||||
"ORDER BY v COLLATE NOCASE").arg(key, uuid));
|
||||
while (query.next()) {
|
||||
values << query.value(0).toString();
|
||||
}
|
||||
eipw->setSuggestions(values);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoWidget::updateUi
|
||||
fill information fetch in m_element_info to the
|
||||
@@ -271,21 +400,37 @@ void ElementInfoWidget::updateUi()
|
||||
for (ElementInfoPartWidget *eipw : m_eipw_list) {
|
||||
eipw -> setText (element_info[eipw->key()].toString());
|
||||
}
|
||||
updateSuggestions();
|
||||
|
||||
// Rebuild the custom-property rows to match whatever
|
||||
// user-defined keys this element currently carries.
|
||||
while (!m_custom_eipw_list.isEmpty()) {
|
||||
CustomElementInfoPartWidget *w = m_custom_eipw_list.takeLast();
|
||||
ui->scroll_vlayout->removeWidget(w);
|
||||
delete w;
|
||||
}
|
||||
const auto known_keys = predefinedKeys();
|
||||
for (const QString &key : element_info.keys()) {
|
||||
if (!known_keys.contains(key)) {
|
||||
addCustomProperty(key, element_info[key].toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Load the lock status for auto numbering
|
||||
if (m_element->elementData().m_type == ElementData::Terminal) {
|
||||
QString lock_value = element_info.value(QStringLiteral("auto_num_locked")).toString();
|
||||
ui->m_auto_num_locked_cb->setChecked(lock_value == QLatin1String("true"));
|
||||
ui->m_auto_num_locked_cb->setChecked(QET::infoFlagIsTrue(lock_value));
|
||||
|
||||
// English: Load the potential isolating status from the element information mapping
|
||||
if (m_potential_isolating_cb) {
|
||||
QString isolating_value = element_info.value(QStringLiteral("potential_isolating")).toString();
|
||||
m_potential_isolating_cb->setChecked(isolating_value == QLatin1String("true"));
|
||||
m_potential_isolating_cb->setChecked(QET::infoFlagIsTrue(isolating_value));
|
||||
}
|
||||
}
|
||||
// English: Load the BOM exclusion status from the element information mapping
|
||||
if (m_exclude_from_bom_cb) {
|
||||
QString exclude_bom_value = element_info.value(QStringLiteral("exclude_from_bom")).toString();
|
||||
m_exclude_from_bom_cb->setChecked(exclude_bom_value == QLatin1String("true"));
|
||||
m_exclude_from_bom_cb->setChecked(QET::infoFlagIsTrue(exclude_bom_value));
|
||||
}
|
||||
|
||||
if (m_live_edit) {
|
||||
@@ -301,8 +446,11 @@ DiagramContext ElementInfoWidget::currentInfo() const
|
||||
{
|
||||
DiagramContext info_;
|
||||
|
||||
for (const auto &eipw : qAsConst(m_eipw_list))
|
||||
for (const auto &eipw : std::as_const(m_eipw_list))
|
||||
{
|
||||
if (!eipw->hasAcceptableInput())
|
||||
continue;
|
||||
|
||||
//add value only if they're something to store
|
||||
if (!eipw->text().isEmpty())
|
||||
{
|
||||
@@ -314,6 +462,17 @@ DiagramContext ElementInfoWidget::currentInfo() const
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &custom : std::as_const(m_custom_eipw_list))
|
||||
{
|
||||
if (custom->hasValidKey() && !custom->value().isEmpty())
|
||||
{
|
||||
QString txt{custom->value()};
|
||||
txt.remove(QStringLiteral("\r"));
|
||||
txt.remove(QStringLiteral("\n"));
|
||||
info_.addValue(custom->key(), txt);
|
||||
}
|
||||
}
|
||||
|
||||
// Save the auto numbering lock status
|
||||
if (m_element->elementData().m_type == ElementData::Terminal) {
|
||||
info_.addValue(QStringLiteral("auto_num_locked"), ui->m_auto_num_locked_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"));
|
||||
|
||||
@@ -26,8 +26,10 @@
|
||||
class Element;
|
||||
class QUndoCommand;
|
||||
class ElementInfoPartWidget;
|
||||
class CustomElementInfoPartWidget;
|
||||
class ChangeElementInformationCommand;
|
||||
class QCheckBox;
|
||||
class QPushButton;
|
||||
|
||||
namespace Ui {
|
||||
class ElementInfoWidget;
|
||||
@@ -63,15 +65,21 @@ class ElementInfoWidget : public AbstractElementPropertiesEditorWidget
|
||||
private:
|
||||
void buildInterface();
|
||||
ElementInfoPartWidget *infoPartWidgetForKey(const QString &key) const;
|
||||
QStringList predefinedKeys() const;
|
||||
void updateSuggestions();
|
||||
|
||||
private slots:
|
||||
void firstActivated();
|
||||
void elementInfoChange();
|
||||
void addCustomProperty(const QString &key = QString(), const QString &value = QString());
|
||||
void removeCustomProperty(CustomElementInfoPartWidget *widget);
|
||||
|
||||
//ATTRIBUTES
|
||||
private:
|
||||
Ui::ElementInfoWidget *ui;
|
||||
QList <ElementInfoPartWidget *> m_eipw_list;
|
||||
QList <CustomElementInfoPartWidget *> m_custom_eipw_list;
|
||||
QPushButton *m_add_custom_property_btn = nullptr;
|
||||
QCheckBox *m_potential_isolating_cb = nullptr;
|
||||
QCheckBox *m_exclude_from_bom_cb = nullptr;
|
||||
bool m_first_activation;
|
||||
|
||||
@@ -23,12 +23,15 @@
|
||||
#include "../qetgraphicsitem/dynamicelementtextitem.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "../qetgraphicsitem/elementtextitemgroup.h"
|
||||
#include "../qetgraphicsitem/masterelement.h"
|
||||
#include "../qeticons.h"
|
||||
#include "dynamicelementtextitemeditor.h"
|
||||
#include "elementinfowidget.h"
|
||||
#include "linksingleelementwidget.h"
|
||||
#include "masterpropertieswidget.h"
|
||||
#include "plclinkwidget.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QLabel>
|
||||
#include <QUndoStack>
|
||||
#include <QVBoxLayout>
|
||||
@@ -303,7 +306,10 @@ void ElementPropertiesWidget::updateUi()
|
||||
m_list_editor << new ElementInfoWidget(m_element, this);
|
||||
break;
|
||||
case Element::Slave:
|
||||
m_list_editor << new LinkSingleElementWidget(m_element, this);
|
||||
if (m_element->elementData().m_slave_type == ElementData::PLCSlave)
|
||||
m_list_editor << new PlcLinkWidget(m_element, this);
|
||||
else
|
||||
m_list_editor << new LinkSingleElementWidget(m_element, this);
|
||||
break;
|
||||
case Element::Terminale:
|
||||
m_list_editor << new ElementInfoWidget(m_element, this);
|
||||
@@ -375,9 +381,48 @@ QWidget *ElementPropertiesWidget::generalWidget()
|
||||
description_string += QString(tr("Rotation : %1°\n")).arg(m_element.data()->rotation());
|
||||
description_string += QString(tr("Dimensions : %1*%2\n")).arg(m_element -> size().width()).arg(m_element -> size().height());
|
||||
description_string += QString(tr("Bornes : %1\n")).arg(m_element -> terminals().count());
|
||||
if (m_element->linkType() == Element::Master){
|
||||
description_string += QString(tr("Nombre maximum de contacts esclaves définis : %1\n")).arg(m_element -> elementData().m_max_slaves);
|
||||
description_string += QString(tr("Nombre de contacts esclaves utilisés : %1\n")).arg(m_element ->linkedElements().count());
|
||||
if (m_element->linkType() == Element::Master)
|
||||
{
|
||||
//The declared limit is optional: -1 means the element sets no
|
||||
//limit at all, which is worth saying rather than printing "-1".
|
||||
const int max_slaves = m_element->elementData().m_max_slaves;
|
||||
description_string += max_slaves == -1
|
||||
? QString(tr("Nombre maximum de contacts esclaves définis : non défini\n"))
|
||||
: QString(tr("Nombre maximum de contacts esclaves définis : %1\n")).arg(max_slaves);
|
||||
|
||||
//Left as a count of linked elements: the line above is a number
|
||||
//of slots, and a slave fills one slot however many contacts it
|
||||
//carries, so the two stay in the same unit.
|
||||
description_string += QString(tr("Nombre de contacts esclaves utilisés : %1\n")).arg(m_element->linkedElements().count());
|
||||
|
||||
//The breakdown below is in contacts, not slots: it answers how
|
||||
//many contacts an auxiliary block must provide.
|
||||
const MasterElement *master =
|
||||
static_cast<const MasterElement *>(m_element.data());
|
||||
const ContactUsage usage = master->contactUsage();
|
||||
const ContactUsage capacity = master->contactCapacity();
|
||||
|
||||
if (capacity.total() > 0)
|
||||
{
|
||||
//The element declares contact groups, so it can say not
|
||||
//only what has been used but what it has to offer. A type
|
||||
//used beyond what is declared shows as e.g. "1/0", which
|
||||
//is the point: it says this contact does not fit the part.
|
||||
description_string += QString(tr(" Contacts : NO : %1/%2, NC : %3/%4, inverseurs : %5/%6, autres : %7/%8\n"))
|
||||
.arg(usage.no).arg(capacity.no)
|
||||
.arg(usage.nc).arg(capacity.nc)
|
||||
.arg(usage.sw).arg(capacity.sw)
|
||||
.arg(usage.other).arg(capacity.other);
|
||||
}
|
||||
else if (usage.total() > 0)
|
||||
{
|
||||
//No declared groups, so a plain count of what is in use.
|
||||
description_string += QString(tr(" Contacts : NO : %1, NC : %2, inverseurs : %3, autres : %4\n"))
|
||||
.arg(usage.no)
|
||||
.arg(usage.nc)
|
||||
.arg(usage.sw)
|
||||
.arg(usage.other);
|
||||
}
|
||||
}
|
||||
description_string += QString(tr("Emplacement : %1\n")).arg(m_element.data()->location().toString());
|
||||
|
||||
@@ -392,6 +437,18 @@ QWidget *ElementPropertiesWidget::generalWidget()
|
||||
label->setTextInteractionFlags(Qt::TextEditorInteraction);
|
||||
vlayout_->addWidget(label);
|
||||
|
||||
//checkbox to lock the element position on the diagram
|
||||
//(same mechanism already used by images and drawn shapes)
|
||||
QCheckBox *lock_pos_cb = new QCheckBox(tr("Verrouiller la position"), general_widget);
|
||||
lock_pos_cb->setChecked(!m_element->isMovable());
|
||||
QPointer<Element> element = m_element;
|
||||
connect(lock_pos_cb, &QCheckBox::clicked, this, [element](bool checked) {
|
||||
if (element) {
|
||||
element->setMovable(!checked);
|
||||
}
|
||||
});
|
||||
vlayout_->addWidget(lock_pos_cb);
|
||||
|
||||
//widget for the pixmap
|
||||
QLabel *pix = new QLabel(general_widget);
|
||||
vlayout_->addWidget(pix, 0, Qt::AlignHCenter);
|
||||
@@ -399,9 +456,9 @@ QWidget *ElementPropertiesWidget::generalWidget()
|
||||
|
||||
//button widget
|
||||
QPushButton *find_in_panel = new QPushButton(QET::Icons::ZoomDraw, tr("Retrouver dans le panel"), general_widget);
|
||||
connect(find_in_panel, SIGNAL(clicked()), this, SLOT(findInPanel()));
|
||||
connect(find_in_panel, &QPushButton::clicked, this, &ElementPropertiesWidget::findInPanel);
|
||||
QPushButton *edit_element = new QPushButton(QET::Icons::ElementEdit, tr("Éditer l'élément"), general_widget);
|
||||
connect(edit_element, SIGNAL(clicked()), this, SLOT(editElement()));
|
||||
connect(edit_element, &QPushButton::clicked, this, &ElementPropertiesWidget::editElement);
|
||||
QHBoxLayout *hlayout_ = new QHBoxLayout;
|
||||
hlayout_->addWidget(find_in_panel);
|
||||
hlayout_->addWidget(edit_element);
|
||||
|
||||
@@ -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 ¢er) {
|
||||
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();
|
||||
}
|
||||
@@ -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
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"));
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "jumptoelementdialog.h"
|
||||
|
||||
#include "../diagram.h"
|
||||
#include "../diagramview.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
|
||||
#include <QEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QLineEdit>
|
||||
#include <QListWidget>
|
||||
#include <QRegularExpression>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
/**
|
||||
@brief JumpToElementDialog::JumpToElementDialog
|
||||
@param diagram : the diagram whose elements can be jumped to
|
||||
@param parent
|
||||
*/
|
||||
JumpToElementDialog::JumpToElementDialog(Diagram *diagram, QWidget *parent) :
|
||||
QDialog(parent),
|
||||
m_diagram(diagram)
|
||||
{
|
||||
setWindowTitle(tr("Atteindre un élément", "window title"));
|
||||
|
||||
m_filter_edit = new QLineEdit(this);
|
||||
m_filter_edit->setPlaceholderText(tr("Nom, label ou information de l'élément, ou case (ex. B13 ou 3-B13)…"));
|
||||
m_filter_edit->installEventFilter(this);
|
||||
|
||||
m_result_list = new QListWidget(this);
|
||||
m_result_list->setFocusPolicy(Qt::NoFocus);
|
||||
|
||||
auto *layout = new QVBoxLayout(this);
|
||||
layout->addWidget(m_filter_edit);
|
||||
layout->addWidget(m_result_list);
|
||||
setLayout(layout);
|
||||
resize(420, 320);
|
||||
|
||||
connect(m_filter_edit, &QLineEdit::textChanged, this, &JumpToElementDialog::updateFilteredList);
|
||||
connect(m_result_list, &QListWidget::itemActivated, this, &JumpToElementDialog::activateCurrentItem);
|
||||
|
||||
buildCandidates();
|
||||
updateFilteredList(QString());
|
||||
m_filter_edit->setFocus();
|
||||
}
|
||||
|
||||
JumpToElementDialog::~JumpToElementDialog()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
@brief JumpToElementDialog::buildCandidates
|
||||
Collect every element on m_diagram into m_candidates, along with the
|
||||
searchable text used to filter it (its type name, its label, and its
|
||||
other element informations).
|
||||
*/
|
||||
void JumpToElementDialog::buildCandidates()
|
||||
{
|
||||
m_candidates.clear();
|
||||
if (!m_diagram) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto item : m_diagram->items()) {
|
||||
Element *element = qgraphicsitem_cast<Element *>(item);
|
||||
if (!element) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString label = element->elementInformations().value(QStringLiteral("label")).toString();
|
||||
const QString name = element->name();
|
||||
|
||||
Candidate candidate;
|
||||
candidate.element = element;
|
||||
candidate.label = label;
|
||||
candidate.display_text = label.isEmpty() ? name : (label + QStringLiteral(" — ") + name);
|
||||
|
||||
QStringList search_parts;
|
||||
search_parts << label << name;
|
||||
const DiagramContext infos = element->elementInformations();
|
||||
for (const QString &key : infos.keys()) {
|
||||
search_parts << infos.value(key).toString();
|
||||
}
|
||||
candidate.search_text = search_parts.join(QLatin1Char(' ')).toLower();
|
||||
|
||||
m_candidates << candidate;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief JumpToElementDialog::updateFilteredList
|
||||
Refill m_result_list with every candidate whose search text contains
|
||||
@a filter_text (case-insensitive), and select the first match.
|
||||
@param filter_text
|
||||
*/
|
||||
void JumpToElementDialog::updateFilteredList(const QString &filter_text)
|
||||
{
|
||||
m_result_list->clear();
|
||||
|
||||
const QString needle = filter_text.trimmed().toLower();
|
||||
bool exact_label_match = false;
|
||||
for (int i = 0; i < m_candidates.size(); ++i) {
|
||||
const Candidate &candidate = m_candidates.at(i);
|
||||
if (!candidate.element) {
|
||||
continue;
|
||||
}
|
||||
if (!needle.isEmpty() && !candidate.search_text.contains(needle)) {
|
||||
continue;
|
||||
}
|
||||
auto *list_item = new QListWidgetItem(candidate.display_text, m_result_list);
|
||||
list_item->setData(Qt::UserRole, i);
|
||||
if (candidate.label.compare(needle, Qt::CaseInsensitive) == 0) {
|
||||
exact_label_match = true;
|
||||
}
|
||||
}
|
||||
|
||||
//A cell of the border, on this folio (ex : B13) or on another one
|
||||
//(ex : 3-B13, folio 3, the way cross references write it), comes
|
||||
//first, unless an element is labelled exactly like it: Enter keeps
|
||||
//jumping to that element.
|
||||
if (QListWidgetItem *cell_item = cellItem(needle)) {
|
||||
m_result_list->insertItem(exact_label_match ? m_result_list->count() : 0,
|
||||
cell_item);
|
||||
}
|
||||
|
||||
if (m_result_list->count() > 0) {
|
||||
m_result_list->setCurrentRow(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief JumpToElementDialog::activateCurrentItem
|
||||
Select the element corresponding to the currently highlighted result
|
||||
on the diagram, scroll it into view, and close this dialog.
|
||||
*/
|
||||
void JumpToElementDialog::activateCurrentItem()
|
||||
{
|
||||
QListWidgetItem *current = m_result_list->currentItem();
|
||||
if (!current || !m_diagram) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
const int index = current->data(Qt::UserRole).toInt();
|
||||
if (index == -1) {
|
||||
const QList<Diagram *> diagrams = m_diagram->project()
|
||||
? m_diagram->project()->diagrams()
|
||||
: QList<Diagram *>();
|
||||
const int folio = current->data(Qt::UserRole + 2).toInt();
|
||||
if (folio >= 0 && folio < diagrams.size()) {
|
||||
zoomToCell(diagrams.at(folio), current->data(Qt::UserRole + 1).toRectF());
|
||||
}
|
||||
accept();
|
||||
return;
|
||||
}
|
||||
if (index < 0 || index >= m_candidates.size()) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
Element *element = m_candidates.at(index).element;
|
||||
if (!element) {
|
||||
reject();
|
||||
return;
|
||||
}
|
||||
|
||||
m_diagram->clearSelection();
|
||||
element->setSelected(true);
|
||||
element->ensureVisible();
|
||||
accept();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief JumpToElementDialog::cellItem
|
||||
@param needle : the typed text, trimmed and lower case
|
||||
@return a new list item for the cell \a needle names, on m_diagram
|
||||
(ex : b13) or on the folio at a position of the project (ex : 3-b13,
|
||||
3b13, p3b13), or nullptr if \a needle names no cell of an existing
|
||||
folio. The item holds -1, the cell rect and the folio position.
|
||||
*/
|
||||
QListWidgetItem *JumpToElementDialog::cellItem(const QString &needle) const
|
||||
{
|
||||
if (!m_diagram || !m_diagram->project()) {
|
||||
return nullptr;
|
||||
}
|
||||
const QList<Diagram *> diagrams = m_diagram->project()->diagrams();
|
||||
|
||||
Diagram *diagram = m_diagram;
|
||||
QString cell = needle;
|
||||
QString text;
|
||||
QRectF cell_rect = diagram->border_and_titleblock.cellRect(cell);
|
||||
if (cell_rect.isNull()) {
|
||||
//A folio position then a cell, like %f-%l%c in cross references
|
||||
static const QRegularExpression folio_cell_re(
|
||||
QStringLiteral("^p?\\s*(\\d{1,4})\\s*[-/.:]?\\s*([a-z]+\\s*\\d{1,4})$"));
|
||||
const QRegularExpressionMatch match = folio_cell_re.match(needle);
|
||||
if (!match.hasMatch()) {
|
||||
return nullptr;
|
||||
}
|
||||
const int folio = match.captured(1).toInt();
|
||||
if (folio < 1 || folio > diagrams.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
diagram = diagrams.at(folio - 1);
|
||||
cell = match.captured(2);
|
||||
cell_rect = diagram->border_and_titleblock.cellRect(cell);
|
||||
if (cell_rect.isNull()) {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
cell = cell.remove(QLatin1Char(' ')).toUpper();
|
||||
if (diagram == m_diagram) {
|
||||
text = tr("Case %1").arg(cell);
|
||||
} else {
|
||||
const QString title = diagram->title();
|
||||
text = title.isEmpty()
|
||||
? tr("Folio %1, case %2").arg(diagrams.indexOf(diagram) + 1).arg(cell)
|
||||
: tr("Folio %1 (%2), case %3").arg(diagrams.indexOf(diagram) + 1).arg(title, cell);
|
||||
}
|
||||
|
||||
auto *item = new QListWidgetItem(text);
|
||||
item->setData(Qt::UserRole, -1);
|
||||
item->setData(Qt::UserRole + 1, cell_rect);
|
||||
item->setData(Qt::UserRole + 2, diagrams.indexOf(diagram));
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief JumpToElementDialog::zoomToCell
|
||||
Show \a diagram and zoom its view on \a cell_rect with one cell of
|
||||
context around it.
|
||||
@param diagram : the folio holding the cell
|
||||
@param cell_rect : the cell, in scene coordinate
|
||||
*/
|
||||
void JumpToElementDialog::zoomToCell(Diagram *diagram, const QRectF &cell_rect)
|
||||
{
|
||||
const QRectF rect = cell_rect.adjusted(-cell_rect.width(), -cell_rect.height(),
|
||||
cell_rect.width(), cell_rect.height());
|
||||
const bool other_folio = diagram != m_diagram;
|
||||
if (other_folio) {
|
||||
diagram->showMe();
|
||||
}
|
||||
for (QGraphicsView *view : diagram->views()) {
|
||||
if (auto *diagram_view = qobject_cast<DiagramView *>(view)) {
|
||||
if (other_folio) {
|
||||
//A folio shown for the first time is laid out at
|
||||
//its size once the tab switch has been handled
|
||||
QMetaObject::invokeMethod(diagram_view, [diagram_view, rect]() {
|
||||
diagram_view->zoomToRect(rect);
|
||||
}, Qt::QueuedConnection);
|
||||
} else {
|
||||
diagram_view->zoomToRect(rect);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief JumpToElementDialog::eventFilter
|
||||
Redirect Up/Down/Enter/Escape typed in the filter field to the result
|
||||
list, so the user never has to leave the text field to navigate or
|
||||
confirm a choice.
|
||||
*/
|
||||
bool JumpToElementDialog::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (watched == m_filter_edit && event->type() == QEvent::KeyPress) {
|
||||
auto *key_event = static_cast<QKeyEvent *>(event);
|
||||
switch (key_event->key()) {
|
||||
case Qt::Key_Down:
|
||||
case Qt::Key_Up: {
|
||||
const int row_count = m_result_list->count();
|
||||
if (row_count == 0) {
|
||||
return true;
|
||||
}
|
||||
int row = m_result_list->currentRow();
|
||||
row = key_event->key() == Qt::Key_Down
|
||||
? (row + 1) % row_count
|
||||
: (row - 1 + row_count) % row_count;
|
||||
m_result_list->setCurrentRow(row);
|
||||
return true;
|
||||
}
|
||||
case Qt::Key_Return:
|
||||
case Qt::Key_Enter:
|
||||
activateCurrentItem();
|
||||
return true;
|
||||
case Qt::Key_Escape:
|
||||
reject();
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return QDialog::eventFilter(watched, event);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef JUMPTOELEMENTDIALOG_H
|
||||
#define JUMPTOELEMENTDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QPointer>
|
||||
|
||||
class Diagram;
|
||||
class Element;
|
||||
class QLineEdit;
|
||||
class QListWidget;
|
||||
class QListWidgetItem;
|
||||
|
||||
/**
|
||||
@brief The JumpToElementDialog class
|
||||
A lightweight, transient "quick open" popup: type part of an element's
|
||||
label or other information to live-filter the elements on a diagram,
|
||||
then Enter to select the chosen element on the diagram and scroll it
|
||||
into view. Typing a cell of the border instead (ex : B13, or 3-B13 for
|
||||
the third folio of the project) offers to zoom on that cell. Up/Down move through the filtered list, Escape cancels
|
||||
without changing the current selection.
|
||||
*/
|
||||
class JumpToElementDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit JumpToElementDialog(Diagram *diagram, QWidget *parent = nullptr);
|
||||
~JumpToElementDialog() override;
|
||||
|
||||
protected:
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void updateFilteredList(const QString &filter_text);
|
||||
void activateCurrentItem();
|
||||
|
||||
private:
|
||||
void buildCandidates();
|
||||
QListWidgetItem *cellItem(const QString &needle) const;
|
||||
void zoomToCell(Diagram *diagram, const QRectF &cell_rect);
|
||||
|
||||
struct Candidate {
|
||||
QPointer<Element> element;
|
||||
QString label;
|
||||
QString display_text;
|
||||
QString search_text;
|
||||
};
|
||||
|
||||
QPointer<Diagram> m_diagram;
|
||||
QList<Candidate> m_candidates;
|
||||
QLineEdit *m_filter_edit;
|
||||
QListWidget *m_result_list;
|
||||
};
|
||||
|
||||
#endif // JUMPTOELEMENTDIALOG_H
|
||||
@@ -16,7 +16,7 @@
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "linksingleelementwidget.h"
|
||||
#include "../qetgraphicsitem/masterelement.h"
|
||||
#include "contactgroupselectiondialog.h"
|
||||
#include "../qetgraphicsitem/conductor.h"
|
||||
#include "../diagram.h"
|
||||
#include "../diagramposition.h"
|
||||
@@ -24,10 +24,16 @@
|
||||
#include "../elementprovider.h"
|
||||
#include "../undocommand/linkelementcommand.h"
|
||||
#include "../qetinformation.h"
|
||||
|
||||
#include "../qetproject.h"
|
||||
#include "../qetgraphicsitem/masterelement.h"
|
||||
#include "../ui_linksingleelementwidget.h"
|
||||
|
||||
#include <QTreeWidgetItem>
|
||||
#include <QInputDialog>
|
||||
#include <QCheckBox>
|
||||
#include <QSettings>
|
||||
#include <QSignalBlocker>
|
||||
|
||||
|
||||
/**
|
||||
@brief LinkSingleElementWidget::LinkSingleElementWidget
|
||||
@@ -50,6 +56,22 @@ LinkSingleElementWidget::LinkSingleElementWidget(Element *elmt,
|
||||
m_show_qtwi = new QAction(tr("Montrer l'élément"), this);
|
||||
m_show_element = new QAction(tr("Montrer l'élément esclave"), this);
|
||||
m_save_header_state = new QAction(tr("Enregistrer la disposition"), this);
|
||||
|
||||
// Hide the full-masters checkbox for non-slave elements
|
||||
const bool is_slave = (elmt && elmt->elementData().m_type == ElementData::Slave);
|
||||
ui->m_hide_full_masters_cb->setVisible(is_slave);
|
||||
|
||||
// Restore persisted state before connecting to avoid toggled firing buildTree()
|
||||
// while m_element is still null.
|
||||
{
|
||||
const QSignalBlocker blocker(ui->m_hide_full_masters_cb);
|
||||
QSettings settings;
|
||||
ui->m_hide_full_masters_cb->setChecked(
|
||||
settings.value(QStringLiteral("link-element-widget/hideFullMasters"), false).toBool());
|
||||
}
|
||||
|
||||
connect(ui->m_hide_full_masters_cb, &QCheckBox::toggled,
|
||||
this, &LinkSingleElementWidget::hideFullMastersToggled);
|
||||
|
||||
connect(m_show_qtwi, &QAction::triggered, this, [=]()
|
||||
{
|
||||
@@ -175,6 +197,7 @@ void LinkSingleElementWidget::apply()
|
||||
m_unlink = false;
|
||||
m_element_to_link = nullptr;
|
||||
m_pending_qtwi = nullptr;
|
||||
m_pending_group_index = -1;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,8 +211,12 @@ QUndoCommand *LinkSingleElementWidget::associatedUndo() const
|
||||
|
||||
if (m_element_to_link || m_unlink)
|
||||
{
|
||||
if (m_element_to_link)
|
||||
if (m_element_to_link) {
|
||||
undo->setLink(m_element_to_link);
|
||||
if (m_pending_group_index >= 0) {
|
||||
undo->setGroupIndex(m_pending_group_index);
|
||||
}
|
||||
}
|
||||
else if (m_unlink)
|
||||
undo->unlinkAll();
|
||||
|
||||
@@ -242,6 +269,7 @@ void LinkSingleElementWidget::buildTree()
|
||||
|
||||
if (m_element->elementData().m_type == ElementData::Slave)
|
||||
{
|
||||
m_full_masters.clear();
|
||||
|
||||
for(const auto &elmt : elmt_vector)
|
||||
{
|
||||
@@ -281,6 +309,16 @@ void LinkSingleElementWidget::buildTree()
|
||||
QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_tree_widget, str_list);
|
||||
m_qtwi_elmt_hash.insert(qtwi, elmt);
|
||||
m_qtwi_strl_hash.insert(qtwi, search_list);
|
||||
|
||||
// Check if this master is full
|
||||
if (elmt->linkType() == Element::Master) {
|
||||
MasterElement *me = qobject_cast<MasterElement*>(elmt);
|
||||
if (me && me->isFull()) {
|
||||
m_full_masters.insert(qtwi);
|
||||
if (ui->m_hide_full_masters_cb->isChecked())
|
||||
qtwi->setHidden(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -345,8 +383,16 @@ void LinkSingleElementWidget::buildTree()
|
||||
|
||||
QSettings settings;
|
||||
QVariant v = settings.value(QStringLiteral("link-element-widget/report-state"));
|
||||
if(!v.isNull())
|
||||
ui->m_tree_widget->header()->restoreState(v.toByteArray());
|
||||
auto *header = ui->m_tree_widget->header();
|
||||
if (v.isNull() || !header->restoreState(v.toByteArray()))
|
||||
{
|
||||
// Keep logical column IDs stable for saved layouts, but show the
|
||||
// folio identity first even when the candidate has no conductor.
|
||||
for (int column = 5; column < 8; ++column)
|
||||
header->moveSection(header->visualIndex(column), column - 5);
|
||||
ui->m_tree_widget->resizeColumnToContents(5);
|
||||
ui->m_tree_widget->resizeColumnToContents(6);
|
||||
}
|
||||
}
|
||||
|
||||
setUpCompleter();
|
||||
@@ -386,20 +432,28 @@ QVector <QPointer<Element>> LinkSingleElementWidget::availableElements()
|
||||
|
||||
//If element is linked, remove is parent from the list
|
||||
if(!m_element->isFree()) elmt_vector.removeAll(m_element->linkedElements().first());
|
||||
// Filter out all master elements from the list
|
||||
|
||||
// Filter out incompatible elements: PLC and non-PLC must not mix
|
||||
const bool element_is_plc = (m_element->elementData().m_type == ElementData::Slave &&
|
||||
m_element->elementData().m_slave_type == ElementData::PLCSlave);
|
||||
for (int i = elmt_vector.size() - 1; i >= 0; --i) {
|
||||
Element *elmt = elmt_vector.at(i);
|
||||
|
||||
// If the item in the list is a master
|
||||
if (elmt->linkType() == Element::Master) {
|
||||
const bool master_is_plc = (elmt->elementData().m_master_type == ElementData::PLC);
|
||||
|
||||
// We convert the generic element pointer into a MasterElement pointer
|
||||
MasterElement *master = static_cast<MasterElement*>(elmt);
|
||||
|
||||
// If the master is full, we'll remove it from the list!
|
||||
if (master->isFull()) {
|
||||
// PLC slave can only link to PLC master, and vice versa
|
||||
if (element_is_plc != master_is_plc) {
|
||||
elmt_vector.removeAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// A master at its declared limit stays in the list. Removing
|
||||
// it made a full master indistinguishable from one that does
|
||||
// not exist: the candidate simply was not there, with nothing
|
||||
// to say why. The limit is advisory -- see the prompt in
|
||||
// MasterPropertiesWidget::on_link_button_clicked() -- so the
|
||||
// user decides, rather than the list deciding for them.
|
||||
}
|
||||
}
|
||||
return elmt_vector;
|
||||
@@ -432,6 +486,8 @@ void LinkSingleElementWidget::setUpCompleter()
|
||||
*/
|
||||
void LinkSingleElementWidget::clearTreeWidget()
|
||||
{
|
||||
m_pending_qtwi = nullptr;
|
||||
|
||||
while(ui->m_tree_widget->topLevelItemCount())
|
||||
{
|
||||
QTreeWidgetItem *qtwi = ui->m_tree_widget->takeTopLevelItem(0);
|
||||
@@ -444,6 +500,7 @@ void LinkSingleElementWidget::clearTreeWidget()
|
||||
|
||||
m_qtwi_elmt_hash.clear();
|
||||
m_qtwi_strl_hash.clear();
|
||||
m_full_masters.clear();
|
||||
}
|
||||
|
||||
void LinkSingleElementWidget::setUpHeaderLabels()
|
||||
@@ -513,7 +570,7 @@ void LinkSingleElementWidget::diagramWasRemovedFromProject()
|
||||
// contains the master element linked to the edited element
|
||||
// we must wait for this elements to be unlinked,
|
||||
// or else the list of available master isn't up to date
|
||||
QTimer::singleShot(10, this, SLOT(updateUi()));
|
||||
QTimer::singleShot(10, this, &LinkSingleElementWidget::updateUi);
|
||||
}
|
||||
|
||||
void LinkSingleElementWidget::showedElementWasDeleted()
|
||||
@@ -529,9 +586,86 @@ void LinkSingleElementWidget::linkTriggered()
|
||||
{
|
||||
if(!m_qtwi_at_context_menu)
|
||||
return;
|
||||
|
||||
|
||||
m_element_to_link = m_qtwi_elmt_hash.value(m_qtwi_at_context_menu);
|
||||
|
||||
m_pending_group_index = -1;
|
||||
|
||||
//If linking a slave to a master with contact groups, show group selection dialog
|
||||
if (m_element->linkType() == Element::Slave
|
||||
&& m_element_to_link
|
||||
&& m_element_to_link->linkType() == Element::Master)
|
||||
{
|
||||
// Check if this is a PLC master
|
||||
if (m_element_to_link->elementData().m_master_type == ElementData::PLC)
|
||||
{
|
||||
// Show PLC IO selection dialog
|
||||
const auto &plc_data = m_element_to_link->elementData().plcMasterData();
|
||||
if (!plc_data.ios.isEmpty())
|
||||
{
|
||||
// Collect already-used IO indices from the master
|
||||
QSet<int> used_indices;
|
||||
for (Element *linked : m_element_to_link->linkedElements()) {
|
||||
int idx = m_element_to_link->groupIndexForElement(linked);
|
||||
if (idx >= 0) {
|
||||
used_indices.insert(idx);
|
||||
}
|
||||
}
|
||||
|
||||
// Build selection dialog
|
||||
QStringList items;
|
||||
for (int i = 0; i < plc_data.ios.size(); ++i) {
|
||||
const auto &io = plc_data.ios.at(i);
|
||||
QString label = QString("[%1] %2 - %3")
|
||||
.arg(i + 1)
|
||||
.arg(io.address)
|
||||
.arg(io.functionText);
|
||||
if (used_indices.contains(i))
|
||||
label += tr(" (déjà utilisé)");
|
||||
items << label;
|
||||
}
|
||||
|
||||
bool ok = false;
|
||||
int selected = QInputDialog::getInt(
|
||||
this,
|
||||
tr("Sélectionner un IO PLC"),
|
||||
tr("IO disponible:"),
|
||||
0, 0, plc_data.ios.size() - 1, 1, &ok);
|
||||
|
||||
if (ok && selected >= 0) {
|
||||
m_pending_group_index = selected;
|
||||
} else {
|
||||
m_element_to_link = nullptr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Normal contact group selection for non-PLC masters
|
||||
const auto &groups = m_element_to_link->elementData().m_slave_contact_groups;
|
||||
if (!groups.isEmpty())
|
||||
{
|
||||
// Collect already-used group indices from the master
|
||||
QSet<int> used_indices;
|
||||
for (Element *linked : m_element_to_link->linkedElements()) {
|
||||
int idx = m_element_to_link->groupIndexForElement(linked);
|
||||
if (idx >= 0) {
|
||||
used_indices.insert(idx);
|
||||
}
|
||||
}
|
||||
|
||||
ContactGroupSelectionDialog dlg(groups, used_indices,
|
||||
m_element->elementData(), this);
|
||||
if (dlg.exec() == QDialog::Accepted && dlg.selectedIndex() >= 0) {
|
||||
m_pending_group_index = dlg.selectedIndex();
|
||||
} else {
|
||||
m_element_to_link = nullptr;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(m_live_edit)
|
||||
{
|
||||
apply();
|
||||
@@ -552,7 +686,7 @@ void LinkSingleElementWidget::linkTriggered()
|
||||
Qt::NoBrush));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (int i=0 ; i<6 ; i++)
|
||||
{
|
||||
m_qtwi_at_context_menu->setBackground(i,
|
||||
@@ -561,7 +695,7 @@ void LinkSingleElementWidget::linkTriggered()
|
||||
}
|
||||
m_pending_qtwi = m_qtwi_at_context_menu;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -575,6 +709,7 @@ void LinkSingleElementWidget::hideButtons()
|
||||
ui->m_show_linked_pb->hide();
|
||||
ui->m_show_this_pb->hide();
|
||||
ui->m_search_field->show();
|
||||
ui->m_hide_full_masters_cb->show();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -588,6 +723,7 @@ void LinkSingleElementWidget::showButtons()
|
||||
ui->m_show_linked_pb->show();
|
||||
ui->m_show_this_pb->show();
|
||||
ui->m_search_field->hide();
|
||||
ui->m_hide_full_masters_cb->hide();
|
||||
}
|
||||
|
||||
void LinkSingleElementWidget::headerCustomContextMenuRequested(
|
||||
@@ -625,8 +761,7 @@ void LinkSingleElementWidget::on_m_tree_widget_itemDoubleClicked(
|
||||
|
||||
if (m_showed_element)
|
||||
{
|
||||
disconnect(m_showed_element, SIGNAL(destroyed()),
|
||||
this, SLOT(showedElementWasDeleted()));
|
||||
disconnect(m_showed_element, &QObject::destroyed, this, &LinkSingleElementWidget::showedElementWasDeleted);
|
||||
m_showed_element->setHighlighted(false);
|
||||
}
|
||||
|
||||
@@ -634,8 +769,7 @@ void LinkSingleElementWidget::on_m_tree_widget_itemDoubleClicked(
|
||||
elmt->diagram()->showMe();
|
||||
elmt->setHighlighted(true);
|
||||
m_showed_element = elmt;
|
||||
connect(m_showed_element, SIGNAL(destroyed()),
|
||||
this, SLOT(showedElementWasDeleted()));
|
||||
connect(m_showed_element, &QObject::destroyed, this, &LinkSingleElementWidget::showedElementWasDeleted);
|
||||
|
||||
}
|
||||
|
||||
@@ -685,27 +819,35 @@ void LinkSingleElementWidget::on_m_show_this_pb_clicked()
|
||||
If arg1 is empty, show all items.
|
||||
@param arg1
|
||||
*/
|
||||
void LinkSingleElementWidget::on_m_search_field_textEdited(const QString &arg1)
|
||||
void LinkSingleElementWidget::on_m_search_field_textEdited(const QString &)
|
||||
{
|
||||
//Show all items if arg1 is empty, if not hide all items
|
||||
foreach(QTreeWidgetItem *qtwi, m_qtwi_elmt_hash.keys())
|
||||
qtwi->setHidden(!arg1.isEmpty());
|
||||
|
||||
QList <QTreeWidgetItem *> qtwi_list;
|
||||
|
||||
foreach(QTreeWidgetItem *qtwi, m_qtwi_strl_hash.keys())
|
||||
updateItemsVisibility();
|
||||
}
|
||||
|
||||
void LinkSingleElementWidget::hideFullMastersToggled(bool checked)
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue(QStringLiteral("link-element-widget/hideFullMasters"), checked);
|
||||
updateItemsVisibility();
|
||||
}
|
||||
|
||||
void LinkSingleElementWidget::updateItemsVisibility()
|
||||
{
|
||||
const QString text = ui->m_search_field->text();
|
||||
const bool hide_full = ui->m_hide_full_masters_cb->isChecked();
|
||||
|
||||
for (auto it = m_qtwi_strl_hash.cbegin(); it != m_qtwi_strl_hash.cend(); ++it)
|
||||
{
|
||||
foreach(QString str, m_qtwi_strl_hash.value(qtwi))
|
||||
{
|
||||
if(str.contains(arg1, Qt::CaseInsensitive))
|
||||
{
|
||||
qtwi_list << qtwi;
|
||||
continue;
|
||||
bool match = text.isEmpty();
|
||||
if (!match) {
|
||||
for (const QString &str : it.value()) {
|
||||
if (str.contains(text, Qt::CaseInsensitive)) {
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
it.key()->setHidden(!match
|
||||
|| (hide_full && m_full_masters.contains(it.key())));
|
||||
}
|
||||
|
||||
//Show items which match with arg1
|
||||
foreach(QTreeWidgetItem *qtwi, qtwi_list)
|
||||
qtwi->setHidden(false);
|
||||
}
|
||||
|
||||
@@ -22,8 +22,11 @@
|
||||
#include "abstractelementpropertieseditorwidget.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QDialog>
|
||||
#include <QComboBox>
|
||||
|
||||
class QTreeWidgetItem;
|
||||
class QCheckBox;
|
||||
class Element;
|
||||
class QMenu;
|
||||
|
||||
@@ -69,6 +72,7 @@ class LinkSingleElementWidget : public AbstractElementPropertiesEditorWidget
|
||||
void setUpCompleter();
|
||||
void clearTreeWidget();
|
||||
void setUpHeaderLabels();
|
||||
void updateItemsVisibility();
|
||||
|
||||
private slots:
|
||||
void diagramWasRemovedFromProject();
|
||||
@@ -85,6 +89,7 @@ class LinkSingleElementWidget : public AbstractElementPropertiesEditorWidget
|
||||
void on_m_show_this_pb_clicked();
|
||||
|
||||
void on_m_search_field_textEdited(const QString &arg1);
|
||||
void hideFullMastersToggled(bool checked);
|
||||
|
||||
private:
|
||||
Ui::LinkSingleElementWidget *ui;
|
||||
@@ -101,6 +106,10 @@ class LinkSingleElementWidget : public AbstractElementPropertiesEditorWidget
|
||||
Element *m_showed_element = nullptr,
|
||||
*m_element_to_link = nullptr;
|
||||
|
||||
QSet<QTreeWidgetItem*> m_full_masters;
|
||||
|
||||
int m_pending_group_index = -1;
|
||||
|
||||
QMenu *m_context_menu{nullptr};
|
||||
QAction *m_link_action{nullptr},
|
||||
*m_show_qtwi{nullptr},
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -16,16 +16,30 @@
|
||||
* along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "masterpropertieswidget.h"
|
||||
|
||||
#include "contactgroupselectiondialog.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../diagram.h"
|
||||
#include "../diagramposition.h"
|
||||
#include "../elementprovider.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "../undocommand/linkelementcommand.h"
|
||||
#include "ui_masterpropertieswidget.h"
|
||||
#include "../properties/elementdata.h"
|
||||
|
||||
#include <QListWidgetItem>
|
||||
#include <QMessageBox>
|
||||
#include <QTableWidget>
|
||||
#include <QSpinBox>
|
||||
#include <QCheckBox>
|
||||
#include <QComboBox>
|
||||
#include <QHeaderView>
|
||||
#include <QMenu>
|
||||
#include <QAction>
|
||||
#include <QClipboard>
|
||||
#include <QApplication>
|
||||
#include <QShortcut>
|
||||
#include <QPushButton>
|
||||
|
||||
|
||||
/**
|
||||
* @brief MasterPropertiesWidget::MasterPropertiesWidget
|
||||
@@ -140,14 +154,12 @@ void MasterPropertiesWidget::setElement(Element *element)
|
||||
m_element->setHighlighted(false);
|
||||
|
||||
if (m_project)
|
||||
disconnect(m_project, SIGNAL(diagramRemoved(QETProject*,Diagram*)),
|
||||
this, SLOT(diagramWasdeletedFromProject()));
|
||||
disconnect(m_project, &QETProject::diagramRemoved, this, &MasterPropertiesWidget::diagramWasdeletedFromProject);
|
||||
|
||||
if(Q_LIKELY(element->diagram() && element->diagram()->project()))
|
||||
if(Q_LIKELY(element->diagram() && element->diagram()->project()))
|
||||
{
|
||||
m_project = element->diagram()->project();
|
||||
connect(m_project, SIGNAL(diagramRemoved(QETProject*,Diagram*)),
|
||||
this, SLOT(diagramWasdeletedFromProject()));
|
||||
connect(m_project, &QETProject::diagramRemoved, this, &MasterPropertiesWidget::diagramWasdeletedFromProject);
|
||||
}
|
||||
else
|
||||
m_project = nullptr;
|
||||
@@ -157,7 +169,7 @@ void MasterPropertiesWidget::setElement(Element *element)
|
||||
disconnect(m_element.data(), &Element::linkedElementChanged,
|
||||
this, &MasterPropertiesWidget::updateUi);
|
||||
|
||||
m_element = element;
|
||||
m_element = element;
|
||||
connect(m_element.data(), &Element::linkedElementChanged,
|
||||
this, &MasterPropertiesWidget::updateUi);
|
||||
|
||||
@@ -176,6 +188,8 @@ void MasterPropertiesWidget::apply()
|
||||
{
|
||||
if (QUndoCommand *undo = associatedUndo())
|
||||
m_element -> diagram() -> undoStack().push(undo);
|
||||
|
||||
m_pending_group_indices.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -200,6 +214,16 @@ void MasterPropertiesWidget::reset()
|
||||
*/
|
||||
QUndoCommand* MasterPropertiesWidget::associatedUndo() const
|
||||
{
|
||||
// PLC masters manage their slave links via the IO table (setElementData),
|
||||
// not via the link tree widget. The link tree is always empty for PLC
|
||||
// masters, so we must not create an unlinkAll command.
|
||||
if (m_element &&
|
||||
m_element->elementData().m_type == ElementData::Master &&
|
||||
m_element->elementData().m_master_type == ElementData::PLC)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
QList <Element *> to_link;
|
||||
QList <Element *> linked_ = m_element->linkedElements();
|
||||
|
||||
@@ -224,8 +248,23 @@ QUndoCommand* MasterPropertiesWidget::associatedUndo() const
|
||||
if (to_link.isEmpty())
|
||||
undo->unlinkAll();
|
||||
else
|
||||
{
|
||||
undo->setLink(to_link);
|
||||
|
||||
//Pass group indices for newly linked slaves
|
||||
if (!m_pending_group_indices.isEmpty())
|
||||
{
|
||||
QMap<Element*, int> indices;
|
||||
for (Element *slave : to_link)
|
||||
{
|
||||
if (m_pending_group_indices.contains(slave))
|
||||
indices[slave] = m_pending_group_indices.value(slave);
|
||||
}
|
||||
if (!indices.isEmpty())
|
||||
undo->setGroupIndices(indices);
|
||||
}
|
||||
}
|
||||
|
||||
return undo;
|
||||
}
|
||||
|
||||
@@ -241,98 +280,7 @@ bool MasterPropertiesWidget::setLiveEdit(bool live_edit)
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief MasterPropertiesWidget::updateUi
|
||||
* Build the interface of the widget
|
||||
*/
|
||||
void MasterPropertiesWidget::updateUi()
|
||||
{
|
||||
ui->m_free_tree_widget->clear();
|
||||
ui->m_link_tree_widget->clear();
|
||||
m_qtwi_hash.clear();
|
||||
|
||||
if (Q_UNLIKELY(!m_project))
|
||||
return;
|
||||
|
||||
ElementProvider elmt_prov(m_project);
|
||||
QSettings settings;
|
||||
|
||||
//Build the list of free available element
|
||||
QList <QTreeWidgetItem *> items_list;
|
||||
for(const auto &elmt : elmt_prov.freeElement(ElementData::Slave))
|
||||
{
|
||||
QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_free_tree_widget);
|
||||
qtwi->setIcon(0, elmt->pixmap());
|
||||
|
||||
if(settings.value("genericpanel/folio", false).toBool())
|
||||
{
|
||||
autonum::sequentialNumbers seq;
|
||||
QString F =autonum::AssignVariables::formulaToLabel(
|
||||
elmt->diagram()->border_and_titleblock.folio(),
|
||||
seq,
|
||||
elmt->diagram(),
|
||||
elmt);
|
||||
qtwi->setText(1, F);
|
||||
}
|
||||
else
|
||||
{
|
||||
qtwi->setText(1, QString::number(
|
||||
elmt->diagram()->folioIndex()
|
||||
+ 1));
|
||||
}
|
||||
|
||||
|
||||
qtwi->setText(2, elmt->diagram()->title());
|
||||
qtwi->setText(4, elmt->diagram()->convertPosition(
|
||||
elmt->scenePos()).toString());
|
||||
items_list.append(qtwi);
|
||||
m_qtwi_hash.insert(qtwi, elmt);
|
||||
}
|
||||
|
||||
ui->m_free_tree_widget->addTopLevelItems(items_list);
|
||||
items_list.clear();
|
||||
|
||||
//Build the list of already linked element
|
||||
const QList<Element *> link_list = m_element->linkedElements();
|
||||
for(Element *elmt : link_list)
|
||||
{
|
||||
QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_link_tree_widget);
|
||||
qtwi->setIcon(0, elmt->pixmap());
|
||||
|
||||
if(settings.value("genericpanel/folio", false).toBool())
|
||||
{
|
||||
autonum::sequentialNumbers seq;
|
||||
QString F =autonum::AssignVariables::formulaToLabel(
|
||||
elmt->diagram()->border_and_titleblock.folio(),
|
||||
seq,
|
||||
elmt->diagram(),
|
||||
elmt);
|
||||
qtwi->setText(1, F);
|
||||
}
|
||||
else
|
||||
{
|
||||
qtwi->setText(1, QString::number(
|
||||
elmt->diagram()->folioIndex()
|
||||
+ 1));
|
||||
}
|
||||
|
||||
qtwi->setText(2, elmt->diagram()->title());
|
||||
qtwi->setText(3, elmt->diagram()->convertPosition(
|
||||
elmt->scenePos()).toString());
|
||||
items_list.append(qtwi);
|
||||
m_qtwi_hash.insert(qtwi, elmt);
|
||||
}
|
||||
|
||||
if(items_list.count())
|
||||
ui->m_link_tree_widget->addTopLevelItems(items_list);
|
||||
|
||||
QVariant v = settings.value("link-element-widget/master-state");
|
||||
if(!v.isNull())
|
||||
{
|
||||
ui->m_free_tree_widget->header()->restoreState(v.toByteArray());
|
||||
ui->m_link_tree_widget->header()->restoreState(v.toByteArray());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief MasterPropertiesWidget::headerCustomContextMenuRequested
|
||||
@@ -359,15 +307,24 @@ void MasterPropertiesWidget::on_link_button_clicked()
|
||||
int max_slaves = max_slaves_variant.toInt();
|
||||
int current_slaves = ui->m_link_tree_widget->topLevelItemCount();
|
||||
|
||||
// If a limit is set and reached
|
||||
// If a limit is set and reached, say so but let the user decide.
|
||||
// The limit records how many contacts the part is expected to
|
||||
// carry; it is not a rule the drawing has to obey, and refusing
|
||||
// the link obstructs drawing a schematic before the hardware has
|
||||
// been chosen.
|
||||
if (max_slaves != -1 && current_slaves >= max_slaves) {
|
||||
|
||||
|
||||
// Show a message box with the actual window as parent to ensure it's on top
|
||||
QMessageBox::warning(this->window(),
|
||||
tr("Nombre maximal d'esclaves atteint."),
|
||||
tr("Cet élément maître ne peut plus accepter aucun nouveau contact esclave, la limite fixée a été atteinte (Limite: %1).").arg(max_slaves));
|
||||
return;
|
||||
const auto answer = QMessageBox::warning(
|
||||
this->window(),
|
||||
tr("Nombre maximal d'esclaves atteint."),
|
||||
tr("La limite fixée pour cet élément maître est atteinte (Limite: %1).\n\n"
|
||||
"Voulez-vous tout de même lier ce contact esclave ?").arg(max_slaves),
|
||||
QMessageBox::Yes | QMessageBox::No,
|
||||
QMessageBox::Yes);
|
||||
if (answer != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,6 +332,38 @@ void MasterPropertiesWidget::on_link_button_clicked()
|
||||
QTreeWidgetItem *qtwi = ui->m_free_tree_widget->currentItem();
|
||||
if (qtwi)
|
||||
{
|
||||
Element *slave_elmt = m_qtwi_hash.value(qtwi);
|
||||
|
||||
//If master has contact groups, show group selection dialog
|
||||
const auto &groups = m_element->elementData().m_slave_contact_groups;
|
||||
if (!groups.isEmpty() && slave_elmt)
|
||||
{
|
||||
// Collect already-used group indices from the master
|
||||
QSet<int> used_indices;
|
||||
for (Element *linked : m_element->linkedElements()) {
|
||||
int idx = m_element->groupIndexForElement(linked);
|
||||
if (idx >= 0) {
|
||||
used_indices.insert(idx);
|
||||
}
|
||||
}
|
||||
|
||||
// Don't mark the current slave as used (it might be relinked)
|
||||
if (slave_elmt->linkedElements().contains(m_element)) {
|
||||
int current_idx = m_element->groupIndexForElement(slave_elmt);
|
||||
if (current_idx >= 0) {
|
||||
used_indices.remove(current_idx);
|
||||
}
|
||||
}
|
||||
|
||||
ContactGroupSelectionDialog dlg(groups, used_indices,
|
||||
slave_elmt->elementData(), this);
|
||||
if (dlg.exec() == QDialog::Accepted && dlg.selectedIndex() >= 0) {
|
||||
m_pending_group_indices[slave_elmt] = dlg.selectedIndex();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ui->m_free_tree_widget->takeTopLevelItem(
|
||||
ui->m_free_tree_widget->indexOfTopLevelItem(qtwi));
|
||||
ui->m_link_tree_widget->insertTopLevelItem(0, qtwi);
|
||||
@@ -415,8 +404,7 @@ void MasterPropertiesWidget::showElementFromTWI(QTreeWidgetItem *qtwi, int colum
|
||||
Q_UNUSED(column);
|
||||
if (m_showed_element)
|
||||
{
|
||||
disconnect(m_showed_element, SIGNAL(destroyed()),
|
||||
this, SLOT(showedElementWasDeleted()));
|
||||
disconnect(m_showed_element, &QObject::destroyed, this, &MasterPropertiesWidget::showedElementWasDeleted);
|
||||
m_showed_element -> setHighlighted(false);
|
||||
}
|
||||
if (m_element)
|
||||
@@ -425,8 +413,7 @@ void MasterPropertiesWidget::showElementFromTWI(QTreeWidgetItem *qtwi, int colum
|
||||
m_showed_element = m_qtwi_hash[qtwi];
|
||||
m_showed_element->diagram()->showMe();
|
||||
m_showed_element->setHighlighted(true);
|
||||
connect(m_showed_element, SIGNAL(destroyed()),
|
||||
this, SLOT(showedElementWasDeleted()));
|
||||
connect(m_showed_element, &QObject::destroyed, this, &MasterPropertiesWidget::showedElementWasDeleted);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -449,7 +436,7 @@ void MasterPropertiesWidget::diagramWasdeletedFromProject()
|
||||
// contains slave element linked to the edited element
|
||||
// we must wait for this elements be unlinked,
|
||||
// or else the linked list provides deleted elements.
|
||||
QTimer::singleShot(10, this, SLOT(updateUi()));
|
||||
QTimer::singleShot(10, this, &MasterPropertiesWidget::updateUi);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -497,3 +484,598 @@ void MasterPropertiesWidget::customContextMenu(const QPoint &pos, int i)
|
||||
m_context_menu->addAction(m_show_element);
|
||||
m_context_menu->popup(point);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief MasterPropertiesWidget::updateUi
|
||||
* Build the interface of the widget
|
||||
*/
|
||||
void MasterPropertiesWidget::updateUi()
|
||||
{
|
||||
ui->m_free_tree_widget->clear();
|
||||
ui->m_link_tree_widget->clear();
|
||||
m_qtwi_hash.clear();
|
||||
|
||||
// Check if this is a PLC master
|
||||
bool is_plc = m_element &&
|
||||
m_element->elementData().m_type == ElementData::Master &&
|
||||
m_element->elementData().m_master_type == ElementData::PLC;
|
||||
|
||||
// Show/hide normal master UI and PLC UI
|
||||
ui->m_free_tree_widget->setVisible(!is_plc);
|
||||
ui->m_link_tree_widget->setVisible(!is_plc);
|
||||
ui->label->setVisible(!is_plc);
|
||||
ui->label_2->setVisible(!is_plc);
|
||||
ui->link_button->setVisible(!is_plc);
|
||||
ui->unlink_button->setVisible(!is_plc);
|
||||
|
||||
// In PLC mode, make hidden widgets take no space in the grid layout
|
||||
if (is_plc) {
|
||||
for (QWidget *w : {static_cast<QWidget*>(ui->m_free_tree_widget),
|
||||
static_cast<QWidget*>(ui->m_link_tree_widget),
|
||||
static_cast<QWidget*>(ui->label),
|
||||
static_cast<QWidget*>(ui->label_2),
|
||||
static_cast<QWidget*>(ui->link_button),
|
||||
static_cast<QWidget*>(ui->unlink_button)}) {
|
||||
w->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
|
||||
w->setMinimumSize(0, 0);
|
||||
w->setMaximumSize(0, 0);
|
||||
}
|
||||
for (int col = 0; col < ui->gridLayout->columnCount(); ++col)
|
||||
ui->gridLayout->setColumnStretch(col, col < 2 ? 1 : 0);
|
||||
}
|
||||
|
||||
if (is_plc) {
|
||||
// Create PLC widget if not yet created
|
||||
if (!m_plc_widget) {
|
||||
m_plc_widget = new QWidget(ui->gridLayout->parentWidget());
|
||||
|
||||
auto *plc_layout = new QVBoxLayout(m_plc_widget);
|
||||
plc_layout->setContentsMargins(0, 0, 0, 0);
|
||||
|
||||
// Table
|
||||
m_plc_table = new QTableWidget(m_plc_widget);
|
||||
m_plc_table->setColumnCount(6);
|
||||
m_plc_table->setHorizontalHeaderLabels({
|
||||
tr("Type"), tr("Adresse"), tr("Fonction"),
|
||||
tr("Commentaire"), tr("Réf. croisée"), tr("Bornes")
|
||||
});
|
||||
m_plc_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Interactive);
|
||||
m_plc_table->horizontalHeader()->setSectionsMovable(true);
|
||||
m_plc_table->horizontalHeader()->resizeSection(0, 120);
|
||||
m_plc_table->horizontalHeader()->resizeSection(1, 100);
|
||||
m_plc_table->horizontalHeader()->resizeSection(2, 150);
|
||||
m_plc_table->horizontalHeader()->resizeSection(3, 150);
|
||||
m_plc_table->horizontalHeader()->resizeSection(4, 100);
|
||||
m_plc_table->horizontalHeader()->resizeSection(5, 120);
|
||||
|
||||
// Restore a previously saved column layout, if any
|
||||
auto *plc_hdr = m_plc_table->horizontalHeader();
|
||||
QSettings plc_settings;
|
||||
const QVariant plc_hdr_state = plc_settings.value(
|
||||
QStringLiteral("masterpropertieswidget/plc-table-header-state"));
|
||||
if (!plc_hdr_state.isNull())
|
||||
plc_hdr->restoreState(plc_hdr_state.toByteArray());
|
||||
|
||||
// Persist the column layout whenever the user resizes or moves a column
|
||||
auto save_plc_header_state = [plc_hdr]()
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue(QStringLiteral("masterpropertieswidget/plc-table-header-state"),
|
||||
plc_hdr->saveState());
|
||||
};
|
||||
connect(plc_hdr, &QHeaderView::sectionResized,
|
||||
this, save_plc_header_state);
|
||||
connect(plc_hdr, &QHeaderView::sectionMoved,
|
||||
this, save_plc_header_state);
|
||||
|
||||
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems);
|
||||
m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
m_plc_table->setMinimumHeight(200);
|
||||
|
||||
plc_layout->addWidget(m_plc_table);
|
||||
|
||||
connect(m_plc_table, &QTableWidget::cellChanged, this, &MasterPropertiesWidget::plcIOCellChanged);
|
||||
|
||||
// Context menu for the PLC table
|
||||
m_plc_table->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
connect(m_plc_table, &QTableWidget::customContextMenuRequested,
|
||||
this, &MasterPropertiesWidget::plcShowTableContextMenu);
|
||||
|
||||
// Ctrl+V shortcut for paste
|
||||
auto *paste_shortcut = new QShortcut(QKeySequence::Paste, m_plc_table);
|
||||
connect(paste_shortcut, &QShortcut::activated, this, &MasterPropertiesWidget::plcPasteFromClipboard);
|
||||
}
|
||||
|
||||
ui->gridLayout->addWidget(m_plc_widget, 0, 0, 1, 5);
|
||||
m_plc_widget->setVisible(true);
|
||||
|
||||
// Load PLC data into table (block signals to prevent cellChanged during population)
|
||||
m_plc_updating = true;
|
||||
m_plc_table->blockSignals(true);
|
||||
|
||||
// Clear existing rows
|
||||
m_plc_table->setRowCount(0);
|
||||
|
||||
ElementData::PlcMasterData plc_data = m_element->elementData().plcMasterData();
|
||||
|
||||
m_plc_table->setRowCount(plc_data.ios.size());
|
||||
for (int row = 0; row < plc_data.ios.size(); ++row) {
|
||||
const ElementData::PlcIO &io = plc_data.ios.at(row);
|
||||
|
||||
// Type combo (read-only — type is defined in the editor)
|
||||
auto *type_cb = new QComboBox(m_plc_table);
|
||||
type_cb->setEnabled(false);
|
||||
type_cb->setStyleSheet("QComboBox { background-color: #f0f0f0; }");
|
||||
QStringList plc_types = ElementData::plcIOTypeList();
|
||||
for (int t = 0; t < plc_types.size(); ++t) {
|
||||
type_cb->addItem(plc_types.at(t), t);
|
||||
}
|
||||
type_cb->setCurrentIndex(static_cast<int>(io.type));
|
||||
m_plc_table->setCellWidget(row, 0, type_cb);
|
||||
|
||||
// Address
|
||||
auto *addr_item = new QTableWidgetItem(io.address);
|
||||
m_plc_table->setItem(row, 1, addr_item);
|
||||
|
||||
// Function text
|
||||
auto *func_item = new QTableWidgetItem(io.functionText);
|
||||
m_plc_table->setItem(row, 2, func_item);
|
||||
|
||||
// Comment
|
||||
auto *comment_item = new QTableWidgetItem(io.comment);
|
||||
m_plc_table->setItem(row, 3, comment_item);
|
||||
|
||||
// CrossRef (read-only)
|
||||
auto *crossref_item = new QTableWidgetItem(io.crossRef);
|
||||
crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable);
|
||||
m_plc_table->setItem(row, 4, crossref_item);
|
||||
|
||||
// Anschlüsse (read-only)
|
||||
auto *terminals_item = new QTableWidgetItem(io.terminals.join(QStringLiteral(", ")));
|
||||
terminals_item->setFlags(terminals_item->flags() & ~Qt::ItemIsEditable);
|
||||
m_plc_table->setItem(row, 5, terminals_item);
|
||||
}
|
||||
|
||||
m_plc_table->blockSignals(false);
|
||||
m_plc_updating = false;
|
||||
} else {
|
||||
// Hide PLC widget if it was shown before
|
||||
if (m_plc_widget)
|
||||
m_plc_widget->setVisible(false);
|
||||
|
||||
// Restore normal widget size policies
|
||||
for (QWidget *w : {static_cast<QWidget*>(ui->m_free_tree_widget),
|
||||
static_cast<QWidget*>(ui->m_link_tree_widget),
|
||||
static_cast<QWidget*>(ui->label),
|
||||
static_cast<QWidget*>(ui->label_2),
|
||||
static_cast<QWidget*>(ui->link_button),
|
||||
static_cast<QWidget*>(ui->unlink_button)}) {
|
||||
w->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
|
||||
w->setMinimumSize(0, 0);
|
||||
w->setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
|
||||
}
|
||||
for (int col = 0; col < ui->gridLayout->columnCount(); ++col)
|
||||
ui->gridLayout->setColumnStretch(col, 0);
|
||||
|
||||
if (Q_UNLIKELY(!m_project))
|
||||
return;
|
||||
|
||||
ElementProvider elmt_prov(m_project);
|
||||
QSettings settings;
|
||||
|
||||
//Build the list of free available element
|
||||
QList <QTreeWidgetItem *> items_list;
|
||||
for(const auto &elmt : elmt_prov.freeElement(ElementData::Slave))
|
||||
{
|
||||
// Filter out PLC-Slave elements — they should only link to PLC-Master
|
||||
if (elmt->elementData().m_slave_type == ElementData::PLCSlave)
|
||||
continue;
|
||||
|
||||
QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_free_tree_widget);
|
||||
qtwi->setIcon(0, elmt->pixmap());
|
||||
|
||||
if(settings.value("genericpanel/folio", false).toBool())
|
||||
{
|
||||
autonum::sequentialNumbers seq;
|
||||
QString F =autonum::AssignVariables::formulaToLabel(
|
||||
elmt->diagram()->border_and_titleblock.folio(),
|
||||
seq,
|
||||
elmt->diagram(),
|
||||
elmt);
|
||||
qtwi->setText(1, F);
|
||||
}
|
||||
else
|
||||
{
|
||||
qtwi->setText(1, QString::number(
|
||||
elmt->diagram()->folioIndex()
|
||||
+ 1));
|
||||
}
|
||||
|
||||
|
||||
qtwi->setText(2, elmt->diagram()->title());
|
||||
qtwi->setText(4, elmt->diagram()->convertPosition(
|
||||
elmt->scenePos()).toString());
|
||||
items_list.append(qtwi);
|
||||
m_qtwi_hash.insert(qtwi, elmt);
|
||||
}
|
||||
|
||||
ui->m_free_tree_widget->addTopLevelItems(items_list);
|
||||
items_list.clear();
|
||||
|
||||
//Build the list of already linked element
|
||||
const QList<Element *> link_list = m_element->linkedElements();
|
||||
for(Element *elmt : link_list)
|
||||
{
|
||||
QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_link_tree_widget);
|
||||
qtwi->setIcon(0, elmt->pixmap());
|
||||
|
||||
if(settings.value("genericpanel/folio", false).toBool())
|
||||
{
|
||||
autonum::sequentialNumbers seq;
|
||||
QString F =autonum::AssignVariables::formulaToLabel(
|
||||
elmt->diagram()->border_and_titleblock.folio(),
|
||||
seq,
|
||||
elmt->diagram(),
|
||||
elmt);
|
||||
qtwi->setText(1, F);
|
||||
}
|
||||
else
|
||||
{
|
||||
qtwi->setText(1, QString::number(
|
||||
elmt->diagram()->folioIndex()
|
||||
+ 1));
|
||||
}
|
||||
|
||||
qtwi->setText(2, elmt->diagram()->title());
|
||||
qtwi->setText(3, elmt->diagram()->convertPosition(
|
||||
elmt->scenePos()).toString());
|
||||
items_list.append(qtwi);
|
||||
m_qtwi_hash.insert(qtwi, elmt);
|
||||
}
|
||||
|
||||
if(items_list.count())
|
||||
ui->m_link_tree_widget->addTopLevelItems(items_list);
|
||||
|
||||
QVariant v = settings.value("link-element-widget/master-state");
|
||||
if(!v.isNull())
|
||||
{
|
||||
ui->m_free_tree_widget->header()->restoreState(v.toByteArray());
|
||||
ui->m_link_tree_widget->header()->restoreState(v.toByteArray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PLC IO Table methods
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* @brief MasterPropertiesWidget::plcPasteFromClipboard
|
||||
* Paste IO data from clipboard.
|
||||
*
|
||||
* Two modes:
|
||||
* 1. Single-column data (no tabs, e.g. copied vertically from A1:A6):
|
||||
* Values are pasted vertically down the current column.
|
||||
* 2. Multi-column data (tab-separated, e.g. copied from a row in Excel):
|
||||
* Each line becomes a separate IO row.
|
||||
*/
|
||||
void MasterPropertiesWidget::plcPasteFromClipboard()
|
||||
{
|
||||
if (!m_plc_table || !m_element)
|
||||
return;
|
||||
|
||||
QString clipboard_text = QApplication::clipboard()->text();
|
||||
if (clipboard_text.isEmpty())
|
||||
return;
|
||||
|
||||
QStringList lines = clipboard_text.split('\n', Qt::SkipEmptyParts);
|
||||
if (lines.isEmpty())
|
||||
return;
|
||||
|
||||
bool has_tabs = false;
|
||||
for (const QString &line : lines) {
|
||||
if (line.contains('\t')) {
|
||||
has_tabs = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
m_plc_updating = true;
|
||||
|
||||
if (!has_tabs) {
|
||||
// Vertical paste: values go down the same column, within existing rows only
|
||||
int target_col = m_plc_table->currentColumn();
|
||||
if (target_col < 0) target_col = 0;
|
||||
int target_row = m_plc_table->currentRow();
|
||||
if (target_row < 0) target_row = 0;
|
||||
int max_rows = m_plc_table->rowCount();
|
||||
|
||||
for (int i = 0; i < lines.size(); ++i) {
|
||||
int row = target_row + i;
|
||||
if (row >= max_rows) break;
|
||||
setCellFromValue(row, target_col, lines.at(i).trimmed());
|
||||
}
|
||||
} else {
|
||||
// Horizontal paste: each line is a separate IO row, within existing rows only
|
||||
int target_row = m_plc_table->currentRow();
|
||||
if (target_row < 0) target_row = 0;
|
||||
int max_rows = m_plc_table->rowCount();
|
||||
|
||||
for (int i = 0; i < lines.size(); ++i) {
|
||||
int row = target_row + i;
|
||||
if (row >= max_rows) break;
|
||||
QStringList cells = lines.at(i).split('\t');
|
||||
for (int c = 0; c < cells.size(); ++c) {
|
||||
if (c > 6) break;
|
||||
setCellFromValue(row, c, cells.at(c).trimmed());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m_plc_updating = false;
|
||||
plcUpdateDisplaySettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief MasterPropertiesWidget::setCellFromValue
|
||||
* Set a single table cell value, respecting the column widget type.
|
||||
*/
|
||||
void MasterPropertiesWidget::setCellFromValue(int row, int col, const QString &val)
|
||||
{
|
||||
if (!m_plc_table || row < 0 || col < 0 || col > 6)
|
||||
return;
|
||||
|
||||
if (col == 0) {
|
||||
// Type combo
|
||||
auto *type_cb = qobject_cast<QComboBox*>(m_plc_table->cellWidget(row, col));
|
||||
if (!type_cb) {
|
||||
type_cb = new QComboBox(m_plc_table);
|
||||
QStringList plc_types = ElementData::plcIOTypeList();
|
||||
for (int t = 0; t < plc_types.size(); ++t)
|
||||
type_cb->addItem(plc_types.at(t), t);
|
||||
m_plc_table->setCellWidget(row, col, type_cb);
|
||||
connect(type_cb, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, [this, row](int) { plcIOCellChanged(row, 0); });
|
||||
}
|
||||
if (!val.isEmpty()) {
|
||||
QStringList plc_types = ElementData::plcIOTypeList();
|
||||
for (int t = 0; t < plc_types.size(); ++t) {
|
||||
if (plc_types.at(t).compare(val, Qt::CaseInsensitive) == 0) {
|
||||
type_cb->setCurrentIndex(t);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (col == 5) {
|
||||
// Anschlüsse (read-only)
|
||||
auto *item = new QTableWidgetItem(val);
|
||||
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
|
||||
m_plc_table->setItem(row, col, item);
|
||||
}
|
||||
else {
|
||||
// Text columns: Address, Function, Comment
|
||||
m_plc_table->setItem(row, col, new QTableWidgetItem(val));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief MasterPropertiesWidget::plcAddRow
|
||||
* Add a new empty row to the PLC IO table
|
||||
*/
|
||||
void MasterPropertiesWidget::plcAddRow()
|
||||
{
|
||||
if (!m_plc_table)
|
||||
return;
|
||||
|
||||
int row = m_plc_table->rowCount();
|
||||
m_plc_table->insertRow(row);
|
||||
|
||||
// Type combo
|
||||
auto *type_cb = new QComboBox(m_plc_table);
|
||||
QStringList plc_types = ElementData::plcIOTypeList();
|
||||
for (int t = 0; t < plc_types.size(); ++t) {
|
||||
type_cb->addItem(plc_types.at(t), t);
|
||||
}
|
||||
m_plc_table->setCellWidget(row, 0, type_cb);
|
||||
connect(type_cb, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, [this, row](int) { plcIOCellChanged(row, 0); });
|
||||
|
||||
m_plc_table->setItem(row, 1, new QTableWidgetItem());
|
||||
m_plc_table->setItem(row, 2, new QTableWidgetItem());
|
||||
m_plc_table->setItem(row, 3, new QTableWidgetItem());
|
||||
m_plc_table->setItem(row, 4, new QTableWidgetItem());
|
||||
|
||||
// Anschlüsse (read-only)
|
||||
auto *terminals_item = new QTableWidgetItem();
|
||||
terminals_item->setFlags(terminals_item->flags() & ~Qt::ItemIsEditable);
|
||||
m_plc_table->setItem(row, 5, terminals_item);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief MasterPropertiesWidget::plcRemoveRow
|
||||
* Remove selected rows from the PLC IO table
|
||||
*/
|
||||
void MasterPropertiesWidget::plcRemoveRow()
|
||||
{
|
||||
if (!m_plc_table)
|
||||
return;
|
||||
|
||||
QModelIndexList selected = m_plc_table->selectionModel()->selectedRows();
|
||||
if (selected.isEmpty())
|
||||
return;
|
||||
|
||||
m_plc_updating = true;
|
||||
|
||||
// Remove from bottom to top to preserve indices
|
||||
std::sort(selected.begin(), selected.end(),
|
||||
[](const QModelIndex &a, const QModelIndex &b) { return a.row() > b.row(); });
|
||||
|
||||
for (const QModelIndex &idx : selected) {
|
||||
m_plc_table->removeRow(idx.row());
|
||||
}
|
||||
|
||||
m_plc_updating = false;
|
||||
plcUpdateDisplaySettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief MasterPropertiesWidget::plcMoveRowUp
|
||||
* Move selected row up
|
||||
*/
|
||||
void MasterPropertiesWidget::plcMoveRowUp()
|
||||
{
|
||||
if (!m_plc_table)
|
||||
return;
|
||||
|
||||
int row = m_plc_table->currentRow();
|
||||
if (row <= 0)
|
||||
return;
|
||||
|
||||
m_plc_updating = true;
|
||||
|
||||
// Swap with row above
|
||||
for (int col = 0; col < m_plc_table->columnCount(); ++col) {
|
||||
QWidget *w1 = m_plc_table->cellWidget(row, col);
|
||||
QWidget *w2 = m_plc_table->cellWidget(row - 1, col);
|
||||
m_plc_table->setCellWidget(row, col, w2);
|
||||
m_plc_table->setCellWidget(row - 1, col, w1);
|
||||
|
||||
QTableWidgetItem *i1 = m_plc_table->item(row, col);
|
||||
QTableWidgetItem *i2 = m_plc_table->item(row - 1, col);
|
||||
m_plc_table->setItem(row, col, i2);
|
||||
m_plc_table->setItem(row - 1, col, i1);
|
||||
}
|
||||
|
||||
m_plc_table->setCurrentCell(row - 1, m_plc_table->currentColumn());
|
||||
m_plc_updating = false;
|
||||
plcUpdateDisplaySettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief MasterPropertiesWidget::plcMoveRowDown
|
||||
* Move selected row down
|
||||
*/
|
||||
void MasterPropertiesWidget::plcMoveRowDown()
|
||||
{
|
||||
if (!m_plc_table)
|
||||
return;
|
||||
|
||||
int row = m_plc_table->currentRow();
|
||||
if (row < 0 || row >= m_plc_table->rowCount() - 1)
|
||||
return;
|
||||
|
||||
m_plc_updating = true;
|
||||
|
||||
// Swap with row below
|
||||
for (int col = 0; col < m_plc_table->columnCount(); ++col) {
|
||||
QWidget *w1 = m_plc_table->cellWidget(row, col);
|
||||
QWidget *w2 = m_plc_table->cellWidget(row + 1, col);
|
||||
m_plc_table->setCellWidget(row, col, w2);
|
||||
m_plc_table->setCellWidget(row + 1, col, w1);
|
||||
|
||||
QTableWidgetItem *i1 = m_plc_table->item(row, col);
|
||||
QTableWidgetItem *i2 = m_plc_table->item(row + 1, col);
|
||||
m_plc_table->setItem(row, col, i2);
|
||||
m_plc_table->setItem(row + 1, col, i1);
|
||||
}
|
||||
|
||||
m_plc_table->setCurrentCell(row + 1, m_plc_table->currentColumn());
|
||||
m_plc_updating = false;
|
||||
plcUpdateDisplaySettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief MasterPropertiesWidget::plcIOCellChanged
|
||||
* Called when a cell in the PLC IO table changes
|
||||
*/
|
||||
void MasterPropertiesWidget::plcIOCellChanged(int row, int column)
|
||||
{
|
||||
Q_UNUSED(row)
|
||||
Q_UNUSED(column)
|
||||
if (m_plc_updating || !m_element || !m_plc_table)
|
||||
return;
|
||||
|
||||
// Save immediately
|
||||
plcUpdateDisplaySettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief MasterPropertiesWidget::plcUpdateDisplaySettings
|
||||
* Update the IO data from the table (display settings are managed by the editor only)
|
||||
*/
|
||||
void MasterPropertiesWidget::plcUpdateDisplaySettings()
|
||||
{
|
||||
if (m_plc_updating || !m_element || !m_plc_table)
|
||||
return;
|
||||
|
||||
m_plc_updating = true;
|
||||
|
||||
// Preserve existing display settings
|
||||
ElementData ed = m_element->elementData();
|
||||
const auto orig_plc = ed.plcMasterData();
|
||||
ElementData::PlcMasterData plc_data = orig_plc;
|
||||
plc_data.ios.clear();
|
||||
|
||||
// Read IOs from table, preserving terminal data from original IOs by row index
|
||||
for (int row = 0; row < m_plc_table->rowCount(); ++row) {
|
||||
ElementData::PlcIO io;
|
||||
|
||||
auto *type_cb = qobject_cast<QComboBox *>(m_plc_table->cellWidget(row, 0));
|
||||
if (type_cb)
|
||||
io.type = static_cast<ElementData::PlcIOType>(type_cb->currentData().toInt());
|
||||
|
||||
auto *addr_item = m_plc_table->item(row, 1);
|
||||
if (addr_item)
|
||||
io.address = addr_item->text();
|
||||
|
||||
auto *func_item = m_plc_table->item(row, 2);
|
||||
if (func_item)
|
||||
io.functionText = func_item->text();
|
||||
|
||||
auto *comment_item = m_plc_table->item(row, 3);
|
||||
if (comment_item)
|
||||
io.comment = comment_item->text();
|
||||
|
||||
auto *crossref_item = m_plc_table->item(row, 4);
|
||||
if (crossref_item)
|
||||
io.crossRef = crossref_item->text();
|
||||
|
||||
// Preserve terminal data by matching row index directly.
|
||||
// This avoids address-based lookup which fails when all addresses
|
||||
// are empty (common in PLC masters) — a hash collision would cause
|
||||
// only the last IO's terminals to be used for all rows.
|
||||
if (row < orig_plc.ios.size()) {
|
||||
const auto &orig_io = orig_plc.ios.at(row);
|
||||
io.terminalCount = orig_io.terminalCount;
|
||||
io.terminals = orig_io.terminals;
|
||||
}
|
||||
|
||||
plc_data.ios.append(io);
|
||||
}
|
||||
|
||||
ed.setPlcMasterData(plc_data);
|
||||
m_element->setElementData(ed);
|
||||
|
||||
// Trigger update of the cross ref item
|
||||
if (m_element->scene())
|
||||
m_element->update();
|
||||
|
||||
m_plc_updating = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief MasterPropertiesWidget::plcShowTableContextMenu
|
||||
* Show context menu for the PLC IO table
|
||||
*/
|
||||
void MasterPropertiesWidget::plcShowTableContextMenu(const QPoint &pos)
|
||||
{
|
||||
Q_UNUSED(pos)
|
||||
if (!m_plc_table)
|
||||
return;
|
||||
|
||||
QMenu menu;
|
||||
menu.addAction(tr("Coller depuis le presse-papiers"), this, &MasterPropertiesWidget::plcPasteFromClipboard);
|
||||
|
||||
menu.exec(m_plc_table->mapToGlobal(pos));
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#include <QWidget>
|
||||
#include <QHash>
|
||||
#include <QMap>
|
||||
|
||||
#include "abstractelementpropertieseditorwidget.h"
|
||||
|
||||
@@ -30,6 +31,10 @@ class Diagram;
|
||||
class QTreeWidgetItem;
|
||||
class QMenu;
|
||||
class QAction;
|
||||
class QTableWidget;
|
||||
class QSpinBox;
|
||||
class QCheckBox;
|
||||
class QComboBox;
|
||||
|
||||
namespace Ui {
|
||||
class MasterPropertiesWidget;
|
||||
@@ -74,9 +79,21 @@ class MasterPropertiesWidget : public AbstractElementPropertiesEditorWidget
|
||||
void diagramWasdeletedFromProject();
|
||||
void customContextMenu(const QPoint &pos, int i=0);
|
||||
|
||||
// PLC IO table slots
|
||||
void plcPasteFromClipboard();
|
||||
void setCellFromValue(int row, int col, const QString &val);
|
||||
void plcAddRow();
|
||||
void plcRemoveRow();
|
||||
void plcMoveRowUp();
|
||||
void plcMoveRowDown();
|
||||
void plcIOCellChanged(int row, int column);
|
||||
void plcUpdateDisplaySettings();
|
||||
void plcShowTableContextMenu(const QPoint &pos);
|
||||
|
||||
private:
|
||||
Ui::MasterPropertiesWidget *ui;
|
||||
QHash <QTreeWidgetItem *, Element *> m_qtwi_hash;
|
||||
QMap <Element *, int> m_pending_group_indices;
|
||||
QTreeWidgetItem *m_qtwi_at_context_menu = nullptr;
|
||||
QPointer <Element> m_showed_element;
|
||||
QETProject *m_project;
|
||||
@@ -86,6 +103,16 @@ class MasterPropertiesWidget : public AbstractElementPropertiesEditorWidget
|
||||
*m_show_qtwi,
|
||||
*m_show_element,
|
||||
*m_save_header_state;
|
||||
|
||||
// PLC-specific members
|
||||
QWidget *m_plc_widget = nullptr;
|
||||
QTableWidget *m_plc_table = nullptr;
|
||||
QCheckBox *m_plc_break_checkboxes[4] = {nullptr, nullptr, nullptr, nullptr};
|
||||
QSpinBox *m_plc_break_spinboxes[4] = {nullptr, nullptr, nullptr, nullptr};
|
||||
QSpinBox *m_plc_row_height_spinbox = nullptr;
|
||||
QList<QCheckBox *> m_plc_col_visibility_checkboxes;
|
||||
QList<QSpinBox *> m_plc_col_width_spinboxes;
|
||||
bool m_plc_updating = false; // Guard against recursive updates
|
||||
};
|
||||
|
||||
#endif // MASTERPROPERTIESWIDGET_H
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
|
||||
#include "multipastedialog.h"
|
||||
|
||||
#include "../qetproject.h"
|
||||
#include "../conductorautonumerotation.h"
|
||||
#include "../diagram.h"
|
||||
#include "../diagramcommands.h"
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "kautosavefile.h"
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QLockFile>
|
||||
#include <QRandomGenerator>
|
||||
#include <QStandardPaths>
|
||||
#include <QTextStream>
|
||||
|
||||
namespace {
|
||||
|
||||
const auto autosaveSuffix = QStringLiteral(".qetautosave");
|
||||
|
||||
QString staleFilesDir()
|
||||
{
|
||||
auto data_dir = QStandardPaths::writableLocation(
|
||||
QStandardPaths::AppDataLocation);
|
||||
while (data_dir.endsWith(QLatin1Char('/'))) {
|
||||
data_dir.chop(1);
|
||||
}
|
||||
if (data_dir.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return data_dir + QDir::separator() + QStringLiteral("autosave");
|
||||
}
|
||||
|
||||
QString metadataFileName(const QString &autosave_file_name)
|
||||
{
|
||||
return autosave_file_name + QStringLiteral(".path");
|
||||
}
|
||||
|
||||
QString lockFileName(const QString &autosave_file_name)
|
||||
{
|
||||
return autosave_file_name + QStringLiteral(".lock");
|
||||
}
|
||||
|
||||
QUrl normalizeManagedFile(const QUrl &url)
|
||||
{
|
||||
if (url.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (url.isLocalFile() || url.scheme().isEmpty()) {
|
||||
const auto path = url.isLocalFile() ? url.toLocalFile() : url.path();
|
||||
QUrl normalized;
|
||||
normalized.setPath(QDir::cleanPath(QFileInfo(path).absoluteFilePath()));
|
||||
return normalized;
|
||||
}
|
||||
|
||||
return url;
|
||||
}
|
||||
|
||||
QString storedManagedFile(const QUrl &url)
|
||||
{
|
||||
if (url.isLocalFile() || url.scheme().isEmpty()) {
|
||||
return url.path();
|
||||
}
|
||||
|
||||
return url.toString(QUrl::FullyEncoded);
|
||||
}
|
||||
|
||||
QUrl managedFileFromStorage(const QString &stored_path)
|
||||
{
|
||||
if (!stored_path.startsWith(QLatin1Char('/'))) {
|
||||
return QUrl(stored_path);
|
||||
}
|
||||
|
||||
QUrl url;
|
||||
url.setPath(QDir::cleanPath(stored_path));
|
||||
return url;
|
||||
}
|
||||
|
||||
bool writeManagedFileMetadata(const QString &autosave_file_name, const QUrl &managed_file)
|
||||
{
|
||||
QFile metadata_file(metadataFileName(autosave_file_name));
|
||||
if (!metadata_file.open(QIODevice::WriteOnly
|
||||
| QIODevice::Truncate
|
||||
| QIODevice::Text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QTextStream stream(&metadata_file);
|
||||
stream << storedManagedFile(managed_file) << '\n';
|
||||
stream.flush();
|
||||
return metadata_file.error() == QFile::NoError;
|
||||
}
|
||||
|
||||
QUrl readManagedFileMetadata(const QString &autosave_file_name)
|
||||
{
|
||||
QFile metadata_file(metadataFileName(autosave_file_name));
|
||||
if (!metadata_file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QTextStream stream(&metadata_file);
|
||||
const auto stored_path = stream.readLine().trimmed();
|
||||
if (stored_path.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return normalizeManagedFile(managedFileFromStorage(stored_path));
|
||||
}
|
||||
|
||||
QString autosaveFileName(const QUrl &managed_file)
|
||||
{
|
||||
const auto stored_path = storedManagedFile(managed_file);
|
||||
const auto digest = QCryptographicHash::hash(
|
||||
stored_path.toUtf8(),
|
||||
QCryptographicHash::Sha256).toHex().left(16);
|
||||
|
||||
auto basename = QFileInfo(managed_file.path()).fileName();
|
||||
if (basename.isEmpty()) {
|
||||
basename = QStringLiteral("autosave");
|
||||
}
|
||||
|
||||
auto encoded_basename = QString::fromLatin1(
|
||||
QUrl::toPercentEncoding(basename));
|
||||
if (encoded_basename.size() > 80) {
|
||||
encoded_basename.truncate(80);
|
||||
}
|
||||
|
||||
const auto random = QString::number(
|
||||
QRandomGenerator::global()->generate64(), 16);
|
||||
|
||||
return QStringLiteral("%1_%2_%3%4").arg(
|
||||
encoded_basename,
|
||||
QString::fromLatin1(digest),
|
||||
random,
|
||||
autosaveSuffix);
|
||||
}
|
||||
|
||||
QStringList findAllStaleFiles(const QString &application_name)
|
||||
{
|
||||
Q_UNUSED(application_name)
|
||||
|
||||
const auto dir_path = staleFilesDir();
|
||||
if (dir_path.isEmpty()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
QDir dir(dir_path);
|
||||
const auto entries = dir.entryList(
|
||||
{QStringLiteral("*") + autosaveSuffix},
|
||||
QDir::Files);
|
||||
|
||||
QStringList files;
|
||||
for (const auto &entry : entries) {
|
||||
files << dir.absoluteFilePath(entry);
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
bool autosaveFileIsRecoverable(const QString &autosave_file_name)
|
||||
{
|
||||
QLockFile lock(lockFileName(autosave_file_name));
|
||||
lock.setStaleLockTime(60 * 1000);
|
||||
if (!lock.tryLock()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
lock.unlock();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
KAutoSaveFile::KAutoSaveFile(const QUrl &filename, QObject *parent) :
|
||||
QFile{parent}
|
||||
{
|
||||
setManagedFile(filename);
|
||||
}
|
||||
|
||||
KAutoSaveFile::KAutoSaveFile(QObject *parent) :
|
||||
QFile{parent}
|
||||
{
|
||||
}
|
||||
|
||||
KAutoSaveFile::~KAutoSaveFile()
|
||||
{
|
||||
releaseLock();
|
||||
}
|
||||
|
||||
QUrl KAutoSaveFile::managedFile() const
|
||||
{
|
||||
return m_managed_file;
|
||||
}
|
||||
|
||||
void KAutoSaveFile::setManagedFile(const QUrl &filename)
|
||||
{
|
||||
releaseLock();
|
||||
|
||||
m_managed_file = normalizeManagedFile(filename);
|
||||
m_managed_file_name_changed = true;
|
||||
setFileName({});
|
||||
}
|
||||
|
||||
void KAutoSaveFile::releaseLock()
|
||||
{
|
||||
if (m_lock && m_lock->isLocked()) {
|
||||
const auto autosave_file_name = fileName();
|
||||
m_lock.reset();
|
||||
|
||||
if (!autosave_file_name.isEmpty()) {
|
||||
QFile::remove(metadataFileName(autosave_file_name));
|
||||
remove();
|
||||
}
|
||||
} else {
|
||||
m_lock.reset();
|
||||
}
|
||||
}
|
||||
|
||||
bool KAutoSaveFile::open(OpenMode openmode)
|
||||
{
|
||||
if (m_managed_file.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (m_managed_file_name_changed) {
|
||||
const auto stale_dir = staleFilesDir();
|
||||
if (stale_dir.isEmpty() || !QDir().mkpath(stale_dir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setFileName(QDir(stale_dir).absoluteFilePath(
|
||||
autosaveFileName(m_managed_file)));
|
||||
if (!writeManagedFileMetadata(fileName(), m_managed_file)) {
|
||||
setFileName({});
|
||||
return false;
|
||||
}
|
||||
|
||||
m_managed_file_name_changed = false;
|
||||
}
|
||||
|
||||
if (!QFile::open(openmode)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!m_lock) {
|
||||
m_lock = std::make_unique<QLockFile>(lockFileName(fileName()));
|
||||
m_lock->setStaleLockTime(60 * 1000);
|
||||
}
|
||||
|
||||
if (m_lock->isLocked() || m_lock->tryLock()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
|
||||
QList<KAutoSaveFile *> KAutoSaveFile::staleFiles(
|
||||
const QUrl &url,
|
||||
const QString &applicationName)
|
||||
{
|
||||
const auto managed_file_filter = normalizeManagedFile(url);
|
||||
QList<KAutoSaveFile *> stale_files;
|
||||
|
||||
for (const auto &file : findAllStaleFiles(applicationName)) {
|
||||
const auto managed_file = readManagedFileMetadata(file);
|
||||
if (managed_file.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (!managed_file_filter.isEmpty()
|
||||
&& managed_file != managed_file_filter) {
|
||||
continue;
|
||||
}
|
||||
if (!autosaveFileIsRecoverable(file)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto *stale_file = new KAutoSaveFile(managed_file);
|
||||
stale_file->setFileName(file);
|
||||
stale_file->m_managed_file_name_changed = false;
|
||||
stale_files << stale_file;
|
||||
}
|
||||
|
||||
return stale_files;
|
||||
}
|
||||
|
||||
QList<KAutoSaveFile *> KAutoSaveFile::allStaleFiles(const QString &applicationName)
|
||||
{
|
||||
return staleFiles({}, applicationName);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef QET_KAUTOSAVEFILE_H
|
||||
#define QET_KAUTOSAVEFILE_H
|
||||
|
||||
#include <QFile>
|
||||
#include <QList>
|
||||
#include <QUrl>
|
||||
|
||||
#include <memory>
|
||||
|
||||
class QLockFile;
|
||||
|
||||
/**
|
||||
Small Qt-only replacement for the KAutoSaveFile API used by QET.
|
||||
|
||||
It stores crash-recovery files below the application data autosave folder
|
||||
and protects each recovery file with QLockFile. The original managed file
|
||||
path is kept in a sidecar file so allStaleFiles() can present recoverable
|
||||
projects on the next startup.
|
||||
*/
|
||||
class KAutoSaveFile : public QFile
|
||||
{
|
||||
public:
|
||||
explicit KAutoSaveFile(const QUrl &filename, QObject *parent = nullptr);
|
||||
explicit KAutoSaveFile(QObject *parent = nullptr);
|
||||
~KAutoSaveFile() override;
|
||||
|
||||
QUrl managedFile() const;
|
||||
void setManagedFile(const QUrl &filename);
|
||||
virtual void releaseLock();
|
||||
bool open(OpenMode openmode) override;
|
||||
|
||||
static QList<KAutoSaveFile *> staleFiles(
|
||||
const QUrl &url,
|
||||
const QString &applicationName = QString());
|
||||
static QList<KAutoSaveFile *> allStaleFiles(
|
||||
const QString &applicationName = QString());
|
||||
|
||||
private:
|
||||
QUrl m_managed_file;
|
||||
std::unique_ptr<QLockFile> m_lock;
|
||||
bool m_managed_file_name_changed = false;
|
||||
};
|
||||
|
||||
#endif // QET_KAUTOSAVEFILE_H
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "kcolorbutton.h"
|
||||
|
||||
#include <QColorDialog>
|
||||
#include <QPalette>
|
||||
|
||||
namespace {
|
||||
QColor fallbackColor()
|
||||
{
|
||||
return QPalette{}.color(QPalette::Button);
|
||||
}
|
||||
}
|
||||
|
||||
KColorButton::KColorButton(QWidget *parent) :
|
||||
QPushButton{parent},
|
||||
m_color{fallbackColor()}
|
||||
{
|
||||
connect(this, &QPushButton::clicked, this, &KColorButton::chooseColor);
|
||||
updateButton();
|
||||
}
|
||||
|
||||
QColor KColorButton::color() const
|
||||
{
|
||||
return m_color;
|
||||
}
|
||||
|
||||
void KColorButton::setColor(const QColor &color)
|
||||
{
|
||||
m_color = color.isValid() ? color : fallbackColor();
|
||||
updateButton();
|
||||
}
|
||||
|
||||
void KColorButton::chooseColor()
|
||||
{
|
||||
const auto selected = QColorDialog::getColor(m_color, this);
|
||||
if (!selected.isValid() || selected == m_color) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_color = selected;
|
||||
updateButton();
|
||||
emit changed(m_color);
|
||||
}
|
||||
|
||||
void KColorButton::updateButton()
|
||||
{
|
||||
setText(m_color.name());
|
||||
|
||||
auto pal = palette();
|
||||
pal.setColor(QPalette::Button, m_color);
|
||||
setAutoFillBackground(true);
|
||||
setPalette(pal);
|
||||
update();
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef QET_KCOLORBUTTON_H
|
||||
#define QET_KCOLORBUTTON_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QPushButton>
|
||||
|
||||
class KColorButton : public QPushButton
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit KColorButton(QWidget *parent = nullptr);
|
||||
|
||||
QColor color() const;
|
||||
|
||||
public slots:
|
||||
void setColor(const QColor &color);
|
||||
|
||||
signals:
|
||||
void changed(const QColor &color);
|
||||
|
||||
private slots:
|
||||
void chooseColor();
|
||||
|
||||
private:
|
||||
void updateButton();
|
||||
|
||||
QColor m_color;
|
||||
};
|
||||
|
||||
#endif // QET_KCOLORBUTTON_H
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "kcolorcombo.h"
|
||||
|
||||
#include <QVariant>
|
||||
|
||||
KColorCombo::KColorCombo(QWidget *parent) :
|
||||
QComboBox{parent}
|
||||
{
|
||||
connect(
|
||||
this,
|
||||
QOverload<int>::of(&QComboBox::activated),
|
||||
this,
|
||||
[this](int index) {
|
||||
emit activated(itemData(index).value<QColor>());
|
||||
});
|
||||
}
|
||||
|
||||
void KColorCombo::setColors(const QList<QColor> &colors)
|
||||
{
|
||||
clear();
|
||||
for (const auto &color : colors) {
|
||||
addItem(color.name(), color);
|
||||
}
|
||||
}
|
||||
|
||||
QColor KColorCombo::color(int index) const
|
||||
{
|
||||
if (index < 0 || index >= count()) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return itemData(index).value<QColor>();
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef QET_KCOLORCOMBO_H
|
||||
#define QET_KCOLORCOMBO_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QComboBox>
|
||||
|
||||
class KColorCombo : public QComboBox
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit KColorCombo(QWidget *parent = nullptr);
|
||||
|
||||
void setColors(const QList<QColor> &colors);
|
||||
QColor color(int index) const;
|
||||
|
||||
signals:
|
||||
void activated(const QColor &color);
|
||||
};
|
||||
|
||||
#endif // QET_KCOLORCOMBO_H
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,430 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "plclinkwidget.h"
|
||||
|
||||
#include "../qetgraphicsitem/masterelement.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "../elementprovider.h"
|
||||
#include "../undocommand/linkelementcommand.h"
|
||||
#include "../diagram.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../properties/elementdata.h"
|
||||
|
||||
#include <QTreeWidget>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QCheckBox>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QGridLayout>
|
||||
#include <QMenu>
|
||||
#include <QAction>
|
||||
#include <QFont>
|
||||
#include <QTimer>
|
||||
#include <QSettings>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QGridLayout>
|
||||
#include <QMenu>
|
||||
#include <QAction>
|
||||
|
||||
PlcLinkWidget::PlcLinkWidget(Element *elmt, QWidget *parent)
|
||||
: AbstractElementPropertiesEditorWidget(parent)
|
||||
{
|
||||
auto *main_layout = new QGridLayout(this);
|
||||
|
||||
// Row 0: Status label + buttons (shown when already linked)
|
||||
m_label = new QLabel(tr("Cet élément est déjà lié"), this);
|
||||
m_unlink_pb = new QPushButton(tr("Délier"), this);
|
||||
m_show_this_pb = new QPushButton(tr("Voir cet élément"), this);
|
||||
|
||||
main_layout->addWidget(m_label, 0, 0);
|
||||
main_layout->addWidget(m_unlink_pb, 0, 1);
|
||||
main_layout->addWidget(m_show_this_pb, 0, 2);
|
||||
|
||||
// Row 1: Hide linked elements checkbox
|
||||
m_hide_linked_cb = new QCheckBox(tr("Masquer les éléments connectés"), this);
|
||||
main_layout->addWidget(m_hide_linked_cb, 1, 0, 1, 3);
|
||||
|
||||
// Row 2: Search field
|
||||
m_search_field = new QLineEdit(this);
|
||||
m_search_field->setPlaceholderText(tr("Recherche"));
|
||||
main_layout->addWidget(m_search_field, 2, 0, 1, 3);
|
||||
|
||||
// Row 3: Tree widget
|
||||
m_tree_widget = new QTreeWidget(this);
|
||||
m_tree_widget->setHeaderLabels({
|
||||
tr("Label"), tr("Type"), tr("Adresse"),
|
||||
tr("Fonction"), tr("Commentaire"), tr("Bornes")
|
||||
});
|
||||
m_tree_widget->setRootIsDecorated(true);
|
||||
m_tree_widget->setIndentation(20);
|
||||
m_tree_widget->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
m_tree_widget->setContextMenuPolicy(Qt::CustomContextMenu);
|
||||
m_tree_widget->header()->setStretchLastSection(false);
|
||||
m_tree_widget->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||||
m_tree_widget->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
|
||||
m_tree_widget->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
||||
m_tree_widget->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
|
||||
m_tree_widget->header()->setSectionResizeMode(4, QHeaderView::ResizeToContents);
|
||||
m_tree_widget->header()->setSectionResizeMode(5, QHeaderView::ResizeToContents);
|
||||
main_layout->addWidget(m_tree_widget, 3, 0, 1, 3);
|
||||
|
||||
// Row 4: Hidden masters note
|
||||
m_hidden_masters_label = new QLabel(
|
||||
tr("Remarque : les éléments maîtres ayant atteint leur nombre maximal "
|
||||
"d'esclaves sont masqués."), this);
|
||||
m_hidden_masters_label->setWordWrap(true);
|
||||
QFont italic_font = m_hidden_masters_label->font();
|
||||
italic_font.setItalic(true);
|
||||
m_hidden_masters_label->setFont(italic_font);
|
||||
m_hidden_masters_label->hide();
|
||||
main_layout->addWidget(m_hidden_masters_label, 4, 0, 1, 3);
|
||||
|
||||
main_layout->setRowStretch(3, 1);
|
||||
|
||||
setMinimumWidth(500);
|
||||
|
||||
// Connections
|
||||
connect(m_unlink_pb, &QPushButton::clicked,
|
||||
this, &PlcLinkWidget::on_m_unlink_pb_clicked);
|
||||
connect(m_show_this_pb, &QPushButton::clicked,
|
||||
this, &PlcLinkWidget::on_m_show_this_pb_clicked);
|
||||
connect(m_search_field, &QLineEdit::textEdited,
|
||||
this, &PlcLinkWidget::on_m_search_field_textEdited);
|
||||
connect(m_tree_widget, &QTreeWidget::customContextMenuRequested,
|
||||
this, &PlcLinkWidget::on_m_tree_widget_customContextMenuRequested);
|
||||
connect(m_hide_linked_cb, &QCheckBox::toggled,
|
||||
this, &PlcLinkWidget::on_m_hide_linked_cb_toggled);
|
||||
|
||||
QSettings settings;
|
||||
m_hide_linked_cb->setChecked(
|
||||
settings.value(QStringLiteral("plclinkwidget/hideLinked"), false).toBool());
|
||||
|
||||
if (elmt)
|
||||
setElement(elmt);
|
||||
}
|
||||
|
||||
void PlcLinkWidget::setElement(Element *element)
|
||||
{
|
||||
if (m_element == element)
|
||||
return;
|
||||
|
||||
m_element = element;
|
||||
updateUi();
|
||||
}
|
||||
|
||||
void PlcLinkWidget::apply()
|
||||
{
|
||||
QUndoCommand *undo = associatedUndo();
|
||||
if (undo)
|
||||
m_element->diagram()->undoStack().push(undo);
|
||||
|
||||
m_element_to_link = nullptr;
|
||||
m_pending_io_index = -1;
|
||||
}
|
||||
|
||||
QUndoCommand *PlcLinkWidget::associatedUndo() const
|
||||
{
|
||||
if (!m_element_to_link || m_pending_io_index < 0)
|
||||
return nullptr;
|
||||
|
||||
LinkElementCommand *undo = new LinkElementCommand(m_element);
|
||||
undo->setLink(m_element_to_link);
|
||||
undo->setGroupIndex(m_pending_io_index);
|
||||
return undo;
|
||||
}
|
||||
|
||||
QString PlcLinkWidget::title() const
|
||||
{
|
||||
return tr("Automate (PLC)");
|
||||
}
|
||||
|
||||
void PlcLinkWidget::updateUi()
|
||||
{
|
||||
if (m_element->isFree())
|
||||
hideButtons();
|
||||
else
|
||||
showButtons();
|
||||
|
||||
buildPlcTree();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief PlcLinkWidget::buildPlcTree
|
||||
Build the tree showing all PLC-Masters with their IO entries.
|
||||
IO entries already linked to a slave are shown as struck-through and greyed out.
|
||||
*/
|
||||
void PlcLinkWidget::buildPlcTree()
|
||||
{
|
||||
m_tree_widget->clear();
|
||||
m_io_entry_hash.clear();
|
||||
m_linked_children.clear();
|
||||
|
||||
if (!m_element || !m_element->diagram() || !m_element->diagram()->project())
|
||||
return;
|
||||
|
||||
ElementProvider ep(m_element->diagram()->project());
|
||||
QVector<QPointer<Element>> masters = ep.find(ElementData::Master);
|
||||
|
||||
QSet<int> used_io_indices;
|
||||
|
||||
for (Element *elmt : masters) {
|
||||
if (!elmt || elmt->elementData().m_master_type != ElementData::PLC)
|
||||
continue;
|
||||
|
||||
const auto &plc_data = elmt->elementData().plcMasterData();
|
||||
if (plc_data.ios.isEmpty())
|
||||
continue;
|
||||
|
||||
// Skip full masters
|
||||
MasterElement *me = static_cast<MasterElement*>(elmt);
|
||||
if (me->isFull()) {
|
||||
m_hidden_masters_label->show();
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect used IO indices for this master
|
||||
used_io_indices.clear();
|
||||
for (Element *linked : elmt->linkedElements()) {
|
||||
int idx = elmt->groupIndexForElement(linked);
|
||||
if (idx >= 0)
|
||||
used_io_indices.insert(idx);
|
||||
}
|
||||
|
||||
// Create parent item for this PLC-Master
|
||||
auto *parent_item = new QTreeWidgetItem(m_tree_widget);
|
||||
|
||||
// Show master label + diagram info
|
||||
QString master_label = elmt->actualLabel();
|
||||
if (master_label.isEmpty())
|
||||
master_label = elmt->name();
|
||||
|
||||
QString folio_info;
|
||||
if (elmt->diagram()) {
|
||||
folio_info = QString::number(elmt->diagram()->folioIndex() + 1);
|
||||
}
|
||||
parent_item->setText(0, QString("%1 (%2)")
|
||||
.arg(master_label)
|
||||
.arg(folio_info));
|
||||
QFont bold_font = parent_item->font(0);
|
||||
bold_font.setBold(true);
|
||||
parent_item->setFont(0, bold_font);
|
||||
parent_item->setExpanded(false);
|
||||
|
||||
// Add child items for each IO entry
|
||||
bool all_children_hidden = true;
|
||||
for (int i = 0; i < plc_data.ios.size(); ++i) {
|
||||
const auto &io = plc_data.ios.at(i);
|
||||
auto *child_item = new QTreeWidgetItem(parent_item);
|
||||
|
||||
child_item->setText(1, ElementData::translatedPlcIOType(io.type));
|
||||
child_item->setText(2, io.address);
|
||||
child_item->setText(3, io.functionText);
|
||||
child_item->setText(4, io.comment);
|
||||
|
||||
// Terminal names
|
||||
child_item->setText(5, io.terminals.join(QStringLiteral(", ")));
|
||||
|
||||
PlcIoEntry entry;
|
||||
entry.master = elmt;
|
||||
entry.ioIndex = i;
|
||||
m_io_entry_hash.insert(child_item, entry);
|
||||
|
||||
// If this IO is already linked to a slave
|
||||
if (used_io_indices.contains(i)) {
|
||||
m_linked_children.insert(child_item);
|
||||
if (m_hide_linked_cb->isChecked()) {
|
||||
child_item->setHidden(true);
|
||||
} else {
|
||||
all_children_hidden = false;
|
||||
QFont strike_font = child_item->font(0);
|
||||
strike_font.setStrikeOut(true);
|
||||
child_item->setFont(0, strike_font);
|
||||
child_item->setFont(1, strike_font);
|
||||
child_item->setFont(2, strike_font);
|
||||
child_item->setFont(3, strike_font);
|
||||
child_item->setFont(4, strike_font);
|
||||
child_item->setFont(5, strike_font);
|
||||
|
||||
QBrush grey_brush(Qt::gray);
|
||||
for (int col = 0; col < 6; ++col)
|
||||
child_item->setForeground(col, grey_brush);
|
||||
|
||||
// Show which slave is linked
|
||||
for (Element *linked : elmt->linkedElements()) {
|
||||
if (elmt->groupIndexForElement(linked) == i) {
|
||||
child_item->setToolTip(0,
|
||||
tr("Lié à: %1").arg(linked->actualLabel()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
child_item->setFlags(child_item->flags() & ~Qt::ItemIsSelectable);
|
||||
}
|
||||
} else {
|
||||
all_children_hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If checkbox is on and every child is linked (hidden), hide the master too
|
||||
if (m_hide_linked_cb->isChecked() && all_children_hidden) {
|
||||
parent_item->setHidden(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PlcLinkWidget::isChildLinked(QTreeWidgetItem *child) const
|
||||
{
|
||||
return m_linked_children.contains(child);
|
||||
}
|
||||
|
||||
void PlcLinkWidget::hideButtons()
|
||||
{
|
||||
m_label->hide();
|
||||
m_unlink_pb->hide();
|
||||
m_show_this_pb->hide();
|
||||
m_search_field->show();
|
||||
}
|
||||
|
||||
void PlcLinkWidget::showButtons()
|
||||
{
|
||||
m_label->show();
|
||||
m_unlink_pb->show();
|
||||
m_show_this_pb->show();
|
||||
m_search_field->hide();
|
||||
}
|
||||
|
||||
void PlcLinkWidget::on_m_search_field_textEdited(const QString &text)
|
||||
{
|
||||
for (int i = 0; i < m_tree_widget->topLevelItemCount(); ++i) {
|
||||
QTreeWidgetItem *parent = m_tree_widget->topLevelItem(i);
|
||||
bool any_child_visible = false;
|
||||
|
||||
for (int j = 0; j < parent->childCount(); ++j) {
|
||||
QTreeWidgetItem *child = parent->child(j);
|
||||
bool match = text.isEmpty();
|
||||
if (!match) {
|
||||
for (int col = 0; col < child->columnCount(); ++col) {
|
||||
if (child->text(col).contains(text, Qt::CaseInsensitive)) {
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
bool hidden = m_hide_linked_cb->isChecked() && isChildLinked(child);
|
||||
child->setHidden(!match || hidden);
|
||||
if (match && !hidden) any_child_visible = true;
|
||||
}
|
||||
|
||||
// Also check if parent label matches
|
||||
if (!text.isEmpty() && parent->text(0).contains(text, Qt::CaseInsensitive)) {
|
||||
for (int j = 0; j < parent->childCount(); ++j) {
|
||||
QTreeWidgetItem *child = parent->child(j);
|
||||
bool hidden = m_hide_linked_cb->isChecked() && isChildLinked(child);
|
||||
if (!hidden) {
|
||||
any_child_visible = true;
|
||||
child->setHidden(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parent->setHidden(!any_child_visible);
|
||||
}
|
||||
}
|
||||
|
||||
void PlcLinkWidget::on_m_tree_widget_customContextMenuRequested(const QPoint &pos)
|
||||
{
|
||||
QTreeWidgetItem *item = m_tree_widget->itemAt(pos);
|
||||
if (!item)
|
||||
return;
|
||||
|
||||
// Only show context menu for IO child items (not parent master items)
|
||||
if (!m_io_entry_hash.contains(item))
|
||||
return;
|
||||
|
||||
const PlcIoEntry &entry = m_io_entry_hash.value(item);
|
||||
|
||||
// Check if this IO is already linked
|
||||
Element *master = entry.master;
|
||||
if (!master)
|
||||
return;
|
||||
|
||||
QSet<int> used_indices;
|
||||
for (Element *linked : master->linkedElements()) {
|
||||
int idx = master->groupIndexForElement(linked);
|
||||
if (idx >= 0)
|
||||
used_indices.insert(idx);
|
||||
}
|
||||
|
||||
if (used_indices.contains(entry.ioIndex))
|
||||
return; // Already linked, no actions
|
||||
|
||||
QMenu menu;
|
||||
QAction *link_action = menu.addAction(tr("Connecter"));
|
||||
|
||||
QAction *selected_action = menu.exec(m_tree_widget->viewport()->mapToGlobal(pos));
|
||||
if (selected_action == link_action) {
|
||||
m_element_to_link = master;
|
||||
m_pending_io_index = entry.ioIndex;
|
||||
apply();
|
||||
}
|
||||
}
|
||||
|
||||
void PlcLinkWidget::on_m_unlink_pb_clicked()
|
||||
{
|
||||
if (!m_element || m_element->isFree())
|
||||
return;
|
||||
|
||||
LinkElementCommand *undo = new LinkElementCommand(m_element);
|
||||
|
||||
// Store the current group index so makeLink() can
|
||||
// restore PLC variables and terminal labels on undo.
|
||||
for (Element *elmt : m_element->linkedElements())
|
||||
{
|
||||
if (elmt->linkType() == Element::Master)
|
||||
{
|
||||
int idx = elmt->groupIndexForElement(m_element);
|
||||
if (idx >= 0)
|
||||
undo->setGroupIndex(idx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
undo->unlinkAll();
|
||||
m_element->diagram()->undoStack().push(undo);
|
||||
|
||||
updateUi();
|
||||
}
|
||||
|
||||
void PlcLinkWidget::on_m_show_this_pb_clicked()
|
||||
{
|
||||
if (!m_element || !m_element->diagram())
|
||||
return;
|
||||
|
||||
m_element->diagram()->showMe();
|
||||
m_element->setHighlighted(true);
|
||||
}
|
||||
|
||||
void PlcLinkWidget::on_m_hide_linked_cb_toggled(bool checked)
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue(QStringLiteral("plclinkwidget/hideLinked"), checked);
|
||||
buildPlcTree();
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef PLCLINKWIDGET_H
|
||||
#define PLCLINKWIDGET_H
|
||||
|
||||
#include "abstractelementpropertieseditorwidget.h"
|
||||
|
||||
#include <QHash>
|
||||
#include <QPointer>
|
||||
#include <QSet>
|
||||
|
||||
class QTreeWidgetItem;
|
||||
class QTreeWidget;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class QLabel;
|
||||
class QCheckBox;
|
||||
class Element;
|
||||
|
||||
/**
|
||||
@brief The PlcLinkWidget class
|
||||
Provides a dedicated widget for linking a PLC-Slave to a PLC-Master IO entry.
|
||||
Displays all PLC-Masters as expandable items, each showing their IO entries.
|
||||
Each IO entry can only be linked to one slave. Already-linked entries are
|
||||
shown as struck-through and greyed out.
|
||||
*/
|
||||
class PlcLinkWidget : public AbstractElementPropertiesEditorWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PlcLinkWidget(Element *elmt, QWidget *parent = nullptr);
|
||||
~PlcLinkWidget() override = default;
|
||||
|
||||
void setElement(Element *element) override;
|
||||
void apply() override;
|
||||
QUndoCommand *associatedUndo() const override;
|
||||
QString title() const override;
|
||||
|
||||
public slots:
|
||||
void updateUi() override;
|
||||
|
||||
private:
|
||||
void buildPlcTree();
|
||||
void hideButtons();
|
||||
void showButtons();
|
||||
bool isChildLinked(QTreeWidgetItem *child) const;
|
||||
|
||||
private slots:
|
||||
void on_m_search_field_textEdited(const QString &text);
|
||||
void on_m_tree_widget_customContextMenuRequested(const QPoint &pos);
|
||||
void on_m_unlink_pb_clicked();
|
||||
void on_m_show_this_pb_clicked();
|
||||
void on_m_hide_linked_cb_toggled(bool checked);
|
||||
|
||||
private:
|
||||
QLabel *m_label{nullptr};
|
||||
QPushButton *m_unlink_pb{nullptr};
|
||||
QPushButton *m_show_this_pb{nullptr};
|
||||
QCheckBox *m_hide_linked_cb{nullptr};
|
||||
QLineEdit *m_search_field{nullptr};
|
||||
QTreeWidget *m_tree_widget{nullptr};
|
||||
QLabel *m_hidden_masters_label{nullptr};
|
||||
|
||||
// Maps IO child items to (master, io_index)
|
||||
struct PlcIoEntry {
|
||||
QPointer<Element> master;
|
||||
int ioIndex = -1;
|
||||
};
|
||||
QHash<QTreeWidgetItem*, PlcIoEntry> m_io_entry_hash;
|
||||
QSet<QTreeWidgetItem*> m_linked_children;
|
||||
|
||||
Element *m_element_to_link = nullptr;
|
||||
int m_pending_io_index = -1;
|
||||
};
|
||||
|
||||
#endif // PLCLINKWIDGET_H
|
||||
@@ -188,8 +188,11 @@ class LinkReportPotentialSelector : public AbstractPotentialSelector
|
||||
//### END PRIVATE CLASS ###//
|
||||
|
||||
|
||||
ConductorProperties PotentialSelectorDialog::chosenProperties(QList<ConductorProperties> list, QWidget *widget)
|
||||
ConductorProperties PotentialSelectorDialog::chosenProperties(QList<ConductorProperties> list, QWidget *widget, bool *cancelled)
|
||||
{
|
||||
if (cancelled)
|
||||
*cancelled = false;
|
||||
|
||||
if (list.isEmpty()) {
|
||||
return ConductorProperties() ;
|
||||
} else if (list.size() == 1) {
|
||||
@@ -222,11 +225,25 @@ ConductorProperties PotentialSelectorDialog::chosenProperties(QList<ConductorPro
|
||||
layout.addWidget(b);
|
||||
H.insert(b, cp);
|
||||
}
|
||||
QDialogButtonBox *button_box = new QDialogButtonBox(QDialogButtonBox::Ok, &dialog);
|
||||
|
||||
// Pre-select the first entry: without this, accepting the dialog without
|
||||
// ever touching a radio button silently returned blank properties too,
|
||||
// the same failure mode as the missing Cancel button below.
|
||||
if (!H.isEmpty())
|
||||
H.constBegin().key()->setChecked(true);
|
||||
|
||||
QDialogButtonBox *button_box = new QDialogButtonBox(
|
||||
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog);
|
||||
layout.addWidget(button_box);
|
||||
connect(button_box, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
||||
connect(button_box, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||
|
||||
if (dialog.exec() != QDialog::Accepted) {
|
||||
if (cancelled)
|
||||
*cancelled = true;
|
||||
return ConductorProperties();
|
||||
}
|
||||
|
||||
dialog.exec();
|
||||
for (QRadioButton *b : H.keys()) {
|
||||
if(b->isChecked()) {
|
||||
return H.value(b);
|
||||
|
||||
@@ -64,7 +64,11 @@ namespace Ui {
|
||||
|
||||
the static function chosenProperties,
|
||||
open a dialog who ask user to make a choice between the given
|
||||
properties
|
||||
properties. If the dialog is cancelled (Cancel button, Escape, or the
|
||||
window's close button) and @a cancelled is non-null, *cancelled is set
|
||||
to true and an empty ConductorProperties() is returned; callers that
|
||||
care about a real cancellation (as opposed to "no properties to choose
|
||||
from") should check it rather than relying on the returned value alone.
|
||||
*/
|
||||
class PotentialSelectorDialog : public QDialog
|
||||
{
|
||||
@@ -73,7 +77,8 @@ class PotentialSelectorDialog : public QDialog
|
||||
public:
|
||||
static ConductorProperties chosenProperties(
|
||||
QList<ConductorProperties> list,
|
||||
QWidget *parent = nullptr);
|
||||
QWidget *parent = nullptr,
|
||||
bool *cancelled = nullptr);
|
||||
|
||||
public:
|
||||
explicit PotentialSelectorDialog(
|
||||
|
||||
@@ -55,7 +55,7 @@ Veuillez choisir les propriétées à appliquer au nouveau potentiel.</string>
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Ok</set>
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -44,9 +44,9 @@ ProjectPropertiesDialog::ProjectPropertiesDialog(QETProject *project, QWidget *p
|
||||
|
||||
m_properties_dialog->addPage(new TerminalStripProjectConfigPage { project, parent });
|
||||
|
||||
connect(projectAutoNumConfigPage,SIGNAL(setAutoNum(QString)),newDiagramPage,SLOT(setFolioAutonum(QString)));
|
||||
connect(projectAutoNumConfigPage,SIGNAL(saveCurrentTbp()),newDiagramPage,SLOT(saveCurrentTbp()));
|
||||
connect(projectAutoNumConfigPage,SIGNAL(loadSavedTbp()),newDiagramPage,SLOT(loadSavedTbp()));
|
||||
connect(projectAutoNumConfigPage, qOverload<QString>(&ProjectAutoNumConfigPage::setAutoNum), newDiagramPage, &NewDiagramPage::setFolioAutonum);
|
||||
connect(projectAutoNumConfigPage, &ProjectAutoNumConfigPage::saveCurrentTbp, newDiagramPage, &NewDiagramPage::saveCurrentTbp);
|
||||
connect(projectAutoNumConfigPage, &ProjectAutoNumConfigPage::loadSavedTbp, newDiagramPage, &NewDiagramPage::loadSavedTbp);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +63,13 @@ ProjectPropertiesDialog::~ProjectPropertiesDialog ()
|
||||
*/
|
||||
void ProjectPropertiesDialog::exec()
|
||||
{
|
||||
m_properties_dialog->setWindowModality(Qt::WindowModal);
|
||||
// ApplicationModal (not WindowModal) so no other window — including other
|
||||
// MDI subwindows — can dispatch events while the dialog holds raw
|
||||
// QETProject* pointers in its config pages. WindowModal only blocks the
|
||||
// parent ProjectView; the rest of the MDI area stays live, which allowed
|
||||
// new_project / close_project to replace the project under the dialog
|
||||
// and cause a SIGSEGV. See issue #527.
|
||||
m_properties_dialog->setWindowModality(Qt::ApplicationModal);
|
||||
m_properties_dialog -> exec();
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
|
||||
#include "../diagram.h"
|
||||
#include "../lastusedstyle.h"
|
||||
#include "../qetgraphicsitem/qetshapeitem.h"
|
||||
#include "../ui_shapegraphicsitempropertieswidget.h"
|
||||
|
||||
@@ -72,7 +73,8 @@ void ShapeGraphicsItemPropertiesWidget::setItem(QetShapeItem *shape)
|
||||
}
|
||||
|
||||
m_shape = shape;
|
||||
ui->m_close_polygon->setVisible(m_shape->shapeType() == QetShapeItem::Polygon);
|
||||
ui->m_close_polygon->setVisible(m_shape->shapeType() == QetShapeItem::Polygon
|
||||
|| m_shape->shapeType() == QetShapeItem::Path);
|
||||
ui->m_filling_gb->setHidden(m_shape->shapeType() == QetShapeItem::Line);
|
||||
|
||||
updateUi();
|
||||
@@ -163,6 +165,58 @@ QUndoCommand* ShapeGraphicsItemPropertiesWidget::associatedUndo() const
|
||||
{
|
||||
QPropertyUndoCommand *undo = nullptr;
|
||||
|
||||
//Geometry
|
||||
const bool hasGeometry = (m_shape->shapeType() == QetShapeItem::Line
|
||||
|| m_shape->shapeType() == QetShapeItem::Rectangle
|
||||
|| m_shape->shapeType() == QetShapeItem::Ellipse);
|
||||
if (hasGeometry)
|
||||
{
|
||||
if (m_shape->shapeType() == QetShapeItem::Line)
|
||||
{
|
||||
const QLineF old_line = m_shape->line();
|
||||
QLineF new_line = old_line;
|
||||
new_line.setLength(ui->m_geom_dim1_dsb->value());
|
||||
|
||||
if (new_line != old_line)
|
||||
{
|
||||
undo = new QPropertyUndoCommand(m_shape, "line", old_line, new_line);
|
||||
undo->setText(tr("Modifier la longueur d'une ligne"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const QRectF old_rect = m_shape->rect().normalized();
|
||||
const bool isEllipse = (m_shape->shapeType() == QetShapeItem::Ellipse);
|
||||
const qreal new_width = isEllipse ? ui->m_geom_dim1_dsb->value() * 2.0 : ui->m_geom_dim1_dsb->value();
|
||||
const qreal new_height = isEllipse ? ui->m_geom_dim2_dsb->value() * 2.0 : ui->m_geom_dim2_dsb->value();
|
||||
// Anchored on the shape's own current top-left, not
|
||||
// re-centered -- matches how the resize handle itself
|
||||
// behaves (the opposite corner stays put while the
|
||||
// dragged one moves), so typing a width/height here
|
||||
// and dragging a handle agree on what "resizing" means.
|
||||
const QRectF new_rect(old_rect.topLeft(), QSizeF(new_width, new_height));
|
||||
|
||||
if (new_rect != old_rect)
|
||||
{
|
||||
undo = new QPropertyUndoCommand(m_shape, "rect", old_rect, new_rect);
|
||||
undo->setText(tr("Modifier la taille d'une forme"));
|
||||
}
|
||||
}
|
||||
|
||||
const qreal old_angle = m_shape->rotation();
|
||||
const qreal new_angle = ui->m_geom_angle_dsb->value();
|
||||
if (qAbs(old_angle - new_angle) > 0.05)
|
||||
{
|
||||
if (undo)
|
||||
new QPropertyUndoCommand(m_shape, "rotation", old_angle, new_angle, undo);
|
||||
else
|
||||
{
|
||||
undo = new QPropertyUndoCommand(m_shape, "rotation", old_angle, new_angle);
|
||||
undo->setText(tr("Modifier l'angle d'une forme"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QPen old_pen = m_shape->pen();
|
||||
QPen new_pen = old_pen;
|
||||
|
||||
@@ -180,6 +234,7 @@ QUndoCommand* ShapeGraphicsItemPropertiesWidget::associatedUndo() const
|
||||
{
|
||||
undo = new QPropertyUndoCommand(m_shape, "pen", old_pen, new_pen);
|
||||
undo->setText(tr("Modifier le trait d'une forme"));
|
||||
LastUsedStyle::setShapePen(new_pen);
|
||||
}
|
||||
|
||||
QBrush old_brush = m_shape->brush();
|
||||
@@ -196,6 +251,7 @@ QUndoCommand* ShapeGraphicsItemPropertiesWidget::associatedUndo() const
|
||||
undo = new QPropertyUndoCommand(m_shape, "brush", old_brush, new_brush);
|
||||
undo->setText(tr("Modifier le remplissage d'une forme"));
|
||||
}
|
||||
LastUsedStyle::setShapeBrush(new_brush);
|
||||
}
|
||||
|
||||
if (ui->m_close_polygon->isChecked() != m_shape->isClosed())
|
||||
@@ -321,6 +377,7 @@ QUndoCommand* ShapeGraphicsItemPropertiesWidget::associatedUndo() const
|
||||
|
||||
if (new_pen != old_pen) {
|
||||
new QPropertyUndoCommand(m_shape, "pen", old_pen, new_pen, undo);
|
||||
LastUsedStyle::setShapePen(new_pen);
|
||||
}
|
||||
|
||||
QBrush old_brush = m_shape->brush();
|
||||
@@ -330,6 +387,7 @@ QUndoCommand* ShapeGraphicsItemPropertiesWidget::associatedUndo() const
|
||||
|
||||
if (new_brush != old_brush) {
|
||||
new QPropertyUndoCommand(m_shape, "brush", old_brush, new_brush, undo);
|
||||
LastUsedStyle::setShapeBrush(new_brush);
|
||||
}
|
||||
|
||||
if (ui->m_close_polygon->isChecked() != m_shape->isClosed()) {
|
||||
@@ -361,6 +419,35 @@ void ShapeGraphicsItemPropertiesWidget::updateUi()
|
||||
|
||||
if (m_shape)
|
||||
{
|
||||
//Geometry
|
||||
const bool hasGeometry = (m_shape->shapeType() == QetShapeItem::Line
|
||||
|| m_shape->shapeType() == QetShapeItem::Rectangle
|
||||
|| m_shape->shapeType() == QetShapeItem::Ellipse);
|
||||
ui->m_geometry_gb->setVisible(hasGeometry);
|
||||
if (hasGeometry)
|
||||
{
|
||||
ui->m_geom_angle_dsb->setValue(m_shape->rotation());
|
||||
|
||||
if (m_shape->shapeType() == QetShapeItem::Line)
|
||||
{
|
||||
ui->m_geom_dim1_label->setText(tr("Longueur"));
|
||||
ui->m_geom_dim1_dsb->setValue(m_shape->line().length());
|
||||
ui->m_geom_dim2_label->setVisible(false);
|
||||
ui->m_geom_dim2_dsb->setVisible(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
const QRectF r = m_shape->rect().normalized();
|
||||
const bool isEllipse = (m_shape->shapeType() == QetShapeItem::Ellipse);
|
||||
ui->m_geom_dim1_label->setText(isEllipse ? tr("Rayon X") : tr("Largeur"));
|
||||
ui->m_geom_dim2_label->setText(isEllipse ? tr("Rayon Y") : tr("Hauteur"));
|
||||
ui->m_geom_dim1_dsb->setValue(isEllipse ? r.width() / 2.0 : r.width());
|
||||
ui->m_geom_dim2_dsb->setValue(isEllipse ? r.height() / 2.0 : r.height());
|
||||
ui->m_geom_dim2_label->setVisible(true);
|
||||
ui->m_geom_dim2_dsb->setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
//Pen
|
||||
ui->m_style_cb->setCurrentIndex(static_cast<int>(m_shape->pen().style()) - 1);
|
||||
ui->m_size_dsb ->setValue(m_shape->pen().widthF());
|
||||
@@ -368,7 +455,7 @@ void ShapeGraphicsItemPropertiesWidget::updateUi()
|
||||
ui->m_color_kpb->setColor(m_shape->pen().color());
|
||||
|
||||
//Brush
|
||||
if (m_shape->shapeType() == QetShapeItem::Polygon)
|
||||
if (m_shape->shapeType() == QetShapeItem::Polygon || m_shape->shapeType() == QetShapeItem::Path)
|
||||
ui->m_filling_gb->setVisible(m_shape->isClosed());
|
||||
|
||||
ui->m_brush_style_cb->setCurrentIndex(static_cast<int>(m_shape->brush().style()));
|
||||
@@ -379,6 +466,7 @@ void ShapeGraphicsItemPropertiesWidget::updateUi()
|
||||
}
|
||||
else if (m_shapes_list.size() >= 2)
|
||||
{
|
||||
ui->m_geometry_gb->setVisible(false);
|
||||
ui->m_close_polygon->setHidden(true);
|
||||
bool same = true;
|
||||
//Pen
|
||||
@@ -472,6 +560,15 @@ void ShapeGraphicsItemPropertiesWidget::setUpEditConnection()
|
||||
|
||||
if (m_shape || !m_shapes_list.isEmpty())
|
||||
{
|
||||
m_edit_connection << connect (ui->m_geom_dim1_dsb, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
||||
this, &ShapeGraphicsItemPropertiesWidget::apply);
|
||||
|
||||
m_edit_connection << connect (ui->m_geom_dim2_dsb, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
||||
this, &ShapeGraphicsItemPropertiesWidget::apply);
|
||||
|
||||
m_edit_connection << connect (ui->m_geom_angle_dsb, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
||||
this, &ShapeGraphicsItemPropertiesWidget::apply);
|
||||
|
||||
m_edit_connection << connect (ui->m_style_cb, QOverload<int>::of(&QComboBox::activated),
|
||||
this, &ShapeGraphicsItemPropertiesWidget::apply);
|
||||
|
||||
@@ -490,21 +587,30 @@ void ShapeGraphicsItemPropertiesWidget::setUpEditConnection()
|
||||
m_edit_connection << connect (ui->m_close_polygon, &QCheckBox::clicked,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::apply);
|
||||
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::penChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
if (m_shape)
|
||||
{
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::penChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::closeChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::closeChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::brushChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::brushChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::geometryChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::transformChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
void ShapeGraphicsItemPropertiesWidget::clearEditConnection()
|
||||
{
|
||||
for (const auto &c : qAsConst(m_edit_connection)) {
|
||||
for (const auto &c : std::as_const(m_edit_connection)) {
|
||||
disconnect(c);
|
||||
}
|
||||
m_edit_connection.clear();
|
||||
|
||||
@@ -14,6 +14,78 @@
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="m_geometry_gb">
|
||||
<property name="title">
|
||||
<string>Géométrie</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="m_geom_dim1_label">
|
||||
<property name="text">
|
||||
<string>Largeur</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QDoubleSpinBox" name="m_geom_dim1_dsb">
|
||||
<property name="decimals">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.010000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>100000.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="m_geom_dim2_label">
|
||||
<property name="text">
|
||||
<string>Hauteur</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QDoubleSpinBox" name="m_geom_dim2_dsb">
|
||||
<property name="decimals">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.010000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>100000.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="m_geom_angle_label">
|
||||
<property name="text">
|
||||
<string>Angle</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QDoubleSpinBox" name="m_geom_angle_dsb">
|
||||
<property name="decimals">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-360.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>360.000000000000000</double>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string> °</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
@@ -230,7 +302,7 @@
|
||||
<item>
|
||||
<widget class="QCheckBox" name="m_close_polygon">
|
||||
<property name="text">
|
||||
<string>Polygone fermé</string>
|
||||
<string>Forme fermée</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -257,6 +329,9 @@
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>m_geom_dim1_dsb</tabstop>
|
||||
<tabstop>m_geom_dim2_dsb</tabstop>
|
||||
<tabstop>m_geom_angle_dsb</tabstop>
|
||||
<tabstop>m_style_cb</tabstop>
|
||||
<tabstop>m_size_dsb</tabstop>
|
||||
<tabstop>m_color_kpb</tabstop>
|
||||
|
||||
@@ -1,22 +1,72 @@
|
||||
#include "terminalnumberingdialog.h"
|
||||
#include "ui_terminalnumberingdialog.h"
|
||||
#include "../qet.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../diagram.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "../undocommand/changeelementinformationcommand.h"
|
||||
#include <QUndoCommand>
|
||||
#include <QCheckBox>
|
||||
#include <QVBoxLayout>
|
||||
#include <QSet>
|
||||
#include <algorithm>
|
||||
|
||||
/**
|
||||
* @brief TerminalNumberingDialog::TerminalNumberingDialog
|
||||
* Constructor
|
||||
* @param parent
|
||||
* @param project Pointer to the current project (used to populate terminal strips)
|
||||
*/
|
||||
TerminalNumberingDialog::TerminalNumberingDialog(QWidget *parent) :
|
||||
QDialog(parent),
|
||||
ui(new Ui::TerminalNumberingDialog)
|
||||
TerminalNumberingDialog::TerminalNumberingDialog(QWidget *parent, QETProject *project) :
|
||||
QDialog(parent),
|
||||
ui(new Ui::TerminalNumberingDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// Connect radio button to enable/disable the "also number letters" checkbox
|
||||
connect(ui->rb_type_alpha, &QRadioButton::toggled, ui->cb_also_alpha, &QCheckBox::setEnabled);
|
||||
|
||||
// Collect all unique terminal strip prefixes from the project
|
||||
if (project) {
|
||||
QSet<QString> prefixes;
|
||||
foreach (Diagram *diagram, project->diagrams()) {
|
||||
foreach (QGraphicsItem *qgi, diagram->items()) {
|
||||
if (Element *elmt = qgraphicsitem_cast<Element *>(qgi)) {
|
||||
if (elmt->elementData().m_type == ElementData::Terminal) {
|
||||
// Ignore locked terminals
|
||||
DiagramContext info = elmt->elementInformations();
|
||||
if (QET::infoFlagIsTrue(info.value(QStringLiteral("auto_num_locked")).toString())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString label = elmt->actualLabel();
|
||||
if (label.isEmpty()) continue;
|
||||
|
||||
// Handle labels with and without colon
|
||||
int colonIndex = label.lastIndexOf(':');
|
||||
QString prefix;
|
||||
if (colonIndex != -1) {
|
||||
prefix = label.left(colonIndex);
|
||||
} else {
|
||||
prefix = label;
|
||||
}
|
||||
prefixes.insert(prefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort prefixes alphabetically and create checkboxes
|
||||
QStringList sortedPrefixes = prefixes.values();
|
||||
sortedPrefixes.sort(Qt::CaseInsensitive);
|
||||
|
||||
foreach (const QString &prefix, sortedPrefixes) {
|
||||
QCheckBox *cb = new QCheckBox(prefix);
|
||||
cb->setChecked(true);
|
||||
ui->verticalLayout_strips_content->addWidget(cb);
|
||||
m_stripCheckboxes.insert(prefix, cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,11 +96,37 @@ bool TerminalNumberingDialog::isAlphanumeric() const
|
||||
return ui->rb_type_alpha->isChecked();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalNumberingDialog::alsoNumberLetters
|
||||
* @return true if the "also number letters" checkbox is checked
|
||||
*/
|
||||
bool TerminalNumberingDialog::alsoNumberLetters() const
|
||||
{
|
||||
return ui->cb_also_alpha->isChecked();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalNumberingDialog::excludedStrips
|
||||
* @return List of terminal strip prefixes that should be excluded from numbering
|
||||
*/
|
||||
QStringList TerminalNumberingDialog::excludedStrips() const
|
||||
{
|
||||
QStringList excluded;
|
||||
QMapIterator<QString, QCheckBox*> it(m_stripCheckboxes);
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
if (!it.value()->isChecked()) {
|
||||
excluded.append(it.key());
|
||||
}
|
||||
}
|
||||
return excluded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalNumberingDialog::getUndoCommand
|
||||
* Scans the given project for terminals, sorts them according to user preferences
|
||||
* (X/Y axis, alphanumeric rules), and generates an undo command containing all label changes.
|
||||
* * @param project Pointer to the current QETProject
|
||||
* @param project Pointer to the current QETProject
|
||||
* @return QUndoCommand* containing the modifications, or nullptr if no changes are needed.
|
||||
*/
|
||||
QUndoCommand* TerminalNumberingDialog::getUndoCommand(QETProject *project) const {
|
||||
@@ -58,6 +134,8 @@ QUndoCommand* TerminalNumberingDialog::getUndoCommand(QETProject *project) const
|
||||
|
||||
bool axisX = isXAxisPriority();
|
||||
bool alpha = isAlphanumeric();
|
||||
bool alsoAlpha = alsoNumberLetters();
|
||||
QStringList excluded = excludedStrips();
|
||||
|
||||
// 1. Helper structure to store and sort terminal data
|
||||
struct TermInfo {
|
||||
@@ -81,7 +159,7 @@ QUndoCommand* TerminalNumberingDialog::getUndoCommand(QETProject *project) const
|
||||
DiagramContext info = elmt->elementInformations();
|
||||
|
||||
// Ignore locked terminals (if the user checked a 'lock' property)
|
||||
if (info.value(QStringLiteral("auto_num_locked")).toString() == QLatin1String("true")) {
|
||||
if (QET::infoFlagIsTrue(info.value(QStringLiteral("auto_num_locked")).toString())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -97,6 +175,9 @@ QUndoCommand* TerminalNumberingDialog::getUndoCommand(QETProject *project) const
|
||||
suffix = label.mid(colonIndex + 1);
|
||||
}
|
||||
|
||||
// Skip excluded terminal strips
|
||||
if (excluded.contains(prefix)) continue;
|
||||
|
||||
// If user chose purely numeric, skip terminals with alphabetical suffixes
|
||||
if (!alpha && !suffix.isEmpty()) {
|
||||
bool isNum;
|
||||
@@ -154,8 +235,16 @@ QUndoCommand* TerminalNumberingDialog::getUndoCommand(QETProject *project) const
|
||||
// If it was a number (e.g., "1") or empty, update it with the new counter
|
||||
newLabel = ti.prefix + ":" + QString::number(newNum);
|
||||
} else {
|
||||
// If it was alphabetical (e.g., "N", "PE"), keep the original text but consume the count!
|
||||
newLabel = ti.prefix + ":" + ti.suffix;
|
||||
// Only append to purely alphabetic suffixes (N, PE), so re-running is a
|
||||
// no-op and already-numbered suffixes (L1, L2, L3) keep their meaning.
|
||||
const bool allLetters = !ti.suffix.isEmpty()
|
||||
&& std::all_of(ti.suffix.cbegin(), ti.suffix.cend(),
|
||||
[](QChar c){ return c.isLetter(); });
|
||||
if (alsoAlpha && allLetters) {
|
||||
newLabel = ti.prefix + ":" + ti.suffix + QString::number(newNum);
|
||||
} else {
|
||||
newLabel = ti.prefix + ":" + ti.suffix;
|
||||
}
|
||||
}
|
||||
|
||||
DiagramContext oldInfo = ti.elmt->elementInformations();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef WIRINGLISTDIALOG_H
|
||||
#define WIRINGLISTDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
|
||||
class QETProject;
|
||||
|
||||
/**
|
||||
@brief The WiringListDialog class
|
||||
Read-only view of the project's from-to wiring list, read from the
|
||||
wiring_list_view of projectDataBase.
|
||||
|
||||
Deliberately not an exporter: QET already ships a wiring-list CSV export
|
||||
(Projet > Exporter le plan de câblage, and --export-cables), which walks
|
||||
the project XML and covers that need. This dialog exists to make the
|
||||
database view inspectable, and above all to state how many conductors
|
||||
are missing from it and why -- a count the CSV export cannot give,
|
||||
because it never excludes anything in the first place.
|
||||
*/
|
||||
class WiringListDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit WiringListDialog(QETProject *project, QWidget *parent = nullptr);
|
||||
|
||||
private:
|
||||
QETProject *m_project = nullptr;
|
||||
};
|
||||
|
||||
#endif // WIRINGLISTDIALOG_H
|
||||
@@ -37,9 +37,10 @@ XRefPropertiesWidget::XRefPropertiesWidget(QHash <QString, XRefProperties> prope
|
||||
{
|
||||
ui->setupUi(this);
|
||||
buildUi();
|
||||
connect(ui->m_display_has_cross_rb, SIGNAL(toggled(bool)), ui->m_cross_properties_gb, SLOT(setEnabled(bool)));
|
||||
connect(ui->m_type_cb, SIGNAL(currentIndexChanged(int)), this, SLOT(typeChanged()));
|
||||
connect(ui->m_snap_to_cb, SIGNAL(currentIndexChanged(int)), this, SLOT(enableOffsetSB(int)));
|
||||
connect(ui->m_display_has_cross_rb, &QRadioButton::toggled, ui->m_cross_properties_gb, &QWidget::setEnabled);
|
||||
connect(ui->m_display_has_contacts_rb, &QRadioButton::toggled, ui->m_show_all_slaves_cb, &QWidget::setEnabled);
|
||||
connect(ui->m_type_cb, qOverload<int>(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::typeChanged);
|
||||
connect(ui->m_snap_to_cb, qOverload<int>(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::enableOffsetSB);
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
@@ -49,9 +50,10 @@ XRefPropertiesWidget::XRefPropertiesWidget(QHash <QString, XRefProperties> prope
|
||||
*/
|
||||
XRefPropertiesWidget::~XRefPropertiesWidget()
|
||||
{
|
||||
disconnect(ui->m_display_has_cross_rb, SIGNAL(toggled(bool)), ui->m_cross_properties_gb, SLOT(setEnabled(bool)));
|
||||
disconnect(ui->m_type_cb, SIGNAL(currentIndexChanged(int)), this, SLOT(typeChanged()));
|
||||
disconnect(ui->m_snap_to_cb, SIGNAL(currentIndexChanged(int)), this, SLOT(enableOffsetSB(int)));
|
||||
disconnect(ui->m_display_has_cross_rb, &QRadioButton::toggled, ui->m_cross_properties_gb, &QWidget::setEnabled);
|
||||
disconnect(ui->m_display_has_contacts_rb, &QRadioButton::toggled, ui->m_show_all_slaves_cb, &QWidget::setEnabled);
|
||||
disconnect(ui->m_type_cb, qOverload<int>(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::typeChanged);
|
||||
disconnect(ui->m_snap_to_cb, qOverload<int>(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::enableOffsetSB);
|
||||
delete ui;
|
||||
}
|
||||
|
||||
@@ -101,6 +103,7 @@ void XRefPropertiesWidget::buildUi()
|
||||
ui -> m_type_cb -> addItem(tr("Bobine"), "coil");
|
||||
ui -> m_type_cb -> addItem(tr("Organe de protection"), "protection");
|
||||
ui -> m_type_cb -> addItem(tr("Commutateur / bouton"), "commutator");
|
||||
ui -> m_type_cb -> addItem(tr("Automate (PLC)"), "plc");
|
||||
|
||||
ui -> m_snap_to_cb -> addItem(tr("En bas de page"), "bottom");
|
||||
ui -> m_snap_to_cb -> addItem(tr("Sous le label de l'élément"), "label");
|
||||
@@ -110,6 +113,7 @@ void XRefPropertiesWidget::buildUi()
|
||||
ui -> m_xrefpos_cb -> addItem(tr("Left"),"left");
|
||||
ui -> m_xrefpos_cb -> addItem(tr("Right"),"right");
|
||||
ui -> m_xrefpos_cb -> addItem(tr("Text alignment"),"alignment");
|
||||
ui -> m_xrefpos_cb -> addItem(tr("Champ de texte"),"text_field");
|
||||
m_previous_type_index = ui -> m_type_cb -> currentIndex();
|
||||
}
|
||||
|
||||
@@ -138,14 +142,17 @@ void XRefPropertiesWidget::saveProperties(int index) {
|
||||
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "left") xrp.setXrefPos(Qt::AlignLeft);
|
||||
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "right") xrp.setXrefPos(Qt::AlignRight);
|
||||
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "alignment") xrp.setXrefPos(Qt::AlignBaseline);
|
||||
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "text_field") xrp.setXrefPos(Qt::AlignHCenter);
|
||||
xrp.setShowPowerContac(ui->m_show_power_cb->isChecked());
|
||||
xrp.setShowTerminalName(ui->m_show_terminal_name_cb->isChecked());
|
||||
xrp.setShowAllConfiguredSlaves(ui->m_show_all_slaves_cb->isChecked());
|
||||
xrp.setPrefix("power", ui->m_power_prefix_le->text());
|
||||
xrp.setPrefix("delay", ui->m_delay_prefix_le->text());
|
||||
xrp.setPrefix("switch", ui->m_switch_prefix_le->text());
|
||||
xrp.setMasterLabel(ui->m_master_le->text());
|
||||
xrp.setSlaveLabel(ui->m_slave_le->text());
|
||||
xrp.setOffset(ui->m_offset_sb->value());
|
||||
xrp.setSlaveOffset(ui->m_slave_offset_sb->value());
|
||||
|
||||
m_properties.insert(type, xrp);
|
||||
}
|
||||
@@ -176,6 +183,9 @@ void XRefPropertiesWidget::updateDisplay()
|
||||
int offset = xrp.offset();
|
||||
ui->m_offset_sb->setValue(offset);
|
||||
|
||||
int slave_offset = xrp.slaveOffset();
|
||||
ui->m_slave_offset_sb->setValue(slave_offset);
|
||||
|
||||
if (xrp.snapTo() == XRefProperties::Bottom){
|
||||
ui->m_snap_to_cb->setCurrentIndex(ui->m_snap_to_cb->findData("bottom"));
|
||||
ui->m_offset_sb->setEnabled(true);
|
||||
@@ -190,12 +200,33 @@ void XRefPropertiesWidget::updateDisplay()
|
||||
else if(xrp.getXrefPos() == Qt::AlignRight) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("right"));
|
||||
else if(xrp.getXrefPos() == Qt::AlignBaseline) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("alignment"));
|
||||
else if(xrp.getXrefPos() == Qt::AlignBottom) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("bottom"));
|
||||
else if(xrp.getXrefPos() == Qt::AlignHCenter) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("text_field"));
|
||||
ui->m_show_power_cb->setChecked(xrp.showPowerContact());
|
||||
ui->m_show_terminal_name_cb->setChecked(xrp.showTerminalName());
|
||||
ui->m_show_all_slaves_cb->setChecked(xrp.showAllConfiguredSlaves());
|
||||
//The radio button only emits toggled() when it really changes: loading
|
||||
//a type whose display did not change left the checkbox with the enabled
|
||||
//state of the previously displayed type (it stayed clickable although
|
||||
//the cross display was selected). Set the state explicitly here.
|
||||
ui->m_show_all_slaves_cb->setEnabled(
|
||||
ui->m_display_has_contacts_rb->isChecked());
|
||||
ui->m_power_prefix_le-> setText(xrp.prefix("power"));
|
||||
ui->m_delay_prefix_le-> setText(xrp.prefix("delay"));
|
||||
ui->m_switch_prefix_le->setText(xrp.prefix("switch"));
|
||||
ui->m_cross_properties_gb->setDisabled(!ui->m_display_has_cross_rb->isChecked());
|
||||
|
||||
//The cross ref of a PLC master is always drawn as its IO table, and
|
||||
//the slaves are referenced directly into that table: the contacts/
|
||||
//cross choice, the two display checkboxes and the cross options below
|
||||
//have no effect at all for this type, so they are hidden instead of
|
||||
//being offered for nothing. The positioning settings and the labels
|
||||
//(the table really uses them) stay available.
|
||||
const bool is_plc = type == QLatin1String("plc");
|
||||
ui->m_display_has_contacts_rb->setVisible(!is_plc);
|
||||
ui->m_display_has_cross_rb->setVisible(!is_plc);
|
||||
ui->m_show_terminal_name_cb->setVisible(!is_plc);
|
||||
ui->m_show_all_slaves_cb->setVisible(!is_plc);
|
||||
ui->m_cross_properties_gb->setVisible(!is_plc);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -90,20 +90,56 @@
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_10">
|
||||
<property name="text">
|
||||
<string>XRef slave position</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="m_xrefpos_cb"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_10">
|
||||
<property name="text">
|
||||
<string>XRef slave position</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="m_xrefpos_cb"/>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_8">
|
||||
<item>
|
||||
<widget class="QLabel" name="label_11">
|
||||
<property name="text">
|
||||
<string>Distance label - slave :</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSpinBox" name="m_slave_offset_sb">
|
||||
<property name="toolTip">
|
||||
<string>Distance in pixels between the label and the slave cross reference</string>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string notr="true">px</string>
|
||||
</property>
|
||||
<property name="correctionMode">
|
||||
<enum>QAbstractSpinBox::CorrectToNearestValue</enum>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<number>-50</number>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="singleStep">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<item>
|
||||
@@ -115,6 +151,16 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="m_show_all_slaves_cb">
|
||||
<property name="toolTip">
|
||||
<string>Afficher dans le peigne de contacts tous les contacts esclaves définis par le maître, même ceux qui ne sont pas encore reliés, dans l'ordre défini par le maître</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Afficher tous les esclaves définis par le maître</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="m_display_has_contacts_rb">
|
||||
<property name="text">
|
||||
@@ -291,6 +337,7 @@
|
||||
<tabstop>m_slave_le</tabstop>
|
||||
<tabstop>m_show_power_cb</tabstop>
|
||||
<tabstop>m_show_terminal_name_cb</tabstop>
|
||||
<tabstop>m_show_all_slaves_cb</tabstop>
|
||||
<tabstop>m_power_prefix_le</tabstop>
|
||||
<tabstop>m_delay_prefix_le</tabstop>
|
||||
<tabstop>m_switch_prefix_le</tabstop>
|
||||
|
||||
Reference in New Issue
Block a user