mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-19 04:24:14 +02:00
Merge remote-tracking branch 'upstream/master' into review-635-fix
# Conflicts: # CMakeLists.txt # cmake/developer_options.cmake # cmake/qet_compilation_vars.cmake # sources/qetapp.cpp
This commit is contained in:
@@ -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,265 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "conductorcolortoolbutton.h"
|
||||
|
||||
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
|
||||
#include "../diagram.h"
|
||||
#include "../diagramcontent.h"
|
||||
#include "../diagramview.h"
|
||||
#include "../lastusedstyle.h"
|
||||
#include "../qetdiagrameditor.h"
|
||||
#include "../projectview.h"
|
||||
#include "../qetgraphicsitem/conductor.h"
|
||||
#include "../conductorproperties.h"
|
||||
|
||||
#include <QColorDialog>
|
||||
#include <QMenu>
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
|
||||
namespace {
|
||||
/**
|
||||
The colours an electrician reaches for, in the order the trade
|
||||
names them: the three phases, neutral, earth, then the ones used
|
||||
for control and extra-low-voltage circuits. Not a standard in
|
||||
itself -- IEC 60445 only fixes blue for neutral and green/yellow
|
||||
for protective earth -- but it covers the wiring a schematic
|
||||
actually shows, which is what makes the toolbar worth a click.
|
||||
*/
|
||||
struct NamedColor { const char *context_name; QColor color; };
|
||||
|
||||
QList<NamedColor> standardColors()
|
||||
{
|
||||
return {
|
||||
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Noir"), QColor(0x00, 0x00, 0x00)},
|
||||
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Marron"), QColor(0x7B, 0x3F, 0x00)},
|
||||
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Gris"), QColor(0x80, 0x80, 0x80)},
|
||||
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Bleu"), QColor(0x00, 0x00, 0xFF)},
|
||||
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Vert"), QColor(0x00, 0x80, 0x00)},
|
||||
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Rouge"), QColor(0xFF, 0x00, 0x00)},
|
||||
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Orange"), QColor(0xFF, 0x80, 0x00)},
|
||||
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Violet"), QColor(0x80, 0x00, 0x80)},
|
||||
{QT_TRANSLATE_NOOP("ConductorColorToolButton", "Blanc"), QColor(0xFF, 0xFF, 0xFF)},
|
||||
};
|
||||
}
|
||||
|
||||
const int MAX_RECENT = 6;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorColorToolButton::ConductorColorToolButton
|
||||
@param editor : the diagram editor this button acts on
|
||||
@param parent
|
||||
*/
|
||||
ConductorColorToolButton::ConductorColorToolButton(QETDiagramEditor *editor, QWidget *parent) :
|
||||
QToolButton(parent),
|
||||
m_editor(editor)
|
||||
{
|
||||
setPopupMode(QToolButton::InstantPopup);
|
||||
setToolTip(tr("Couleur de conducteur"));
|
||||
setStatusTip(tr("Applique une couleur aux conducteurs sélectionnés, et l'utilise pour les prochains conducteurs tracés",
|
||||
"status bar tip"));
|
||||
|
||||
m_current = LastUsedStyle::hasConductorColor() ? LastUsedStyle::conductorColor()
|
||||
: QColor(Qt::black);
|
||||
setMenu(new QMenu(this));
|
||||
rebuildMenu();
|
||||
setSwatch(m_current);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorColorToolButton::currentView
|
||||
@return the folio being edited, or nullptr when no project is open.
|
||||
Goes through the project view because QETDiagramEditor keeps its own
|
||||
currentDiagramView() private.
|
||||
*/
|
||||
DiagramView *ConductorColorToolButton::currentView() const
|
||||
{
|
||||
ProjectView *pv = m_editor ? m_editor->currentProjectView() : nullptr;
|
||||
return pv ? pv->currentDiagram() : nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorColorToolButton::updateEnabledState
|
||||
Greyed out when there is no folio to act on, or when the project is
|
||||
read-only -- the same rule the other conductor actions follow.
|
||||
*/
|
||||
void ConductorColorToolButton::updateEnabledState()
|
||||
{
|
||||
DiagramView *dv = currentView();
|
||||
setEnabled(dv && dv->diagram() && !dv->diagram()->isReadOnly());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorColorToolButton::rebuildMenu
|
||||
The standard colours never change; the recent ones do, so the menu is
|
||||
rebuilt rather than kept in sync entry by entry.
|
||||
*/
|
||||
void ConductorColorToolButton::rebuildMenu()
|
||||
{
|
||||
QMenu *m = menu();
|
||||
m->clear();
|
||||
|
||||
for (const auto &nc : standardColors())
|
||||
{
|
||||
const QColor c = nc.color;
|
||||
QAction *a = m->addAction(swatchIcon(c),
|
||||
tr(nc.context_name));
|
||||
connect(a, &QAction::triggered, this, [this, c]() { applyColor(c); });
|
||||
}
|
||||
|
||||
if (!m_recent.isEmpty())
|
||||
{
|
||||
m->addSeparator();
|
||||
QAction *title = m->addAction(tr("Récemment utilisées"));
|
||||
title->setEnabled(false);
|
||||
for (const QColor &c : std::as_const(m_recent))
|
||||
{
|
||||
QAction *a = m->addAction(swatchIcon(c), c.name());
|
||||
connect(a, &QAction::triggered, this, [this, c]() { applyColor(c); });
|
||||
}
|
||||
}
|
||||
|
||||
m->addSeparator();
|
||||
QAction *other = m->addAction(tr("Autre couleur…"));
|
||||
connect(other, &QAction::triggered, this, &ConductorColorToolButton::chooseOtherColor);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorColorToolButton::applyColor
|
||||
Recolour the selected conductors, and remember the colour for the next
|
||||
one drawn.
|
||||
@param color
|
||||
*/
|
||||
void ConductorColorToolButton::applyColor(const QColor &color)
|
||||
{
|
||||
if (!color.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Always: this is the half that works with nothing selected.
|
||||
LastUsedStyle::setConductorColor(color);
|
||||
rememberRecent(color);
|
||||
setSwatch(color);
|
||||
|
||||
DiagramView *dv = currentView();
|
||||
if (!dv) {
|
||||
return;
|
||||
}
|
||||
Diagram *diagram = dv->diagram();
|
||||
if (!diagram || diagram->isReadOnly()) {
|
||||
return;
|
||||
}
|
||||
|
||||
DiagramContent dc(diagram);
|
||||
const auto conductors = dc.conductors(DiagramContent::AnyConductor);
|
||||
if (conductors.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QUndoCommand *undo = new QUndoCommand(tr("Modifier la couleur de %n conducteur(s)",
|
||||
"undo caption", conductors.count()));
|
||||
int changed = 0;
|
||||
for (Conductor *conductor : conductors)
|
||||
{
|
||||
ConductorProperties before = conductor->properties();
|
||||
if (before.color == color) {
|
||||
continue;
|
||||
}
|
||||
ConductorProperties after = before;
|
||||
after.color = color;
|
||||
|
||||
QVariant old_value, new_value;
|
||||
old_value.setValue(before);
|
||||
new_value.setValue(after);
|
||||
new QPropertyUndoCommand(conductor, "properties", old_value, new_value, undo);
|
||||
++changed;
|
||||
}
|
||||
|
||||
//Every selected conductor was already this colour: pushing an
|
||||
//empty command would put a no-op step in the undo stack.
|
||||
if (changed) {
|
||||
diagram->undoStack().push(undo);
|
||||
} else {
|
||||
delete undo;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorColorToolButton::chooseOtherColor
|
||||
*/
|
||||
void ConductorColorToolButton::chooseOtherColor()
|
||||
{
|
||||
const QColor c = QColorDialog::getColor(m_current, this,
|
||||
tr("Choisir une couleur de conducteur"));
|
||||
if (c.isValid()) {
|
||||
applyColor(c);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorColorToolButton::rememberRecent
|
||||
Most recent first, no duplicates, capped.
|
||||
@param color
|
||||
*/
|
||||
void ConductorColorToolButton::rememberRecent(const QColor &color)
|
||||
{
|
||||
//A colour that is already one row up in the standard list would
|
||||
//only appear twice, under a hex name it does not need.
|
||||
for (const auto &nc : standardColors()) {
|
||||
if (nc.color == color) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_recent.removeAll(color);
|
||||
m_recent.prepend(color);
|
||||
while (m_recent.size() > MAX_RECENT) {
|
||||
m_recent.removeLast();
|
||||
}
|
||||
rebuildMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorColorToolButton::setSwatch
|
||||
@param color
|
||||
*/
|
||||
void ConductorColorToolButton::setSwatch(const QColor &color)
|
||||
{
|
||||
m_current = color;
|
||||
setIcon(swatchIcon(color));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ConductorColorToolButton::swatchIcon
|
||||
@param color
|
||||
@return a plain square of that colour, outlined so that white and very
|
||||
light colours are still visible against the toolbar.
|
||||
*/
|
||||
QIcon ConductorColorToolButton::swatchIcon(const QColor &color)
|
||||
{
|
||||
QPixmap pix(16, 16);
|
||||
pix.fill(Qt::transparent);
|
||||
QPainter p(&pix);
|
||||
p.setRenderHint(QPainter::Antialiasing, false);
|
||||
p.setBrush(color);
|
||||
p.setPen(QPen(QColor(0x40, 0x40, 0x40), 1));
|
||||
p.drawRect(0, 0, 15, 15);
|
||||
p.end();
|
||||
return QIcon(pix);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
#include "../titleblockpropertieswidget.h"
|
||||
#include "../xrefpropertieswidget.h"
|
||||
#include "guidespropertieswidget.h"
|
||||
#include "../autoNum/numerotationcontext.h"
|
||||
#include "../autoNum/ui/selectautonumw.h"
|
||||
#include <QFont>
|
||||
#include <QFontDialog>
|
||||
#include <QSizePolicy>
|
||||
@@ -101,6 +103,32 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
|
||||
}
|
||||
m_gpw->setGuides(loaded_guides);
|
||||
|
||||
// global auto-numbering defaults (only when editing global settings, not a project)
|
||||
if (!m_project) {
|
||||
auto saw_conductor = new SelectAutonumW(1);
|
||||
auto saw_element = new SelectAutonumW(0);
|
||||
auto saw_folio = new SelectAutonumW(2);
|
||||
|
||||
initAutoNumTab(m_autonum_conductor, saw_conductor, QStringLiteral("autonum/conductor"));
|
||||
initAutoNumTab(m_autonum_element, saw_element, QStringLiteral("autonum/element"));
|
||||
initAutoNumTab(m_autonum_folio, saw_folio, QStringLiteral("autonum/folio"));
|
||||
|
||||
QSettings autonum_settings;
|
||||
loadAutoNumTab(m_autonum_conductor, autonum_settings);
|
||||
loadAutoNumTab(m_autonum_element, autonum_settings);
|
||||
loadAutoNumTab(m_autonum_folio, autonum_settings);
|
||||
|
||||
// Intercept Return key in the combo line edits so it doesn't
|
||||
// activate the dialog's default button (OK).
|
||||
for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) {
|
||||
if (QComboBox *combo = tab->widget->contextComboBox()) {
|
||||
if (combo->lineEdit()) {
|
||||
combo->lineEdit()->installEventFilter(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//If there is a project, we edit his properties
|
||||
if (m_project) {
|
||||
bpw -> setProperties (m_project -> defaultBorderProperties());
|
||||
@@ -110,10 +138,11 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
|
||||
xrefpw -> setProperties (m_project -> defaultXRefProperties());
|
||||
}
|
||||
|
||||
connect(ipw,SIGNAL(openAutoNumFolioEditor(QString)),this,SLOT(changeToAutoFolioTab()));
|
||||
connect(ipw, &TitleBlockPropertiesWidget::openAutoNumFolioEditor, this, &NewDiagramPage::changeToAutoFolioTab);
|
||||
|
||||
// main tab widget
|
||||
QTabWidget *tab_widget = new QTabWidget(this);
|
||||
m_tab_widget = tab_widget;
|
||||
QWidget *diagram_widget = new QWidget();
|
||||
QVBoxLayout *diagram_layout = new QVBoxLayout(diagram_widget);
|
||||
|
||||
@@ -127,6 +156,19 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
|
||||
tab_widget -> addTab (xrefpw, tr("Références croisées"));
|
||||
tab_widget -> addTab (m_gpw, tr("Guides"));
|
||||
|
||||
// add auto-numbering tab only for global settings (not per project)
|
||||
if (!m_project) {
|
||||
QWidget *autonum_widget = new QWidget();
|
||||
QVBoxLayout *autonum_layout = new QVBoxLayout(autonum_widget);
|
||||
autonum_layout->addWidget(new QLabel(tr("Définir les règles de numérotation automatique par défaut pour les nouveaux projets :")));
|
||||
QTabWidget *autonum_inner_tab = new QTabWidget();
|
||||
autonum_inner_tab->addTab(m_autonum_conductor.widget, tr("Conducteurs"));
|
||||
autonum_inner_tab->addTab(m_autonum_element.widget, tr("Eléments"));
|
||||
autonum_inner_tab->addTab(m_autonum_folio.widget, tr("Folios"));
|
||||
autonum_layout->addWidget(autonum_inner_tab);
|
||||
tab_widget -> addTab (autonum_widget, tr("Numérotation auto"));
|
||||
}
|
||||
|
||||
QVBoxLayout *vlayout1 = new QVBoxLayout();
|
||||
vlayout1->addWidget(tab_widget);
|
||||
|
||||
@@ -138,7 +180,7 @@ NewDiagramPage::NewDiagramPage(QETProject *project,
|
||||
*/
|
||||
NewDiagramPage::~NewDiagramPage()
|
||||
{
|
||||
disconnect(ipw,SIGNAL(openAutoNumFolioEditor(QString)),this,SLOT(changeToAutoFolioTab()));
|
||||
disconnect(ipw, &TitleBlockPropertiesWidget::openAutoNumFolioEditor, this, &NewDiagramPage::changeToAutoFolioTab);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -214,7 +256,11 @@ void NewDiagramPage::applyConf()
|
||||
rpw->toSettings(settings, "diagrameditor/defaultreport");
|
||||
|
||||
// default xref properties
|
||||
QHash <QString, XRefProperties> hash_xrp = xrefpw -> properties();
|
||||
const QHash<QString, XRefProperties> hash_xrp = xrefpw->properties();
|
||||
for (auto it = hash_xrp.constBegin() ; it != hash_xrp.constEnd() ; ++it) {
|
||||
it.value().toSettings(settings,
|
||||
QStringLiteral("diagrameditor/defaultxref") % it.key());
|
||||
}
|
||||
|
||||
// Global in QSettings speichern
|
||||
QList<Diagram::Guide> current_guides = m_gpw->guides();
|
||||
@@ -226,6 +272,9 @@ void NewDiagramPage::applyConf()
|
||||
settings.setValue(QStringLiteral("color"), current_guides[i].color.name());
|
||||
}
|
||||
settings.endArray();
|
||||
|
||||
// save global auto-numbering defaults
|
||||
persistAutonumSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,6 +340,134 @@ void NewDiagramPage::loadSavedTbp()
|
||||
applyConf();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::isPlaceholder
|
||||
Return true if @a name matches the combo box's built-in placeholder text
|
||||
(first item). This is locale-independent because it reads the actual item text.
|
||||
*/
|
||||
bool NewDiagramPage::isPlaceholder(QComboBox *combo, const QString &name)
|
||||
{
|
||||
return !combo->count() || name == combo->itemText(0);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::initAutoNumTab
|
||||
Initialise an AutoNumTab struct and connect its signals.
|
||||
*/
|
||||
void NewDiagramPage::initAutoNumTab(AutoNumTab &tab, SelectAutonumW *w, const QString &prefix)
|
||||
{
|
||||
tab.widget = w;
|
||||
tab.prefix = prefix;
|
||||
|
||||
connect(w, &SelectAutonumW::applyPressed, this, [this, &tab]() { saveAutoNumContext(tab); });
|
||||
connect(w, &SelectAutonumW::removeClicked, this, [this, &tab]() { removeAutoNumContext(tab); });
|
||||
connect(w->contextComboBox(), &QComboBox::activated, this, [this, &tab](int index) {
|
||||
if (index >= 0) {
|
||||
QString name = tab.widget->contextComboBox()->itemText(index);
|
||||
if (tab.contexts.contains(name)) {
|
||||
tab.widget->setContext(tab.contexts.value(name));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::loadAutoNumTab
|
||||
Load saved rules from QSettings into an AutoNumTab.
|
||||
*/
|
||||
void NewDiagramPage::loadAutoNumTab(AutoNumTab &tab, QSettings &settings)
|
||||
{
|
||||
auto data = NumerotationContext::loadFromSettings(settings, tab.prefix);
|
||||
tab.contexts = data.first;
|
||||
for (auto it = tab.contexts.constBegin(); it != tab.contexts.constEnd(); ++it) {
|
||||
tab.widget->contextComboBox()->addItem(it.key());
|
||||
}
|
||||
if (!tab.contexts.isEmpty() && !data.second.isEmpty()
|
||||
&& tab.contexts.contains(data.second)) {
|
||||
tab.widget->contextComboBox()->setCurrentText(data.second);
|
||||
tab.widget->setContext(tab.contexts.value(data.second));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::saveAutoNumContext
|
||||
Save the current context from an AutoNumTab's widget into its hash and
|
||||
persist to QSettings immediately.
|
||||
*/
|
||||
void NewDiagramPage::saveAutoNumContext(AutoNumTab &tab)
|
||||
{
|
||||
QString name = tab.widget->contextComboBox()->currentText().trimmed();
|
||||
if (name.isEmpty() || isPlaceholder(tab.widget->contextComboBox(), name)) {
|
||||
return;
|
||||
}
|
||||
tab.contexts.insert(name, tab.widget->toNumContext());
|
||||
if (tab.widget->contextComboBox()->findText(name) == -1) {
|
||||
tab.widget->contextComboBox()->addItem(name);
|
||||
}
|
||||
persistAutonumSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::removeAutoNumContext
|
||||
Remove the current context from an AutoNumTab's hash and persist.
|
||||
*/
|
||||
void NewDiagramPage::removeAutoNumContext(AutoNumTab &tab)
|
||||
{
|
||||
QString name = tab.widget->contextComboBox()->currentText().trimmed();
|
||||
if (name.isEmpty() || isPlaceholder(tab.widget->contextComboBox(), name)) {
|
||||
return;
|
||||
}
|
||||
int idx = tab.widget->contextComboBox()->findText(name);
|
||||
if (idx == -1) return;
|
||||
tab.contexts.remove(name);
|
||||
tab.widget->contextComboBox()->removeItem(idx);
|
||||
tab.widget->contextComboBox()->setCurrentText(QString());
|
||||
tab.widget->setContext(NumerotationContext());
|
||||
persistAutonumSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::persistAutonumSettings
|
||||
Save all autonum contexts to QSettings immediately.
|
||||
*/
|
||||
void NewDiagramPage::persistAutonumSettings()
|
||||
{
|
||||
QSettings settings;
|
||||
for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) {
|
||||
QString current;
|
||||
QComboBox *combo = tab->widget->contextComboBox();
|
||||
if (!isPlaceholder(combo, combo->currentText().trimmed())
|
||||
&& tab->contexts.contains(combo->currentText().trimmed())) {
|
||||
current = combo->currentText().trimmed();
|
||||
}
|
||||
NumerotationContext::saveToSettings(tab->contexts, current,
|
||||
settings, tab->prefix);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief NewDiagramPage::eventFilter
|
||||
Intercept Return/Enter in combo box line edits so it doesn't close the
|
||||
settings dialog.
|
||||
*/
|
||||
bool NewDiagramPage::eventFilter(QObject *obj, QEvent *event)
|
||||
{
|
||||
if (event->type() == QEvent::KeyPress) {
|
||||
auto *ke = static_cast<QKeyEvent *>(event);
|
||||
if ((ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter)) {
|
||||
// Check if this is a line edit inside one of our autonum combos
|
||||
for (auto *tab : {&m_autonum_conductor, &m_autonum_element, &m_autonum_folio}) {
|
||||
if (QComboBox *combo = tab->widget->contextComboBox()) {
|
||||
if (combo->lineEdit() && combo->lineEdit() == obj) {
|
||||
return true; // eat the event
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ConfigPage::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
/**
|
||||
Constructeur
|
||||
@param parent QWidget parent
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "configpage.h"
|
||||
#include "../projectpropertiesdialog.h"
|
||||
#include "../titleblockpropertieswidget.h"
|
||||
#include "../autoNum/numerotationcontext.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QtWidgets>
|
||||
@@ -32,6 +33,7 @@ class XRefPropertiesWidget;
|
||||
class GuidesPropertiesWidget;
|
||||
class QETProject;
|
||||
class TitleBlockProperties;
|
||||
class SelectAutonumW;
|
||||
|
||||
/**
|
||||
@brief The NewDiagramPage class
|
||||
@@ -48,6 +50,7 @@ class NewDiagramPage : public ConfigPage {
|
||||
~NewDiagramPage() override;
|
||||
private:
|
||||
NewDiagramPage(const NewDiagramPage &);
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
public slots:
|
||||
void changeToAutoFolioTab();
|
||||
void setFolioAutonum(QString);
|
||||
@@ -72,7 +75,25 @@ public slots:
|
||||
XRefPropertiesWidget *xrefpw; ///< Widget to edit default xref properties
|
||||
GuidesPropertiesWidget *m_gpw; ///< Widget to edit guides
|
||||
TitleBlockProperties savedTbp; ///< Used to save current TBP and retrieve later
|
||||
QTabWidget *m_tab_widget; ///< Main tab widget (stored for later access)
|
||||
|
||||
// auto-numbering tab data
|
||||
struct AutoNumTab {
|
||||
SelectAutonumW *widget = nullptr;
|
||||
QHash<QString, NumerotationContext> contexts;
|
||||
QString prefix;
|
||||
};
|
||||
AutoNumTab m_autonum_conductor;
|
||||
AutoNumTab m_autonum_element;
|
||||
AutoNumTab m_autonum_folio;
|
||||
|
||||
void initAutoNumTab(AutoNumTab &tab, SelectAutonumW *w, const QString &prefix);
|
||||
void loadAutoNumTab(AutoNumTab &tab, QSettings &settings);
|
||||
void saveAutoNumContext(AutoNumTab &tab);
|
||||
void removeAutoNumContext(AutoNumTab &tab);
|
||||
void persistAutonumSettings();
|
||||
|
||||
static bool isPlaceholder(QComboBox *combo, const QString &name);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,9 +40,6 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
|
||||
QSettings settings;
|
||||
|
||||
//Appearance tab
|
||||
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) // ###Qt 6:remove
|
||||
ui->m_hdpi_round_policy_widget->setDisabled(true);
|
||||
#else
|
||||
ui->m_hdpi_round_policy_cb->addItem(tr("Arrondi supérieur pour 0.5 et plus"), QLatin1String("Round"));
|
||||
ui->m_hdpi_round_policy_cb->addItem(tr("Toujours arrondi supérieur"), QLatin1String("Ceil"));
|
||||
ui->m_hdpi_round_policy_cb->addItem(tr("Toujours arrondi inférieur"), QLatin1String("Floor"));
|
||||
@@ -65,7 +62,7 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
|
||||
ui->m_hdpi_round_policy_cb->setCurrentIndex(4);
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
|
||||
ui->grid_startup_cb->setChecked(settings.value("diagrameditor/grid_display_startup", true).toBool());
|
||||
ui->guides_startup_cb->setChecked(settings.value("diagrameditor/guides_display_startup", false).toBool());
|
||||
ui->DiagramEditor_xGrid_sb->setValue(settings.value("diagrameditor/Xgrid", 10).toInt());
|
||||
@@ -215,10 +212,8 @@ void GeneralConfigurationPage::applyConf()
|
||||
settings.setValue("lang", ui->m_lang_cb->itemData(ui->m_lang_cb->currentIndex()).toString());
|
||||
|
||||
//hdpi
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
QetSettings::setHdpiScaleFactorRoundingPolicy(ui->m_hdpi_round_policy_cb->currentData().toString());
|
||||
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFactorRoundingPolicy());
|
||||
#endif
|
||||
|
||||
//ELEMENT EDITOR
|
||||
settings.setValue("elementeditor/default-informations", ui->m_default_elements_info->toPlainText());
|
||||
@@ -393,7 +388,7 @@ void GeneralConfigurationPage::fillLang()
|
||||
ui->m_lang_cb->addItem(QET::Icons::hr, tr("Croate"), "hr");
|
||||
ui->m_lang_cb->addItem(QET::Icons::it, tr("Italien"), "it");
|
||||
ui->m_lang_cb->addItem(QET::Icons::jp, tr("Japonais"), "ja");
|
||||
ui->m_lang_cb->addItem(QET::Icons::ko, tr("Coréen"), "ko");
|
||||
ui->m_lang_cb->addItem(QET::Icons::ko, tr("Coréen"), "ko");
|
||||
ui->m_lang_cb->addItem(QET::Icons::pl, tr("Polonais"), "pl");
|
||||
ui->m_lang_cb->addItem(QET::Icons::pt, tr("Portugais"), "pt");
|
||||
ui->m_lang_cb->addItem(QET::Icons::ro, tr("Roumains"), "ro");
|
||||
@@ -405,9 +400,9 @@ void GeneralConfigurationPage::fillLang()
|
||||
ui->m_lang_cb->addItem(QET::Icons::tr, tr("Turc"), "tr");
|
||||
ui->m_lang_cb->addItem(QET::Icons::hu, tr("Hongrois"), "hu");
|
||||
ui->m_lang_cb->addItem(QET::Icons::mn, tr("Mongol"), "mn");
|
||||
ui->m_lang_cb->addItem(QET::Icons::uk, tr("Ukrainien"), "uk");
|
||||
ui->m_lang_cb->addItem(QET::Icons::zh, tr("Chinois"), "zh");
|
||||
ui->m_lang_cb->addItem(QET::Icons::se, tr("Suédois"), "sv");
|
||||
ui->m_lang_cb->addItem(QET::Icons::uk, tr("Ukrainien"), "uk");
|
||||
ui->m_lang_cb->addItem(QET::Icons::zh, tr("Chinois"), "zh");
|
||||
ui->m_lang_cb->addItem(QET::Icons::se, tr("Suédois"), "sv");
|
||||
//set current index to the lang found in setting file
|
||||
//if lang doesn't exist set to system
|
||||
QSettings settings;
|
||||
|
||||
@@ -258,27 +258,6 @@
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>Répertoire de la collection commune</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="m_common_elmt_path_cb">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Par defaut</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Parcourir...</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_16">
|
||||
<property name="text">
|
||||
@@ -286,8 +265,8 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="m_company_elmt_path_cb">
|
||||
<item row="5" column="1">
|
||||
<widget class="QComboBox" name="m_custom_tbt_path_cb">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Par defaut</string>
|
||||
@@ -300,6 +279,27 @@
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QComboBox" name="m_custom_elmt_path_cb">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Par defaut</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Parcourir...</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>Répertoire de la collection commune</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_17">
|
||||
<property name="text">
|
||||
@@ -328,48 +328,6 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<widget class="QComboBox" name="m_custom_elmt_path_cb">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Par defaut</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Parcourir...</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="label_10">
|
||||
<property name="text">
|
||||
<string>Répertoire des cartouches utilisateur</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="1">
|
||||
<widget class="QComboBox" name="m_custom_tbt_path_cb">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Par defaut</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Parcourir...</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="text">
|
||||
<string>(Recharger les collections d'éléments pour appliquer les changements)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="7" column="0">
|
||||
<widget class="QLabel" name="label_23">
|
||||
<property name="text">
|
||||
@@ -391,6 +349,48 @@
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="m_common_elmt_path_cb">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Par defaut</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Parcourir...</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="5" column="0">
|
||||
<widget class="QLabel" name="label_10">
|
||||
<property name="text">
|
||||
<string>Répertoire des cartouches utilisateur</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QComboBox" name="m_company_elmt_path_cb">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Par defaut</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Parcourir...</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="8" column="1">
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="text">
|
||||
<string>(Recharger les collections d'éléments pour appliquer les changements)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "../autoNum/ui/formulaautonumberingw.h"
|
||||
#include "../autoNum/ui/selectautonumw.h"
|
||||
#include "../project/projectpropertieshandler.h"
|
||||
#include "../qet.h"
|
||||
#include "../qeticons.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../borderpropertieswidget.h"
|
||||
@@ -290,9 +291,14 @@ ProjectAutoNumConfigPage::ProjectAutoNumConfigPage (QETProject *project,
|
||||
QWidget *parent) :
|
||||
ProjectConfigPage(project, parent)
|
||||
{
|
||||
initWidgets();
|
||||
buildConnections();
|
||||
readValuesFromProject();
|
||||
// Follow the same contract ProjectMainConfigPage's constructor does --
|
||||
// init() is documented to be the thing a subclass constructor calls
|
||||
// (see ProjectConfigPage::init()'s doc comment). Calling the pieces
|
||||
// individually here used to skip both initLayout() and adjustReadOnly(),
|
||||
// and called readValuesFromProject() even when m_project was null.
|
||||
// buildConnections() itself is invoked from the end of initWidgets()
|
||||
// below, in the same relative position it held here.
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -349,9 +355,22 @@ void ProjectAutoNumConfigPage::initWidgets()
|
||||
m_faw = new FolioAutonumberingW(project());
|
||||
tab_widget->addTab(m_faw, tr("Numérotation auto des folios"));
|
||||
|
||||
QHBoxLayout *main_layout = new QHBoxLayout();
|
||||
m_import_pb = new QPushButton(
|
||||
tr("Importer depuis un autre projet..."), this);
|
||||
m_import_pb->setToolTip(
|
||||
tr("Reprendre les numérotations automatiques "
|
||||
"enregistrées dans un autre projet"));
|
||||
|
||||
QHBoxLayout *button_layout = new QHBoxLayout();
|
||||
button_layout->addStretch();
|
||||
button_layout->addWidget(m_import_pb);
|
||||
|
||||
QVBoxLayout *main_layout = new QVBoxLayout();
|
||||
main_layout->addWidget(tab_widget);
|
||||
main_layout->addLayout(button_layout);
|
||||
setLayout(main_layout);
|
||||
|
||||
buildConnections();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -360,6 +379,12 @@ void ProjectAutoNumConfigPage::initWidgets()
|
||||
*/
|
||||
void ProjectAutoNumConfigPage::readValuesFromProject()
|
||||
{
|
||||
// This is called again after an import, so start from an empty
|
||||
// combo box instead of appending a second copy of every name.
|
||||
m_saw_conductor->contextComboBox()->clear();
|
||||
m_saw_element->contextComboBox()->clear();
|
||||
m_saw_folio->contextComboBox()->clear();
|
||||
|
||||
//Conductor Tab
|
||||
const QStringList strlc(m_project->conductorAutoNum().keys());
|
||||
m_saw_conductor->contextComboBox()->addItems(strlc);
|
||||
@@ -382,6 +407,9 @@ void ProjectAutoNumConfigPage::readValuesFromProject()
|
||||
*/
|
||||
void ProjectAutoNumConfigPage::adjustReadOnly()
|
||||
{
|
||||
if (m_import_pb && m_project) {
|
||||
m_import_pb->setDisabled(m_project->isReadOnly());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -391,25 +419,28 @@ void ProjectAutoNumConfigPage::adjustReadOnly()
|
||||
void ProjectAutoNumConfigPage::buildConnections()
|
||||
{
|
||||
//Management Tab
|
||||
connect (m_amw, SIGNAL(applyPressed()), this, SLOT(applyManagement()));
|
||||
connect(m_amw, &AutoNumberingManagementW::applyPressed, this, &ProjectAutoNumConfigPage::applyManagement);
|
||||
|
||||
//Conductor Tab
|
||||
connect(m_saw_conductor, &SelectAutonumW::applyPressed, this, &ProjectAutoNumConfigPage::saveContextConductor);
|
||||
connect(m_saw_conductor, &SelectAutonumW::removeClicked, this, &ProjectAutoNumConfigPage::removeContextConductor);
|
||||
connect(m_saw_conductor->contextComboBox(), SIGNAL(currentIndexChanged(QString)), this, SLOT(updateContextConductor(QString)));
|
||||
connect(m_saw_conductor->contextComboBox(), &QComboBox::textActivated, this, &ProjectAutoNumConfigPage::updateContextConductor);
|
||||
|
||||
//Element Tab
|
||||
connect(m_saw_element, &SelectAutonumW::applyPressed, this, &ProjectAutoNumConfigPage::saveContextElement);
|
||||
connect(m_saw_element, &SelectAutonumW::removeClicked, this, &ProjectAutoNumConfigPage::removeContextElement);
|
||||
connect(m_saw_element->contextComboBox(), SIGNAL(currentIndexChanged(QString)), this, SLOT(updateContextElement(QString)));
|
||||
connect(m_saw_element->contextComboBox(), &QComboBox::textActivated, this, &ProjectAutoNumConfigPage::updateContextElement);
|
||||
|
||||
//Folio Tab
|
||||
connect(m_saw_folio, &SelectAutonumW::applyPressed, this, &ProjectAutoNumConfigPage::saveContextFolio);
|
||||
connect(m_saw_folio, &SelectAutonumW::removeClicked, this, &ProjectAutoNumConfigPage::removeContextFolio);
|
||||
connect(m_saw_folio->contextComboBox(), SIGNAL(currentIndexChanged(QString)), this, SLOT(updateContextFolio(QString)));
|
||||
connect(m_saw_folio->contextComboBox(), &QComboBox::textActivated, this, &ProjectAutoNumConfigPage::updateContextFolio);
|
||||
|
||||
// Auto Folio Numbering
|
||||
connect (m_faw, SIGNAL (applyPressed()), this, SLOT (applyAutoNum()));
|
||||
connect(m_faw, &FolioAutonumberingW::applyPressed, this, &ProjectAutoNumConfigPage::applyAutoNum);
|
||||
|
||||
//Import from another project
|
||||
connect(m_import_pb, &QPushButton::clicked, this, &ProjectAutoNumConfigPage::importFromProject);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -480,6 +511,198 @@ void ProjectAutoNumConfigPage::saveContextElement()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ProjectAutoNumConfigPage::importFromProject
|
||||
Read the automatic numbering rules stored in another .qet project and
|
||||
copy the ones the user selects into this project.
|
||||
|
||||
The source file is parsed as plain XML rather than opened as a
|
||||
QETProject: opening it would run the whole load path, including the
|
||||
modal dialog raised when the file was written by a different version
|
||||
of QElectroTech.
|
||||
*/
|
||||
void ProjectAutoNumConfigPage::importFromProject()
|
||||
{
|
||||
if (!m_project || m_project->isReadOnly()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QString path = QFileDialog::getOpenFileName(
|
||||
this,
|
||||
tr("Importer les numérotations d'un projet"),
|
||||
m_project->currentDir(),
|
||||
tr("Projet QElectroTech (*.qet)"));
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
QMessageBox::warning(this, tr("Import impossible"),
|
||||
tr("Impossible d'ouvrir %1").arg(path));
|
||||
return;
|
||||
}
|
||||
|
||||
QDomDocument doc;
|
||||
if (!doc.setContent(&file)) {
|
||||
QMessageBox::warning(this, tr("Import impossible"),
|
||||
tr("%1 n'est pas un projet QElectroTech valide.")
|
||||
.arg(QFileInfo(path).fileName()));
|
||||
return;
|
||||
}
|
||||
file.close();
|
||||
|
||||
const QDomNodeList newdiagrams =
|
||||
doc.elementsByTagName(QStringLiteral("newdiagrams"));
|
||||
if (newdiagrams.isEmpty()) {
|
||||
QMessageBox::information(this, tr("Aucune numérotation"),
|
||||
tr("Ce projet ne contient aucune numérotation automatique."));
|
||||
return;
|
||||
}
|
||||
const QDomElement root = newdiagrams.at(0).toElement();
|
||||
|
||||
// tag of the group, tag of one entry, label shown to the user
|
||||
struct Category {
|
||||
QString group_tag;
|
||||
QString item_tag;
|
||||
QString label;
|
||||
};
|
||||
const QList<Category> categories {
|
||||
{QStringLiteral("conductors_autonums"),
|
||||
QStringLiteral("conductor_autonum"), tr("Conducteurs")},
|
||||
{QStringLiteral("element_autonums"),
|
||||
QStringLiteral("element_autonum"), tr("Eléments")},
|
||||
{QStringLiteral("folio_autonums"),
|
||||
QStringLiteral("folio_autonum"), tr("Folios")}
|
||||
};
|
||||
|
||||
QDialog dialog(this);
|
||||
dialog.setWindowTitle(tr("Numérotations à importer"));
|
||||
QVBoxLayout *layout = new QVBoxLayout(&dialog);
|
||||
layout->addWidget(new QLabel(
|
||||
tr("Numérotations trouvées dans %1 :")
|
||||
.arg(QFileInfo(path).fileName()), &dialog));
|
||||
|
||||
QListWidget *list = new QListWidget(&dialog);
|
||||
layout->addWidget(list);
|
||||
|
||||
// NumerotationContext is not a QVariant type, so the list item
|
||||
// carries an index into this instead of the context itself.
|
||||
QList<NumerotationContext> contexts;
|
||||
for (int i = 0 ; i < categories.count() ; ++i)
|
||||
{
|
||||
const Category &category = categories.at(i);
|
||||
QDomElement group;
|
||||
for (QDomNode n = root.firstChild() ; !n.isNull() ; n = n.nextSibling()) {
|
||||
if (n.toElement().tagName() == category.group_tag) {
|
||||
group = n.toElement();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (group.isNull()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (QDomElement entry : QET::findInDomElement(group, category.item_tag))
|
||||
{
|
||||
const QString title = entry.attribute(QStringLiteral("title"));
|
||||
if (title.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bool exists = false;
|
||||
switch (i) {
|
||||
case 0: exists = m_project->conductorAutoNum().contains(title); break;
|
||||
case 1: exists = m_project->elementAutoNum().contains(title); break;
|
||||
default: exists = m_project->folioAutoNum().contains(title); break;
|
||||
}
|
||||
|
||||
QListWidgetItem *item = new QListWidgetItem(
|
||||
exists ? tr("%1 : %2 (existe déjà)")
|
||||
.arg(category.label, title)
|
||||
: QStringLiteral("%1 : %2")
|
||||
.arg(category.label, title),
|
||||
list);
|
||||
item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
|
||||
item->setCheckState(exists ? Qt::Unchecked : Qt::Checked);
|
||||
item->setData(Qt::UserRole, i);
|
||||
item->setData(Qt::UserRole + 1, title);
|
||||
|
||||
NumerotationContext nc;
|
||||
nc.fromXml(entry);
|
||||
item->setData(Qt::UserRole + 2, contexts.count());
|
||||
contexts << nc;
|
||||
}
|
||||
}
|
||||
|
||||
if (contexts.isEmpty()) {
|
||||
QMessageBox::information(this, tr("Aucune numérotation"),
|
||||
tr("Ce projet ne contient aucune numérotation automatique."));
|
||||
return;
|
||||
}
|
||||
|
||||
QCheckBox *overwrite_cb = new QCheckBox(
|
||||
tr("Remplacer les numérotations de même nom"), &dialog);
|
||||
layout->addWidget(overwrite_cb);
|
||||
|
||||
QDialogButtonBox *buttons = new QDialogButtonBox(
|
||||
QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dialog);
|
||||
layout->addWidget(buttons);
|
||||
connect(buttons, &QDialogButtonBox::accepted, &dialog, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, &dialog, &QDialog::reject);
|
||||
|
||||
if (dialog.exec() != QDialog::Accepted) {
|
||||
return;
|
||||
}
|
||||
|
||||
int imported = 0, skipped = 0, conductors = 0;
|
||||
for (int row = 0 ; row < list->count() ; ++row)
|
||||
{
|
||||
QListWidgetItem *item = list->item(row);
|
||||
if (item->checkState() != Qt::Checked) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int category = item->data(Qt::UserRole).toInt();
|
||||
const QString title = item->data(Qt::UserRole + 1).toString();
|
||||
|
||||
bool exists = false;
|
||||
switch (category) {
|
||||
case 0: exists = m_project->conductorAutoNum().contains(title); break;
|
||||
case 1: exists = m_project->elementAutoNum().contains(title); break;
|
||||
default: exists = m_project->folioAutoNum().contains(title); break;
|
||||
}
|
||||
if (exists && !overwrite_cb->isChecked()) {
|
||||
++skipped;
|
||||
continue;
|
||||
}
|
||||
|
||||
const NumerotationContext &nc =
|
||||
contexts.at(item->data(Qt::UserRole + 2).toInt());
|
||||
switch (category) {
|
||||
case 0:
|
||||
m_project->addConductorAutoNum(title, nc);
|
||||
++conductors;
|
||||
break;
|
||||
case 1: m_project->addElementAutoNum(title, nc); break;
|
||||
default: m_project->addFolioAutoNum(title, nc); break;
|
||||
}
|
||||
++imported;
|
||||
}
|
||||
|
||||
readValuesFromProject();
|
||||
if (conductors) {
|
||||
m_project->conductorAutoNumAdded();
|
||||
}
|
||||
|
||||
QMessageBox::information(
|
||||
this, tr("Import terminé"),
|
||||
skipped ? tr("%1 numérotation(s) importée(s), "
|
||||
"%2 conservée(s) telles quelles.")
|
||||
.arg(imported).arg(skipped)
|
||||
: tr("%1 numérotation(s) importée(s).").arg(imported));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ProjectAutoNumConfigPage::removeContextElement
|
||||
Remove from project the current element numerotation context
|
||||
@@ -491,6 +714,10 @@ void ProjectAutoNumConfigPage::removeContextElement()
|
||||
return;
|
||||
m_project->removeElementAutoNum (m_saw_element->contextComboBox()->currentText());
|
||||
m_saw_element->contextComboBox()->removeItem (m_saw_element->contextComboBox()->currentIndex());
|
||||
// removeItem() removes the current selection programmatically but
|
||||
// textActivated() does not react to (by design, see buildConnections()).
|
||||
// Refresh the displayed pattern explicitly so it matches the new selection.
|
||||
updateContextElement(m_saw_element->contextComboBox()->currentText());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -678,6 +905,10 @@ void ProjectAutoNumConfigPage::removeContextConductor()
|
||||
if ( m_saw_conductor->contextComboBox()-> currentText() == tr("Nom de la nouvelle numérotation") ) return;
|
||||
m_project -> removeConductorAutoNum (m_saw_conductor->contextComboBox()-> currentText() );
|
||||
m_saw_conductor->contextComboBox()-> removeItem (m_saw_conductor->contextComboBox()-> currentIndex() );
|
||||
// removeItem() removes the current selection programmatically but
|
||||
// textActivated() does not react to (by design, see buildConnections()).
|
||||
// Refresh the displayed pattern explicitly so it matches the new selection.
|
||||
updateContextConductor(m_saw_conductor->contextComboBox()->currentText());
|
||||
project()->conductorAutoNumRemoved();
|
||||
}
|
||||
|
||||
@@ -691,6 +922,10 @@ void ProjectAutoNumConfigPage::removeContextFolio()
|
||||
if ( m_saw_folio->contextComboBox() -> currentText() == tr("Nom de la nouvelle numérotation") ) return;
|
||||
m_project -> removeFolioAutoNum (m_saw_folio->contextComboBox() -> currentText() );
|
||||
m_saw_folio->contextComboBox() -> removeItem (m_saw_folio->contextComboBox() -> currentIndex() );
|
||||
// removeItem() removes the current selection programmatically but
|
||||
// textActivated() does not react to (by design, see buildConnections()).
|
||||
// Refresh the displayed pattern explicitly so it matches the new selection.
|
||||
updateContextFolio(m_saw_folio->contextComboBox()->currentText());
|
||||
project()->folioAutoNumRemoved();
|
||||
}
|
||||
|
||||
|
||||
@@ -156,6 +156,7 @@ class ProjectAutoNumConfigPage : public ProjectConfigPage {
|
||||
void updateContextElement(const QString&);//element
|
||||
void saveContextElement();
|
||||
void removeContextElement();
|
||||
void importFromProject();
|
||||
|
||||
void applyAutoNum();
|
||||
void applyManagement();
|
||||
@@ -173,6 +174,7 @@ class ProjectAutoNumConfigPage : public ProjectConfigPage {
|
||||
SelectAutonumW *m_saw_element;
|
||||
FolioAutonumberingW *m_faw;
|
||||
AutoNumberingManagementW *m_amw;
|
||||
QPushButton *m_import_pb = nullptr;
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "../../qeticons.h"
|
||||
#include "../../shortcutmanager.h"
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QFrame>
|
||||
#include <QHBoxLayout>
|
||||
#include <QHeaderView>
|
||||
@@ -27,10 +28,33 @@
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QTableWidget>
|
||||
#include <QRegularExpression>
|
||||
#include <QToolButton>
|
||||
#include <QTreeWidget>
|
||||
#include <QTreeWidgetItem>
|
||||
#include <QVBoxLayout>
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
@brief normalizedForSearch
|
||||
Decompose \a text and strip every non-spacing combining mark, then fold the
|
||||
case. Both the query terms and the row's searchable text are run through
|
||||
this, so "general" matches "Général" and "Ctrl+S" matches "ctrl+s"
|
||||
regardless of the keyboard layout the query was typed on.
|
||||
*/
|
||||
static QString normalizedForSearch(const QString &text)
|
||||
{
|
||||
const QString decomposed = text.normalized(QString::NormalizationForm_D);
|
||||
QString stripped;
|
||||
stripped.reserve(decomposed.size());
|
||||
for (const QChar &c : decomposed) {
|
||||
if (c.category() != QChar::Mark_NonSpacing) {
|
||||
stripped.append(c);
|
||||
}
|
||||
}
|
||||
return stripped.toCaseFolded();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ShortcutsConfigPage::ShortcutsConfigPage
|
||||
@@ -51,18 +75,33 @@ ShortcutsConfigPage::ShortcutsConfigPage(QWidget *parent) :
|
||||
m_filter_edit = new QLineEdit(this);
|
||||
m_filter_edit->setPlaceholderText(tr("Filtrer les raccourcis…"));
|
||||
connect(m_filter_edit, &QLineEdit::textChanged, this, &ShortcutsConfigPage::filterRows);
|
||||
vlayout->addWidget(m_filter_edit);
|
||||
|
||||
m_table = new QTableWidget(0, 4, this);
|
||||
m_table->setHorizontalHeaderLabels({tr("Catégorie"), tr("Action"), tr("Raccourci"), QString()});
|
||||
m_table->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents);
|
||||
m_table->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch);
|
||||
m_table->horizontalHeader()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
||||
m_table->horizontalHeader()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
|
||||
m_table->verticalHeader()->setVisible(false);
|
||||
m_table->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_table->setSelectionMode(QAbstractItemView::NoSelection);
|
||||
vlayout->addWidget(m_table);
|
||||
m_quick_filter = new QComboBox(this);
|
||||
m_quick_filter->setObjectName(QStringLiteral("quickFilterCombo"));
|
||||
m_quick_filter->addItem(tr("Tous"));
|
||||
m_quick_filter->addItem(tr("Attribués uniquement"));
|
||||
m_quick_filter->addItem(tr("Non attribués uniquement"));
|
||||
m_quick_filter->addItem(tr("Conflits uniquement"));
|
||||
connect(m_quick_filter, QOverload<int>::of(&QComboBox::currentIndexChanged),
|
||||
this, &ShortcutsConfigPage::quickFilterChanged);
|
||||
|
||||
m_count_label = new QLabel(this);
|
||||
m_count_label->setObjectName(QStringLiteral("shortcutCountLabel"));
|
||||
|
||||
auto *filter_layout = new QHBoxLayout();
|
||||
filter_layout->addWidget(m_filter_edit, 1);
|
||||
filter_layout->addWidget(m_quick_filter);
|
||||
filter_layout->addWidget(m_count_label);
|
||||
vlayout->addLayout(filter_layout);
|
||||
|
||||
m_tree = new QTreeWidget(this);
|
||||
m_tree->setHeaderLabels({tr("Action"), tr("Raccourci"), QString()});
|
||||
m_tree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
|
||||
m_tree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
|
||||
m_tree->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
|
||||
m_tree->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
||||
m_tree->setSelectionMode(QAbstractItemView::NoSelection);
|
||||
vlayout->addWidget(m_tree);
|
||||
|
||||
auto *reset_all_button = new QPushButton(tr("Tout réinitialiser"), this);
|
||||
connect(reset_all_button, &QPushButton::clicked, this, &ShortcutsConfigPage::resetAllRows);
|
||||
@@ -75,6 +114,7 @@ ShortcutsConfigPage::ShortcutsConfigPage(QWidget *parent) :
|
||||
setLayout(vlayout);
|
||||
|
||||
populateTable();
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
ShortcutsConfigPage::~ShortcutsConfigPage()
|
||||
@@ -83,8 +123,8 @@ ShortcutsConfigPage::~ShortcutsConfigPage()
|
||||
|
||||
/**
|
||||
@brief ShortcutsConfigPage::populateTable
|
||||
Fill the table with one row per shortcut known to ShortcutManager, sorted
|
||||
by category then action name.
|
||||
Fill the tree with one collapsible top-level node per category and one child
|
||||
per shortcut known to ShortcutManager, sorted by category then action name.
|
||||
*/
|
||||
void ShortcutsConfigPage::populateTable()
|
||||
{
|
||||
@@ -97,33 +137,38 @@ void ShortcutsConfigPage::populateTable()
|
||||
return a.description < b.description;
|
||||
});
|
||||
|
||||
m_table->setRowCount(shortcuts.size());
|
||||
m_tree->clear();
|
||||
m_rows.clear();
|
||||
m_rows.reserve(shortcuts.size());
|
||||
|
||||
for (int row = 0; row < shortcuts.size(); ++row) {
|
||||
const ShortcutManager::ShortcutInfo &info = shortcuts.at(row);
|
||||
QHash<QString, QTreeWidgetItem *> category_nodes;
|
||||
|
||||
auto *category_item = new QTableWidgetItem(info.category);
|
||||
category_item->setFlags(category_item->flags() & ~Qt::ItemIsEditable);
|
||||
m_table->setItem(row, 0, category_item);
|
||||
for (const ShortcutManager::ShortcutInfo &info : shortcuts) {
|
||||
QTreeWidgetItem *category_item = category_nodes.value(info.category, nullptr);
|
||||
if (!category_item) {
|
||||
category_item = new QTreeWidgetItem(m_tree);
|
||||
category_item->setText(0, info.category);
|
||||
category_item->setFlags(category_item->flags() & ~Qt::ItemIsEditable);
|
||||
category_nodes.insert(info.category, category_item);
|
||||
}
|
||||
|
||||
auto *description_item = new QTableWidgetItem(info.description);
|
||||
description_item->setFlags(description_item->flags() & ~Qt::ItemIsEditable);
|
||||
m_table->setItem(row, 1, description_item);
|
||||
auto *child = new QTreeWidgetItem(category_item);
|
||||
child->setText(0, info.description);
|
||||
child->setFlags(child->flags() & ~Qt::ItemIsEditable);
|
||||
|
||||
auto *edit = new QKeySequenceEdit(info.current_sequence, m_table);
|
||||
auto *edit = new QKeySequenceEdit(info.current_sequence, m_tree);
|
||||
connect(edit, &QKeySequenceEdit::editingFinished, this, &ShortcutsConfigPage::checkConflicts);
|
||||
m_table->setCellWidget(row, 2, edit);
|
||||
m_tree->setItemWidget(child, 1, edit);
|
||||
|
||||
auto *reset_button = new QToolButton(m_table);
|
||||
auto *reset_button = new QToolButton(m_tree);
|
||||
reset_button->setIcon(QET::Icons::EditUndo);
|
||||
reset_button->setToolTip(tr("Réinitialiser ce raccourci"));
|
||||
reset_button->setAutoRaise(true);
|
||||
connect(reset_button, &QToolButton::clicked, this, [this, row]() { resetRow(row); });
|
||||
m_table->setCellWidget(row, 3, reset_button);
|
||||
const int row_index = m_rows.size();
|
||||
connect(reset_button, &QToolButton::clicked, this, [this, row_index]() { resetRow(row_index); });
|
||||
m_tree->setItemWidget(child, 2, reset_button);
|
||||
|
||||
m_rows << Row{info.id, info.default_sequence, edit};
|
||||
m_rows << Row{info.id, info.category, info.description, info.default_sequence, edit, child, false};
|
||||
}
|
||||
|
||||
checkConflicts();
|
||||
@@ -131,17 +176,116 @@ void ShortcutsConfigPage::populateTable()
|
||||
|
||||
/**
|
||||
@brief ShortcutsConfigPage::filterRows
|
||||
Hide every row whose category or action name doesn't contain \a filter_text.
|
||||
Re-run the combined text + quick filter whenever the search box changes.
|
||||
*/
|
||||
void ShortcutsConfigPage::filterRows(const QString &filter_text)
|
||||
{
|
||||
const QString needle = filter_text.trimmed();
|
||||
for (int row = 0; row < m_table->rowCount(); ++row) {
|
||||
const bool matches = needle.isEmpty()
|
||||
|| m_table->item(row, 0)->text().contains(needle, Qt::CaseInsensitive)
|
||||
|| m_table->item(row, 1)->text().contains(needle, Qt::CaseInsensitive);
|
||||
m_table->setRowHidden(row, !matches);
|
||||
Q_UNUSED(filter_text)
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ShortcutsConfigPage::quickFilterChanged
|
||||
Re-run the combined text + quick filter whenever the quick filter changes.
|
||||
*/
|
||||
void ShortcutsConfigPage::quickFilterChanged(int index)
|
||||
{
|
||||
Q_UNUSED(index)
|
||||
applyFilter();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ShortcutsConfigPage::applyFilter
|
||||
Hide every row that doesn't match both the search box and the quick filter.
|
||||
The text query is split on whitespace and every term must match the category,
|
||||
action name or current sequence (accent- and case-insensitively). Matching
|
||||
category nodes are expanded so hits are not hidden inside collapsed groups,
|
||||
and the "N actions" label tracks how many actions remain visible.
|
||||
*/
|
||||
void ShortcutsConfigPage::applyFilter()
|
||||
{
|
||||
const QString needle = m_filter_edit->text().trimmed();
|
||||
const QStringList terms = needle.isEmpty()
|
||||
? QStringList()
|
||||
: needle.split(QRegularExpression(QStringLiteral("\\s+")), Qt::SkipEmptyParts);
|
||||
|
||||
const int quick_filter = m_quick_filter->currentIndex();
|
||||
|
||||
int visible_actions = 0;
|
||||
for (const Row &row : std::as_const(m_rows)) {
|
||||
// Text is matched by substring, but the key sequence is matched exactly:
|
||||
// a substring match would let "Ctrl+S" also hit "Ctrl+Shift+F" (the "S"
|
||||
// of "Shift"), which is precisely the kind of false positive that hides
|
||||
// the one binding the user is looking for.
|
||||
const QString text_haystack = normalizedForSearch(
|
||||
row.category + QLatin1Char(' ') + row.description);
|
||||
const QString sequence_text = normalizedForSearch(row.edit->keySequence().toString());
|
||||
|
||||
bool matches = true;
|
||||
for (const QString &term : terms) {
|
||||
const QString t = normalizedForSearch(term);
|
||||
if (!text_haystack.contains(t) && sequence_text != t) {
|
||||
matches = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (matches) {
|
||||
switch (quick_filter) {
|
||||
case BoundOnly:
|
||||
matches = !row.edit->keySequence().isEmpty();
|
||||
break;
|
||||
case UnboundOnly:
|
||||
matches = row.edit->keySequence().isEmpty();
|
||||
break;
|
||||
case ConflictsOnly:
|
||||
matches = row.conflicted;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
row.item->setHidden(!matches);
|
||||
if (matches) {
|
||||
++visible_actions;
|
||||
}
|
||||
}
|
||||
|
||||
const bool filtering = !needle.isEmpty() || quick_filter != ShowAll;
|
||||
for (int i = 0; i < m_tree->topLevelItemCount(); ++i) {
|
||||
QTreeWidgetItem *top = m_tree->topLevelItem(i);
|
||||
bool any_visible = false;
|
||||
for (int j = 0; j < top->childCount(); ++j) {
|
||||
if (!top->child(j)->isHidden()) {
|
||||
any_visible = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
top->setHidden(!any_visible);
|
||||
top->setExpanded(filtering && any_visible);
|
||||
}
|
||||
|
||||
m_count_label->setText(tr("%n action(s)", nullptr, visible_actions));
|
||||
}
|
||||
|
||||
// IDs use stable context prefixes; translated display categories are not scopes.
|
||||
// Main-window actions are shared by the editors, and depth actions by the
|
||||
// diagram and element editors. Panel-specific shortcuts retain their own scope.
|
||||
static bool shortcutScopesOverlap(const QString &first_id, const QString &second_id)
|
||||
{
|
||||
const QString first = first_id.section(QLatin1Char('.'), 0, 0);
|
||||
const QString second = second_id.section(QLatin1Char('.'), 0, 0);
|
||||
if (first == second || first == QLatin1String("mainwindow")
|
||||
|| second == QLatin1String("mainwindow"))
|
||||
return true;
|
||||
|
||||
const auto has_depth = [](const QString &scope) {
|
||||
return scope == QLatin1String("diagrameditor")
|
||||
|| scope == QLatin1String("elementeditor");
|
||||
};
|
||||
return (first == QLatin1String("depth") && has_depth(second))
|
||||
|| (second == QLatin1String("depth") && has_depth(first));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,27 +304,41 @@ void ShortcutsConfigPage::checkConflicts()
|
||||
}
|
||||
|
||||
for (int row = 0; row < m_rows.size(); ++row) {
|
||||
const Row ¤t_row = m_rows.at(row);
|
||||
Row ¤t_row = m_rows[row];
|
||||
const QString sequence_text = current_row.edit->keySequence().toString();
|
||||
const QList<int> &conflicting_rows = sequence_to_rows.value(sequence_text);
|
||||
const bool conflicted = !sequence_text.isEmpty() && conflicting_rows.size() > 1;
|
||||
QList<int> conflicting_rows;
|
||||
for (int other : sequence_to_rows.value(sequence_text)) {
|
||||
if (other != row && shortcutScopesOverlap(current_row.id, m_rows.at(other).id))
|
||||
conflicting_rows << other;
|
||||
}
|
||||
const bool conflicted = !conflicting_rows.isEmpty();
|
||||
|
||||
current_row.conflicted = conflicted;
|
||||
|
||||
QTableWidgetItem *description_item = m_table->item(row, 1);
|
||||
if (conflicted) {
|
||||
QStringList other_descriptions;
|
||||
for (int other_row : conflicting_rows) {
|
||||
if (other_row != row) {
|
||||
other_descriptions << m_table->item(other_row, 1)->text();
|
||||
other_descriptions << m_rows.at(other_row).description;
|
||||
}
|
||||
}
|
||||
description_item->setBackground(QColor(255, 205, 205));
|
||||
current_row.item->setBackground(0, QColor(255, 205, 205));
|
||||
current_row.edit->setToolTip(
|
||||
tr("Ce raccourci est aussi utilisé par : %1").arg(other_descriptions.join(QStringLiteral(", "))));
|
||||
} else {
|
||||
description_item->setBackground(Qt::NoBrush);
|
||||
current_row.item->setBackground(0, QBrush());
|
||||
current_row.edit->setToolTip(QString());
|
||||
}
|
||||
}
|
||||
|
||||
// A sequence edit can change while a filter is active (search-by-key or the
|
||||
// conflicts-only quick filter); refresh the visible set so the list doesn't
|
||||
// show stale results.
|
||||
const bool filtering = !m_filter_edit->text().trimmed().isEmpty()
|
||||
|| m_quick_filter->currentIndex() != ShowAll;
|
||||
if (filtering) {
|
||||
applyFilter();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,7 +356,7 @@ void ShortcutsConfigPage::resetRow(int row_index)
|
||||
|
||||
void ShortcutsConfigPage::resetAllRows()
|
||||
{
|
||||
for (const Row &row : qAsConst(m_rows)) {
|
||||
for (const Row &row : std::as_const(m_rows)) {
|
||||
row.edit->setKeySequence(row.default_sequence);
|
||||
}
|
||||
checkConflicts();
|
||||
@@ -211,7 +369,7 @@ void ShortcutsConfigPage::resetAllRows()
|
||||
*/
|
||||
void ShortcutsConfigPage::applyConf()
|
||||
{
|
||||
for (const Row &row : qAsConst(m_rows)) {
|
||||
for (const Row &row : std::as_const(m_rows)) {
|
||||
ShortcutManager::instance().setSequence(row.id, row.edit->keySequence());
|
||||
}
|
||||
}
|
||||
@@ -223,5 +381,5 @@ QString ShortcutsConfigPage::title() const
|
||||
|
||||
QIcon ShortcutsConfigPage::icon() const
|
||||
{
|
||||
return QET::Icons::ConfigureToolbars;
|
||||
return QET::Icons::ConfigureShortcuts;
|
||||
}
|
||||
|
||||
@@ -22,9 +22,12 @@
|
||||
|
||||
#include <QKeySequence>
|
||||
|
||||
class QComboBox;
|
||||
class QKeySequenceEdit;
|
||||
class QLabel;
|
||||
class QLineEdit;
|
||||
class QTableWidget;
|
||||
class QTreeWidget;
|
||||
class QTreeWidgetItem;
|
||||
|
||||
/**
|
||||
@brief The ShortcutsConfigPage class
|
||||
@@ -47,21 +50,36 @@ class ShortcutsConfigPage : public ConfigPage
|
||||
|
||||
private slots:
|
||||
void filterRows(const QString &filter_text);
|
||||
void quickFilterChanged(int index);
|
||||
void checkConflicts();
|
||||
void resetAllRows();
|
||||
|
||||
private:
|
||||
enum QuickFilter {
|
||||
ShowAll = 0,
|
||||
BoundOnly,
|
||||
UnboundOnly,
|
||||
ConflictsOnly
|
||||
};
|
||||
|
||||
struct Row {
|
||||
QString id;
|
||||
QString category;
|
||||
QString description;
|
||||
QKeySequence default_sequence;
|
||||
QKeySequenceEdit *edit;
|
||||
QTreeWidgetItem *item;
|
||||
bool conflicted;
|
||||
};
|
||||
|
||||
void populateTable();
|
||||
void applyFilter();
|
||||
void resetRow(int row_index);
|
||||
|
||||
QLineEdit *m_filter_edit;
|
||||
QTableWidget *m_table;
|
||||
QComboBox *m_quick_filter;
|
||||
QLabel *m_count_label;
|
||||
QTreeWidget *m_tree;
|
||||
QList<Row> m_rows;
|
||||
};
|
||||
|
||||
|
||||
@@ -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,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,242 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#include "diagrambgcolorbutton.h"
|
||||
|
||||
#include "../diagram.h"
|
||||
#include "../diagramview.h"
|
||||
#include "../palettegraphicsview.h"
|
||||
#include "../qetdiagrameditor.h"
|
||||
#include "../projectview.h"
|
||||
#include "../qetproject.h"
|
||||
|
||||
#include <QApplication>
|
||||
#include <QColorDialog>
|
||||
#include <QMenu>
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
|
||||
namespace {
|
||||
struct NamedColor { const char *context_name; QColor color; };
|
||||
|
||||
QList<NamedColor> standardColors()
|
||||
{
|
||||
return {
|
||||
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Blanc"), QColor(0xFF, 0xFF, 0xFF)},
|
||||
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Blanc cassé"), QColor(0xFD, 0xFB, 0xF5)},
|
||||
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Gris clair"), QColor(0xE0, 0xE0, 0xE0)},
|
||||
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Gris"), QColor(0x80, 0x80, 0x80)},
|
||||
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Gris foncé"), QColor(0x40, 0x40, 0x40)},
|
||||
{QT_TRANSLATE_NOOP("DiagramBgColorToolButton", "Noir"), QColor(0x00, 0x00, 0x00)},
|
||||
};
|
||||
}
|
||||
|
||||
const int MAX_RECENT = 6;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::DiagramBgColorToolButton
|
||||
@param editor : the diagram editor this button acts on
|
||||
@param parent
|
||||
*/
|
||||
DiagramBgColorToolButton::DiagramBgColorToolButton(QETDiagramEditor *editor, QWidget *parent) :
|
||||
QToolButton(parent),
|
||||
m_editor(editor)
|
||||
{
|
||||
setPopupMode(QToolButton::InstantPopup);
|
||||
setToolTip(tr("Couleur de fond du folio"));
|
||||
setStatusTip(tr("Choisir la couleur de fond du folio",
|
||||
"status bar tip"));
|
||||
|
||||
m_is_system_color = true;
|
||||
m_current = QApplication::palette().color(QPalette::Base);
|
||||
setMenu(new QMenu(this));
|
||||
rebuildMenu();
|
||||
setSwatch(m_current);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::rebuildMenu
|
||||
*/
|
||||
void DiagramBgColorToolButton::rebuildMenu()
|
||||
{
|
||||
QMenu *m = menu();
|
||||
m->clear();
|
||||
|
||||
QAction *sys = m->addAction(tr("Couleur système"));
|
||||
connect(sys, &QAction::triggered, this, &DiagramBgColorToolButton::applySystemColor);
|
||||
m->addSeparator();
|
||||
|
||||
for (const auto &nc : standardColors())
|
||||
{
|
||||
const QColor c = nc.color;
|
||||
QAction *a = m->addAction(swatchIcon(c),
|
||||
tr(nc.context_name));
|
||||
connect(a, &QAction::triggered, this, [this, c]() { applyColor(c); });
|
||||
}
|
||||
|
||||
if (!m_recent.isEmpty())
|
||||
{
|
||||
m->addSeparator();
|
||||
QAction *title = m->addAction(tr("Récemment utilisées"));
|
||||
title->setEnabled(false);
|
||||
for (const QColor &c : std::as_const(m_recent))
|
||||
{
|
||||
QAction *a = m->addAction(swatchIcon(c), c.name());
|
||||
connect(a, &QAction::triggered, this, [this, c]() { applyColor(c); });
|
||||
}
|
||||
}
|
||||
|
||||
m->addSeparator();
|
||||
QAction *other = m->addAction(tr("Autre couleur…"));
|
||||
connect(other, &QAction::triggered, this, &DiagramBgColorToolButton::chooseOtherColor);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::applyColor
|
||||
Set a custom background color on all open diagrams.
|
||||
@param color
|
||||
*/
|
||||
void DiagramBgColorToolButton::applyColor(const QColor &color)
|
||||
{
|
||||
if (!color.isValid()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_is_system_color = false;
|
||||
rememberRecent(color);
|
||||
setSwatch(color);
|
||||
|
||||
PaletteGraphicsView::setCustomBackgroundColor(true);
|
||||
Diagram::background_color = color;
|
||||
|
||||
QETDiagramEditor *editor = m_editor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
for (ProjectView *pv : editor->openedProjects())
|
||||
for (Diagram *d : pv->project()->diagrams())
|
||||
d->update();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::applySystemColor
|
||||
Restore the system-default background and re-enable dark-mode
|
||||
inversion.
|
||||
*/
|
||||
void DiagramBgColorToolButton::applySystemColor()
|
||||
{
|
||||
m_is_system_color = true;
|
||||
m_current = QApplication::palette().color(QPalette::Base);
|
||||
setSwatch(m_current);
|
||||
|
||||
PaletteGraphicsView::setCustomBackgroundColor(false);
|
||||
Diagram::background_color = Qt::white;
|
||||
|
||||
QETDiagramEditor *editor = m_editor;
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
for (ProjectView *pv : editor->openedProjects())
|
||||
for (Diagram *d : pv->project()->diagrams())
|
||||
d->update();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::chooseOtherColor
|
||||
*/
|
||||
void DiagramBgColorToolButton::chooseOtherColor()
|
||||
{
|
||||
const QColor c = QColorDialog::getColor(m_current, this,
|
||||
tr("Choisir une couleur de fond"));
|
||||
if (c.isValid()) {
|
||||
applyColor(c);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::rememberRecent
|
||||
Most recent first, no duplicates, capped.
|
||||
@param color
|
||||
*/
|
||||
void DiagramBgColorToolButton::rememberRecent(const QColor &color)
|
||||
{
|
||||
for (const auto &nc : standardColors()) {
|
||||
if (nc.color == color) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
m_recent.removeAll(color);
|
||||
m_recent.prepend(color);
|
||||
while (m_recent.size() > MAX_RECENT) {
|
||||
m_recent.removeLast();
|
||||
}
|
||||
rebuildMenu();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::setSwatch
|
||||
@param color
|
||||
*/
|
||||
void DiagramBgColorToolButton::setSwatch(const QColor &color)
|
||||
{
|
||||
m_current = color;
|
||||
setIcon(swatchIcon(color));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::syncFromDiagram
|
||||
Sync the button swatch to whatever Diagram::background_color is
|
||||
currently set to. Called when the active folio changes.
|
||||
*/
|
||||
void DiagramBgColorToolButton::syncFromDiagram()
|
||||
{
|
||||
if (m_is_system_color) {
|
||||
m_current = QApplication::palette().color(QPalette::Base);
|
||||
} else {
|
||||
m_current = Diagram::background_color;
|
||||
}
|
||||
setSwatch(m_current);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::updateEnabledState
|
||||
*/
|
||||
void DiagramBgColorToolButton::updateEnabledState()
|
||||
{
|
||||
setEnabled(m_editor && m_editor->currentProjectView());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief DiagramBgColorToolButton::swatchIcon
|
||||
@param color
|
||||
@return a plain square of that colour, outlined so that white and very
|
||||
light colours are still visible against the toolbar.
|
||||
*/
|
||||
QIcon DiagramBgColorToolButton::swatchIcon(const QColor &color)
|
||||
{
|
||||
QPixmap pix(16, 16);
|
||||
pix.fill(Qt::transparent);
|
||||
QPainter p(&pix);
|
||||
p.setRenderHint(QPainter::Antialiasing, false);
|
||||
p.setBrush(color);
|
||||
p.setPen(QPen(QColor(0x40, 0x40, 0x40), 1));
|
||||
p.drawRect(0, 0, 15, 15);
|
||||
p.end();
|
||||
return QIcon(pix);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
Copyright 2006-2026 The QElectroTech Team
|
||||
This file is part of QElectroTech.
|
||||
|
||||
QElectroTech is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 2 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
QElectroTech is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with QElectroTech. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
#ifndef DIAGRAMBGCOLORTOOLBUTTON_H
|
||||
#define DIAGRAMBGCOLORTOOLBUTTON_H
|
||||
|
||||
#include <QColor>
|
||||
#include <QList>
|
||||
#include <QToolButton>
|
||||
|
||||
class QETDiagramEditor;
|
||||
|
||||
/**
|
||||
@brief The DiagramBgColorToolButton class
|
||||
Color picker for the diagram sheet background, placed in the
|
||||
"Affichage" toolbar. Mirrors the ConductorColorToolButton UX:
|
||||
preset colors in the dropdown, "Autre couleur..." at the bottom,
|
||||
and a swatch icon on the button itself.
|
||||
|
||||
Picking "Couleur système" clears any custom colour and lets the
|
||||
dark-mode inversion handle the background as before.
|
||||
*/
|
||||
class DiagramBgColorToolButton : public QToolButton
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DiagramBgColorToolButton(QETDiagramEditor *editor,
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
public slots:
|
||||
void updateEnabledState();
|
||||
void syncFromDiagram();
|
||||
|
||||
private:
|
||||
void rebuildMenu();
|
||||
void applyColor(const QColor &color);
|
||||
void applySystemColor();
|
||||
void chooseOtherColor();
|
||||
void rememberRecent(const QColor &color);
|
||||
void setSwatch(const QColor &color);
|
||||
static QIcon swatchIcon(const QColor &color);
|
||||
|
||||
QETDiagramEditor *m_editor = nullptr;
|
||||
QList<QColor> m_recent;
|
||||
QColor m_current;
|
||||
bool m_is_system_color = false;
|
||||
};
|
||||
|
||||
#endif // DIAGRAMBGCOLORTOOLBUTTON_H
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
@@ -250,8 +251,10 @@ QList<QStandardItem *> DynamicElementTextModel::itemsForText(
|
||||
QStandardItem *color = new QStandardItem(tr("Couleur"));
|
||||
color->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
|
||||
|
||||
// Shown as a swatch next to the value, not as the text color: black
|
||||
// text (the default) is unreadable on a dark palette.
|
||||
QStandardItem *colora = new QStandardItem;
|
||||
colora->setData(deti->color(), Qt::ForegroundRole);
|
||||
colora->setData(deti->color(), Qt::DecorationRole);
|
||||
colora->setData(deti->color(), Qt::EditRole);
|
||||
colora->setData(DynamicElementTextModel::color, Qt::UserRole+1);
|
||||
colora->setFlags(Qt::ItemIsSelectable
|
||||
@@ -1499,7 +1502,7 @@ void DynamicElementTextModel::updateDataFromText(DynamicElementTextItem *deti,
|
||||
case color:
|
||||
{
|
||||
qsi->child(color_txt_row,1)->setData(deti->color(), Qt::EditRole);
|
||||
qsi->child(color_txt_row,1)->setData(deti->color(), Qt::ForegroundRole);
|
||||
qsi->child(color_txt_row,1)->setData(deti->color(), Qt::DecorationRole);
|
||||
break;
|
||||
}
|
||||
case pos:
|
||||
@@ -1595,6 +1598,15 @@ DynamicTextItemDelegate::DynamicTextItemDelegate(QObject *parent) :
|
||||
QStyledItemDelegate(parent)
|
||||
{}
|
||||
|
||||
void DynamicTextItemDelegate::commitAndCloseDeferred(QWidget *editor) const
|
||||
{
|
||||
auto *self = const_cast<DynamicTextItemDelegate *>(this);
|
||||
QTimer::singleShot(0, self, [self, editor]() {
|
||||
emit self->commitData(editor);
|
||||
emit self->closeEditor(editor);
|
||||
});
|
||||
}
|
||||
|
||||
QWidget *DynamicTextItemDelegate::createEditor(
|
||||
QWidget *parent,
|
||||
const QStyleOptionViewItem &option,
|
||||
@@ -1682,13 +1694,43 @@ QWidget *DynamicTextItemDelegate::createEditor(
|
||||
w->setProperty("ok", ok);
|
||||
}
|
||||
w->setObjectName("font_dialog");
|
||||
commitAndCloseDeferred(w);
|
||||
return w;
|
||||
}
|
||||
case DynamicElementTextModel::color:
|
||||
{
|
||||
QColorDialog *cd = new QColorDialog(index.data(Qt::EditRole).value<QColor>(), parent);
|
||||
cd->setObjectName("color_dialog");
|
||||
return cd;
|
||||
/* Like the font case above: run the dialog synchronously via
|
||||
* its static convenience function and stash the result on a
|
||||
* plain placeholder widget, rather than handing back the
|
||||
* QColorDialog itself as the item view's "editor".
|
||||
*
|
||||
* The item view's own Enter/Escape handling (the base
|
||||
* QStyledItemDelegate::eventFilter(), since "color_dialog"
|
||||
* isn't one of the objectNames special-cased in this
|
||||
* delegate's own eventFilter() above) treats whatever
|
||||
* createEditor() returned as a small inline editor it
|
||||
* commits and destroys directly on a key press. A QColorDialog
|
||||
* is not that: on Windows it can hand off to the native
|
||||
* color picker, whose own accept/close path then races the
|
||||
* view's -- pressing Enter fired both, one tearing down an
|
||||
* object the other was still using (case #323 on the bug
|
||||
* tracker: "QObject::installEventFilter(): Cannot filter
|
||||
* events for objects in a different thread" immediately
|
||||
* followed by a segfault; clicking the dialog's own OK
|
||||
* button with the mouse didn't reach the view's key
|
||||
* handling, so it didn't crash). Resolving the dialog
|
||||
* before returning removes the second, competing teardown
|
||||
* path entirely. */
|
||||
QColor color = QColorDialog::getColor(index.data(Qt::EditRole).value<QColor>(), parent);
|
||||
QWidget *w = new QWidget(parent);
|
||||
if (color.isValid())
|
||||
{
|
||||
w->setProperty("color", color);
|
||||
w->setProperty("ok", true);
|
||||
}
|
||||
w->setObjectName("color_dialog");
|
||||
commitAndCloseDeferred(w);
|
||||
return w;
|
||||
}
|
||||
case DynamicElementTextModel::pos:
|
||||
{
|
||||
@@ -1788,15 +1830,15 @@ void DynamicTextItemDelegate::setModelData(
|
||||
{
|
||||
if(QStandardItem *qsi = qsim->itemFromIndex(index))
|
||||
{
|
||||
QColorDialog *cd = static_cast<QColorDialog *> (editor);
|
||||
if (cd->result() == QDialog::Accepted)
|
||||
if (editor->property("ok").toBool() == true)
|
||||
{
|
||||
qsi->setData(cd->selectedColor(), Qt::EditRole);
|
||||
qsi->setData(cd->selectedColor(), Qt::ForegroundRole);
|
||||
QColor color = editor->property("color").value<QColor>();
|
||||
qsi->setData(color, Qt::EditRole);
|
||||
qsi->setData(color, Qt::DecorationRole);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
else if (editor->objectName() == "info_text")
|
||||
|
||||
@@ -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,9 @@
|
||||
#include "elementinfopartwidget.h"
|
||||
|
||||
#include "../SearchAndReplace/searchandreplaceworker.h"
|
||||
#include "../qetinformation.h"
|
||||
#include "ui_elementinfopartwidget.h"
|
||||
#include <QRegularExpressionValidator>
|
||||
|
||||
#include <utility>
|
||||
|
||||
@@ -43,6 +45,14 @@ ElementInfoPartWidget::ElementInfoPartWidget(
|
||||
ui->label_->setText(translated_key);
|
||||
ui->m_erase_text->setVisible(false);
|
||||
|
||||
if (key_ == QETInformation::ELMT_WIDTH || key_ == QETInformation::ELMT_HEIGHT || key_ == QETInformation::ELMT_DEPTH)
|
||||
{
|
||||
auto *validator = new QETInformation::NumericInfoValidator(ui->line_edit);
|
||||
ui->line_edit->setValidator(validator);
|
||||
ui->line_edit->setPlaceholderText(tr("ex. 80.5"));
|
||||
ui->line_edit->setToolTip(tr("Nombre décimal avec un point comme séparateur (ex. 80.5)"));
|
||||
}
|
||||
|
||||
connect(ui->line_edit, &QLineEdit::textEdited,
|
||||
this, &ElementInfoPartWidget::textEdited);
|
||||
connect(ui->line_edit, &QLineEdit::textChanged,
|
||||
@@ -67,6 +77,15 @@ QString ElementInfoPartWidget::text() const
|
||||
return (ui->line_edit->text());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoPartWidget::hasAcceptableInput
|
||||
@return whether the line edit's current text satisfies its validator
|
||||
*/
|
||||
bool ElementInfoPartWidget::hasAcceptableInput() const
|
||||
{
|
||||
return ui->line_edit->hasAcceptableInput();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoPartWidget::setText
|
||||
Set text to line edit
|
||||
|
||||
@@ -41,9 +41,9 @@ class ElementInfoPartWidget : public QWidget
|
||||
QWidget *parent = nullptr);
|
||||
~ElementInfoPartWidget() override;
|
||||
|
||||
QString key () const
|
||||
{return key_;}
|
||||
QString key () const {return key_;}
|
||||
QString text () const;
|
||||
bool hasAcceptableInput() const;
|
||||
void setText (const QString &);
|
||||
void setPlaceHolderText (const QString &text);
|
||||
void setFocusTolineEdit();
|
||||
|
||||
@@ -17,12 +17,14 @@
|
||||
*/
|
||||
#include "elementinfowidget.h"
|
||||
#include <QCheckBox>
|
||||
#include <QPushButton>
|
||||
#include "../diagram.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "../qetinformation.h"
|
||||
#include "../ui_elementinfowidget.h"
|
||||
#include "../undocommand/changeelementinformationcommand.h"
|
||||
#include "customelementinfopartwidget.h"
|
||||
#include "elementinfopartwidget.h"
|
||||
|
||||
/**
|
||||
@@ -47,6 +49,7 @@ ElementInfoWidget::ElementInfoWidget(Element *elmt, QWidget *parent) :
|
||||
ElementInfoWidget::~ElementInfoWidget()
|
||||
{
|
||||
qDeleteAll(m_eipw_list);
|
||||
qDeleteAll(m_custom_eipw_list);
|
||||
delete ui;
|
||||
}
|
||||
|
||||
@@ -145,7 +148,7 @@ bool ElementInfoWidget::event(QEvent *event)
|
||||
{
|
||||
if (event -> type() == QEvent::WindowActivate || event -> type() == QEvent::Show)
|
||||
{
|
||||
QTimer::singleShot(250, this, SLOT(firstActivated()));
|
||||
QTimer::singleShot(250, this, &ElementInfoWidget::firstActivated);
|
||||
m_first_activation = false;
|
||||
}
|
||||
}
|
||||
@@ -201,12 +204,25 @@ void ElementInfoWidget::buildInterface()
|
||||
keys = QETInformation::elementInfoKeys();
|
||||
}
|
||||
|
||||
//"exclude_from_bom" is part of elementInfoKeys() because the project
|
||||
//database builds the element_info table from that list, but it is not
|
||||
//a free-text property: it already has its own check box below. Without
|
||||
//this it also gets a generic edit row, and since translatedInfoKey()
|
||||
//has no entry for it that row carries no label at all - an anonymous
|
||||
//line that currentInfo() then fills with "true"/"false".
|
||||
keys.removeAll(QStringLiteral("exclude_from_bom"));
|
||||
|
||||
for (auto str : keys)
|
||||
{
|
||||
ElementInfoPartWidget *eipw = new ElementInfoPartWidget(str, QETInformation::translatedInfoKey(str), this);
|
||||
ui->scroll_vlayout->addWidget(eipw);
|
||||
m_eipw_list << eipw;
|
||||
}
|
||||
|
||||
m_add_custom_property_btn = new QPushButton(tr("Ajouter une propriété personnalisée"), this);
|
||||
connect(m_add_custom_property_btn, &QPushButton::clicked, this, [this]() { addCustomProperty(); });
|
||||
ui->scroll_vlayout->addWidget(m_add_custom_property_btn);
|
||||
|
||||
ui->scroll_vlayout->addStretch();
|
||||
|
||||
// Existing potential isolating checkbox
|
||||
@@ -235,6 +251,67 @@ void ElementInfoWidget::buildInterface()
|
||||
m_potential_isolating_cb->setVisible(false);
|
||||
}
|
||||
}
|
||||
/**
|
||||
@brief ElementInfoWidget::predefinedKeys
|
||||
@return every key this widget already exposes a dedicated row for,
|
||||
whether through ElementInfoPartWidget (the ~40 ELMT_* keys) or one
|
||||
of the standalone checkboxes. Anything present in the element's
|
||||
informations but absent from this list is a user-defined custom
|
||||
property.
|
||||
*/
|
||||
QStringList ElementInfoWidget::predefinedKeys() const
|
||||
{
|
||||
QStringList keys = (m_element.data()->elementData().m_type == ElementData::Terminal)
|
||||
? QETInformation::terminalElementInfoKeys()
|
||||
: QETInformation::elementInfoKeys();
|
||||
|
||||
keys << QStringLiteral("auto_num_locked")
|
||||
<< QStringLiteral("potential_isolating")
|
||||
<< QStringLiteral("exclude_from_bom");
|
||||
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoWidget::addCustomProperty
|
||||
Append a new user-defined key/value row to the widget.
|
||||
@param key initial key, left empty for a freshly added row
|
||||
@param value initial value
|
||||
*/
|
||||
void ElementInfoWidget::addCustomProperty(const QString &key, const QString &value)
|
||||
{
|
||||
auto *widget = new CustomElementInfoPartWidget(key, value, this);
|
||||
|
||||
const int insert_index = ui->scroll_vlayout->indexOf(m_add_custom_property_btn);
|
||||
ui->scroll_vlayout->insertWidget(insert_index >= 0 ? insert_index : ui->scroll_vlayout->count(), widget);
|
||||
m_custom_eipw_list << widget;
|
||||
|
||||
connect(widget, &CustomElementInfoPartWidget::removeRequested, this, &ElementInfoWidget::removeCustomProperty);
|
||||
connect(widget, &CustomElementInfoPartWidget::changed, this, [this]() {
|
||||
if (m_live_edit) apply();
|
||||
});
|
||||
|
||||
if (key.isEmpty()) {
|
||||
widget->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoWidget::removeCustomProperty
|
||||
Remove a user-defined key/value row.
|
||||
@param widget the row to remove
|
||||
*/
|
||||
void ElementInfoWidget::removeCustomProperty(CustomElementInfoPartWidget *widget)
|
||||
{
|
||||
if (!m_custom_eipw_list.removeOne(widget))
|
||||
return;
|
||||
|
||||
ui->scroll_vlayout->removeWidget(widget);
|
||||
widget->deleteLater();
|
||||
|
||||
if (m_live_edit) apply();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoWidget::infoPartWidgetForKey
|
||||
@param key
|
||||
@@ -271,6 +348,21 @@ void ElementInfoWidget::updateUi()
|
||||
for (ElementInfoPartWidget *eipw : m_eipw_list) {
|
||||
eipw -> setText (element_info[eipw->key()].toString());
|
||||
}
|
||||
|
||||
// Rebuild the custom-property rows to match whatever
|
||||
// user-defined keys this element currently carries.
|
||||
while (!m_custom_eipw_list.isEmpty()) {
|
||||
CustomElementInfoPartWidget *w = m_custom_eipw_list.takeLast();
|
||||
ui->scroll_vlayout->removeWidget(w);
|
||||
delete w;
|
||||
}
|
||||
const auto known_keys = predefinedKeys();
|
||||
for (const QString &key : element_info.keys()) {
|
||||
if (!known_keys.contains(key)) {
|
||||
addCustomProperty(key, element_info[key].toString());
|
||||
}
|
||||
}
|
||||
|
||||
// Load the lock status for auto numbering
|
||||
if (m_element->elementData().m_type == ElementData::Terminal) {
|
||||
QString lock_value = element_info.value(QStringLiteral("auto_num_locked")).toString();
|
||||
@@ -303,6 +395,9 @@ DiagramContext ElementInfoWidget::currentInfo() const
|
||||
|
||||
for (const auto &eipw : std::as_const(m_eipw_list))
|
||||
{
|
||||
if (!eipw->hasAcceptableInput())
|
||||
continue;
|
||||
|
||||
//add value only if they're something to store
|
||||
if (!eipw->text().isEmpty())
|
||||
{
|
||||
@@ -314,6 +409,17 @@ DiagramContext ElementInfoWidget::currentInfo() const
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &custom : std::as_const(m_custom_eipw_list))
|
||||
{
|
||||
if (custom->hasValidKey() && !custom->value().isEmpty())
|
||||
{
|
||||
QString txt{custom->value()};
|
||||
txt.remove(QStringLiteral("\r"));
|
||||
txt.remove(QStringLiteral("\n"));
|
||||
info_.addValue(custom->key(), txt);
|
||||
}
|
||||
}
|
||||
|
||||
// Save the auto numbering lock status
|
||||
if (m_element->elementData().m_type == ElementData::Terminal) {
|
||||
info_.addValue(QStringLiteral("auto_num_locked"), ui->m_auto_num_locked_cb->isChecked() ? QStringLiteral("true") : QStringLiteral("false"));
|
||||
|
||||
@@ -26,8 +26,10 @@
|
||||
class Element;
|
||||
class QUndoCommand;
|
||||
class ElementInfoPartWidget;
|
||||
class CustomElementInfoPartWidget;
|
||||
class ChangeElementInformationCommand;
|
||||
class QCheckBox;
|
||||
class QPushButton;
|
||||
|
||||
namespace Ui {
|
||||
class ElementInfoWidget;
|
||||
@@ -63,15 +65,20 @@ class ElementInfoWidget : public AbstractElementPropertiesEditorWidget
|
||||
private:
|
||||
void buildInterface();
|
||||
ElementInfoPartWidget *infoPartWidgetForKey(const QString &key) const;
|
||||
QStringList predefinedKeys() const;
|
||||
|
||||
private slots:
|
||||
void firstActivated();
|
||||
void elementInfoChange();
|
||||
void addCustomProperty(const QString &key = QString(), const QString &value = QString());
|
||||
void removeCustomProperty(CustomElementInfoPartWidget *widget);
|
||||
|
||||
//ATTRIBUTES
|
||||
private:
|
||||
Ui::ElementInfoWidget *ui;
|
||||
QList <ElementInfoPartWidget *> m_eipw_list;
|
||||
QList <CustomElementInfoPartWidget *> m_custom_eipw_list;
|
||||
QPushButton *m_add_custom_property_btn = nullptr;
|
||||
QCheckBox *m_potential_isolating_cb = nullptr;
|
||||
QCheckBox *m_exclude_from_bom_cb = nullptr;
|
||||
bool m_first_activation;
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "../qetgraphicsitem/dynamicelementtextitem.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "../qetgraphicsitem/elementtextitemgroup.h"
|
||||
#include "../qetgraphicsitem/masterelement.h"
|
||||
#include "../qeticons.h"
|
||||
#include "dynamicelementtextitemeditor.h"
|
||||
#include "elementinfowidget.h"
|
||||
@@ -30,6 +31,7 @@
|
||||
#include "masterpropertieswidget.h"
|
||||
#include "plclinkwidget.h"
|
||||
|
||||
#include <QCheckBox>
|
||||
#include <QLabel>
|
||||
#include <QUndoStack>
|
||||
#include <QVBoxLayout>
|
||||
@@ -379,9 +381,48 @@ QWidget *ElementPropertiesWidget::generalWidget()
|
||||
description_string += QString(tr("Rotation : %1°\n")).arg(m_element.data()->rotation());
|
||||
description_string += QString(tr("Dimensions : %1*%2\n")).arg(m_element -> size().width()).arg(m_element -> size().height());
|
||||
description_string += QString(tr("Bornes : %1\n")).arg(m_element -> terminals().count());
|
||||
if (m_element->linkType() == Element::Master){
|
||||
description_string += QString(tr("Nombre maximum de contacts esclaves définis : %1\n")).arg(m_element -> elementData().m_max_slaves);
|
||||
description_string += QString(tr("Nombre de contacts esclaves utilisés : %1\n")).arg(m_element ->linkedElements().count());
|
||||
if (m_element->linkType() == Element::Master)
|
||||
{
|
||||
//The declared limit is optional: -1 means the element sets no
|
||||
//limit at all, which is worth saying rather than printing "-1".
|
||||
const int max_slaves = m_element->elementData().m_max_slaves;
|
||||
description_string += max_slaves == -1
|
||||
? QString(tr("Nombre maximum de contacts esclaves définis : non défini\n"))
|
||||
: QString(tr("Nombre maximum de contacts esclaves définis : %1\n")).arg(max_slaves);
|
||||
|
||||
//Left as a count of linked elements: the line above is a number
|
||||
//of slots, and a slave fills one slot however many contacts it
|
||||
//carries, so the two stay in the same unit.
|
||||
description_string += QString(tr("Nombre de contacts esclaves utilisés : %1\n")).arg(m_element->linkedElements().count());
|
||||
|
||||
//The breakdown below is in contacts, not slots: it answers how
|
||||
//many contacts an auxiliary block must provide.
|
||||
const MasterElement *master =
|
||||
static_cast<const MasterElement *>(m_element.data());
|
||||
const ContactUsage usage = master->contactUsage();
|
||||
const ContactUsage capacity = master->contactCapacity();
|
||||
|
||||
if (capacity.total() > 0)
|
||||
{
|
||||
//The element declares contact groups, so it can say not
|
||||
//only what has been used but what it has to offer. A type
|
||||
//used beyond what is declared shows as e.g. "1/0", which
|
||||
//is the point: it says this contact does not fit the part.
|
||||
description_string += QString(tr(" Contacts : NO : %1/%2, NC : %3/%4, inverseurs : %5/%6, autres : %7/%8\n"))
|
||||
.arg(usage.no).arg(capacity.no)
|
||||
.arg(usage.nc).arg(capacity.nc)
|
||||
.arg(usage.sw).arg(capacity.sw)
|
||||
.arg(usage.other).arg(capacity.other);
|
||||
}
|
||||
else if (usage.total() > 0)
|
||||
{
|
||||
//No declared groups, so a plain count of what is in use.
|
||||
description_string += QString(tr(" Contacts : NO : %1, NC : %2, inverseurs : %3, autres : %4\n"))
|
||||
.arg(usage.no)
|
||||
.arg(usage.nc)
|
||||
.arg(usage.sw)
|
||||
.arg(usage.other);
|
||||
}
|
||||
}
|
||||
description_string += QString(tr("Emplacement : %1\n")).arg(m_element.data()->location().toString());
|
||||
|
||||
@@ -396,6 +437,18 @@ QWidget *ElementPropertiesWidget::generalWidget()
|
||||
label->setTextInteractionFlags(Qt::TextEditorInteraction);
|
||||
vlayout_->addWidget(label);
|
||||
|
||||
//checkbox to lock the element position on the diagram
|
||||
//(same mechanism already used by images and drawn shapes)
|
||||
QCheckBox *lock_pos_cb = new QCheckBox(tr("Verrouiller la position"), general_widget);
|
||||
lock_pos_cb->setChecked(!m_element->isMovable());
|
||||
QPointer<Element> element = m_element;
|
||||
connect(lock_pos_cb, &QCheckBox::clicked, this, [element](bool checked) {
|
||||
if (element) {
|
||||
element->setMovable(!checked);
|
||||
}
|
||||
});
|
||||
vlayout_->addWidget(lock_pos_cb);
|
||||
|
||||
//widget for the pixmap
|
||||
QLabel *pix = new QLabel(general_widget);
|
||||
vlayout_->addWidget(pix, 0, Qt::AlignHCenter);
|
||||
@@ -403,9 +456,9 @@ QWidget *ElementPropertiesWidget::generalWidget()
|
||||
|
||||
//button widget
|
||||
QPushButton *find_in_panel = new QPushButton(QET::Icons::ZoomDraw, tr("Retrouver dans le panel"), general_widget);
|
||||
connect(find_in_panel, SIGNAL(clicked()), this, SLOT(findInPanel()));
|
||||
connect(find_in_panel, &QPushButton::clicked, this, &ElementPropertiesWidget::findInPanel);
|
||||
QPushButton *edit_element = new QPushButton(QET::Icons::ElementEdit, tr("Éditer l'élément"), general_widget);
|
||||
connect(edit_element, SIGNAL(clicked()), this, SLOT(editElement()));
|
||||
connect(edit_element, &QPushButton::clicked, this, &ElementPropertiesWidget::editElement);
|
||||
QHBoxLayout *hlayout_ = new QHBoxLayout;
|
||||
hlayout_->addWidget(find_in_panel);
|
||||
hlayout_->addWidget(edit_element);
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
*/
|
||||
#include "linksingleelementwidget.h"
|
||||
#include "contactgroupselectiondialog.h"
|
||||
#include "../qetgraphicsitem/masterelement.h"
|
||||
#include "../qetgraphicsitem/conductor.h"
|
||||
#include "../diagram.h"
|
||||
#include "../diagramposition.h"
|
||||
@@ -26,10 +25,14 @@
|
||||
#include "../undocommand/linkelementcommand.h"
|
||||
#include "../qetinformation.h"
|
||||
#include "../qetproject.h"
|
||||
#include "../qetgraphicsitem/masterelement.h"
|
||||
#include "../ui_linksingleelementwidget.h"
|
||||
|
||||
#include <QTreeWidgetItem>
|
||||
#include <QInputDialog>
|
||||
#include <QCheckBox>
|
||||
#include <QSettings>
|
||||
#include <QSignalBlocker>
|
||||
|
||||
|
||||
/**
|
||||
@@ -53,6 +56,22 @@ LinkSingleElementWidget::LinkSingleElementWidget(Element *elmt,
|
||||
m_show_qtwi = new QAction(tr("Montrer l'élément"), this);
|
||||
m_show_element = new QAction(tr("Montrer l'élément esclave"), this);
|
||||
m_save_header_state = new QAction(tr("Enregistrer la disposition"), this);
|
||||
|
||||
// Hide the full-masters checkbox for non-slave elements
|
||||
const bool is_slave = (elmt && elmt->elementData().m_type == ElementData::Slave);
|
||||
ui->m_hide_full_masters_cb->setVisible(is_slave);
|
||||
|
||||
// Restore persisted state before connecting to avoid toggled firing buildTree()
|
||||
// while m_element is still null.
|
||||
{
|
||||
const QSignalBlocker blocker(ui->m_hide_full_masters_cb);
|
||||
QSettings settings;
|
||||
ui->m_hide_full_masters_cb->setChecked(
|
||||
settings.value(QStringLiteral("link-element-widget/hideFullMasters"), false).toBool());
|
||||
}
|
||||
|
||||
connect(ui->m_hide_full_masters_cb, &QCheckBox::toggled,
|
||||
this, &LinkSingleElementWidget::hideFullMastersToggled);
|
||||
|
||||
connect(m_show_qtwi, &QAction::triggered, this, [=]()
|
||||
{
|
||||
@@ -250,6 +269,7 @@ void LinkSingleElementWidget::buildTree()
|
||||
|
||||
if (m_element->elementData().m_type == ElementData::Slave)
|
||||
{
|
||||
m_full_masters.clear();
|
||||
|
||||
for(const auto &elmt : elmt_vector)
|
||||
{
|
||||
@@ -289,6 +309,16 @@ void LinkSingleElementWidget::buildTree()
|
||||
QTreeWidgetItem *qtwi = new QTreeWidgetItem(ui->m_tree_widget, str_list);
|
||||
m_qtwi_elmt_hash.insert(qtwi, elmt);
|
||||
m_qtwi_strl_hash.insert(qtwi, search_list);
|
||||
|
||||
// Check if this master is full
|
||||
if (elmt->linkType() == Element::Master) {
|
||||
MasterElement *me = qobject_cast<MasterElement*>(elmt);
|
||||
if (me && me->isFull()) {
|
||||
m_full_masters.insert(qtwi);
|
||||
if (ui->m_hide_full_masters_cb->isChecked())
|
||||
qtwi->setHidden(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -353,8 +383,16 @@ void LinkSingleElementWidget::buildTree()
|
||||
|
||||
QSettings settings;
|
||||
QVariant v = settings.value(QStringLiteral("link-element-widget/report-state"));
|
||||
if(!v.isNull())
|
||||
ui->m_tree_widget->header()->restoreState(v.toByteArray());
|
||||
auto *header = ui->m_tree_widget->header();
|
||||
if (v.isNull() || !header->restoreState(v.toByteArray()))
|
||||
{
|
||||
// Keep logical column IDs stable for saved layouts, but show the
|
||||
// folio identity first even when the candidate has no conductor.
|
||||
for (int column = 5; column < 8; ++column)
|
||||
header->moveSection(header->visualIndex(column), column - 5);
|
||||
ui->m_tree_widget->resizeColumnToContents(5);
|
||||
ui->m_tree_widget->resizeColumnToContents(6);
|
||||
}
|
||||
}
|
||||
|
||||
setUpCompleter();
|
||||
@@ -410,11 +448,12 @@ QVector <QPointer<Element>> LinkSingleElementWidget::availableElements()
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the master is full, we'll remove it from the list!
|
||||
MasterElement *master = static_cast<MasterElement*>(elmt);
|
||||
if (master->isFull()) {
|
||||
elmt_vector.removeAt(i);
|
||||
}
|
||||
// A master at its declared limit stays in the list. Removing
|
||||
// it made a full master indistinguishable from one that does
|
||||
// not exist: the candidate simply was not there, with nothing
|
||||
// to say why. The limit is advisory -- see the prompt in
|
||||
// MasterPropertiesWidget::on_link_button_clicked() -- so the
|
||||
// user decides, rather than the list deciding for them.
|
||||
}
|
||||
}
|
||||
return elmt_vector;
|
||||
@@ -447,6 +486,8 @@ void LinkSingleElementWidget::setUpCompleter()
|
||||
*/
|
||||
void LinkSingleElementWidget::clearTreeWidget()
|
||||
{
|
||||
m_pending_qtwi = nullptr;
|
||||
|
||||
while(ui->m_tree_widget->topLevelItemCount())
|
||||
{
|
||||
QTreeWidgetItem *qtwi = ui->m_tree_widget->takeTopLevelItem(0);
|
||||
@@ -459,6 +500,7 @@ void LinkSingleElementWidget::clearTreeWidget()
|
||||
|
||||
m_qtwi_elmt_hash.clear();
|
||||
m_qtwi_strl_hash.clear();
|
||||
m_full_masters.clear();
|
||||
}
|
||||
|
||||
void LinkSingleElementWidget::setUpHeaderLabels()
|
||||
@@ -528,7 +570,7 @@ void LinkSingleElementWidget::diagramWasRemovedFromProject()
|
||||
// contains the master element linked to the edited element
|
||||
// we must wait for this elements to be unlinked,
|
||||
// or else the list of available master isn't up to date
|
||||
QTimer::singleShot(10, this, SLOT(updateUi()));
|
||||
QTimer::singleShot(10, this, &LinkSingleElementWidget::updateUi);
|
||||
}
|
||||
|
||||
void LinkSingleElementWidget::showedElementWasDeleted()
|
||||
@@ -667,6 +709,7 @@ void LinkSingleElementWidget::hideButtons()
|
||||
ui->m_show_linked_pb->hide();
|
||||
ui->m_show_this_pb->hide();
|
||||
ui->m_search_field->show();
|
||||
ui->m_hide_full_masters_cb->show();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -680,6 +723,7 @@ void LinkSingleElementWidget::showButtons()
|
||||
ui->m_show_linked_pb->show();
|
||||
ui->m_show_this_pb->show();
|
||||
ui->m_search_field->hide();
|
||||
ui->m_hide_full_masters_cb->hide();
|
||||
}
|
||||
|
||||
void LinkSingleElementWidget::headerCustomContextMenuRequested(
|
||||
@@ -717,8 +761,7 @@ void LinkSingleElementWidget::on_m_tree_widget_itemDoubleClicked(
|
||||
|
||||
if (m_showed_element)
|
||||
{
|
||||
disconnect(m_showed_element, SIGNAL(destroyed()),
|
||||
this, SLOT(showedElementWasDeleted()));
|
||||
disconnect(m_showed_element, &QObject::destroyed, this, &LinkSingleElementWidget::showedElementWasDeleted);
|
||||
m_showed_element->setHighlighted(false);
|
||||
}
|
||||
|
||||
@@ -726,8 +769,7 @@ void LinkSingleElementWidget::on_m_tree_widget_itemDoubleClicked(
|
||||
elmt->diagram()->showMe();
|
||||
elmt->setHighlighted(true);
|
||||
m_showed_element = elmt;
|
||||
connect(m_showed_element, SIGNAL(destroyed()),
|
||||
this, SLOT(showedElementWasDeleted()));
|
||||
connect(m_showed_element, &QObject::destroyed, this, &LinkSingleElementWidget::showedElementWasDeleted);
|
||||
|
||||
}
|
||||
|
||||
@@ -777,27 +819,35 @@ void LinkSingleElementWidget::on_m_show_this_pb_clicked()
|
||||
If arg1 is empty, show all items.
|
||||
@param arg1
|
||||
*/
|
||||
void LinkSingleElementWidget::on_m_search_field_textEdited(const QString &arg1)
|
||||
void LinkSingleElementWidget::on_m_search_field_textEdited(const QString &)
|
||||
{
|
||||
//Show all items if arg1 is empty, if not hide all items
|
||||
foreach(QTreeWidgetItem *qtwi, m_qtwi_elmt_hash.keys())
|
||||
qtwi->setHidden(!arg1.isEmpty());
|
||||
|
||||
QList <QTreeWidgetItem *> qtwi_list;
|
||||
|
||||
foreach(QTreeWidgetItem *qtwi, m_qtwi_strl_hash.keys())
|
||||
updateItemsVisibility();
|
||||
}
|
||||
|
||||
void LinkSingleElementWidget::hideFullMastersToggled(bool checked)
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue(QStringLiteral("link-element-widget/hideFullMasters"), checked);
|
||||
updateItemsVisibility();
|
||||
}
|
||||
|
||||
void LinkSingleElementWidget::updateItemsVisibility()
|
||||
{
|
||||
const QString text = ui->m_search_field->text();
|
||||
const bool hide_full = ui->m_hide_full_masters_cb->isChecked();
|
||||
|
||||
for (auto it = m_qtwi_strl_hash.cbegin(); it != m_qtwi_strl_hash.cend(); ++it)
|
||||
{
|
||||
foreach(QString str, m_qtwi_strl_hash.value(qtwi))
|
||||
{
|
||||
if(str.contains(arg1, Qt::CaseInsensitive))
|
||||
{
|
||||
qtwi_list << qtwi;
|
||||
continue;
|
||||
bool match = text.isEmpty();
|
||||
if (!match) {
|
||||
for (const QString &str : it.value()) {
|
||||
if (str.contains(text, Qt::CaseInsensitive)) {
|
||||
match = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
it.key()->setHidden(!match
|
||||
|| (hide_full && m_full_masters.contains(it.key())));
|
||||
}
|
||||
|
||||
//Show items which match with arg1
|
||||
foreach(QTreeWidgetItem *qtwi, qtwi_list)
|
||||
qtwi->setHidden(false);
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
#include <QComboBox>
|
||||
|
||||
class QTreeWidgetItem;
|
||||
class QCheckBox;
|
||||
class Element;
|
||||
class QMenu;
|
||||
|
||||
@@ -71,6 +72,7 @@ class LinkSingleElementWidget : public AbstractElementPropertiesEditorWidget
|
||||
void setUpCompleter();
|
||||
void clearTreeWidget();
|
||||
void setUpHeaderLabels();
|
||||
void updateItemsVisibility();
|
||||
|
||||
private slots:
|
||||
void diagramWasRemovedFromProject();
|
||||
@@ -87,6 +89,7 @@ class LinkSingleElementWidget : public AbstractElementPropertiesEditorWidget
|
||||
void on_m_show_this_pb_clicked();
|
||||
|
||||
void on_m_search_field_textEdited(const QString &arg1);
|
||||
void hideFullMastersToggled(bool checked);
|
||||
|
||||
private:
|
||||
Ui::LinkSingleElementWidget *ui;
|
||||
@@ -103,6 +106,8 @@ class LinkSingleElementWidget : public AbstractElementPropertiesEditorWidget
|
||||
Element *m_showed_element = nullptr,
|
||||
*m_element_to_link = nullptr;
|
||||
|
||||
QSet<QTreeWidgetItem*> m_full_masters;
|
||||
|
||||
int m_pending_group_index = -1;
|
||||
|
||||
QMenu *m_context_menu{nullptr};
|
||||
|
||||
@@ -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/>
|
||||
|
||||
@@ -154,14 +154,12 @@ void MasterPropertiesWidget::setElement(Element *element)
|
||||
m_element->setHighlighted(false);
|
||||
|
||||
if (m_project)
|
||||
disconnect(m_project, SIGNAL(diagramRemoved(QETProject*,Diagram*)),
|
||||
this, SLOT(diagramWasdeletedFromProject()));
|
||||
disconnect(m_project, &QETProject::diagramRemoved, this, &MasterPropertiesWidget::diagramWasdeletedFromProject);
|
||||
|
||||
if(Q_LIKELY(element->diagram() && element->diagram()->project()))
|
||||
{
|
||||
m_project = element->diagram()->project();
|
||||
connect(m_project, SIGNAL(diagramRemoved(QETProject*,Diagram*)),
|
||||
this, SLOT(diagramWasdeletedFromProject()));
|
||||
connect(m_project, &QETProject::diagramRemoved, this, &MasterPropertiesWidget::diagramWasdeletedFromProject);
|
||||
}
|
||||
else
|
||||
m_project = nullptr;
|
||||
@@ -309,15 +307,24 @@ void MasterPropertiesWidget::on_link_button_clicked()
|
||||
int max_slaves = max_slaves_variant.toInt();
|
||||
int current_slaves = ui->m_link_tree_widget->topLevelItemCount();
|
||||
|
||||
// If a limit is set and reached
|
||||
// If a limit is set and reached, say so but let the user decide.
|
||||
// The limit records how many contacts the part is expected to
|
||||
// carry; it is not a rule the drawing has to obey, and refusing
|
||||
// the link obstructs drawing a schematic before the hardware has
|
||||
// been chosen.
|
||||
if (max_slaves != -1 && current_slaves >= max_slaves) {
|
||||
|
||||
|
||||
// Show a message box with the actual window as parent to ensure it's on top
|
||||
QMessageBox::warning(this->window(),
|
||||
tr("Nombre maximal d'esclaves atteint."),
|
||||
tr("Cet élément maître ne peut plus accepter aucun nouveau contact esclave, la limite fixée a été atteinte (Limite: %1).").arg(max_slaves));
|
||||
return;
|
||||
const auto answer = QMessageBox::warning(
|
||||
this->window(),
|
||||
tr("Nombre maximal d'esclaves atteint."),
|
||||
tr("La limite fixée pour cet élément maître est atteinte (Limite: %1).\n\n"
|
||||
"Voulez-vous tout de même lier ce contact esclave ?").arg(max_slaves),
|
||||
QMessageBox::Yes | QMessageBox::No,
|
||||
QMessageBox::Yes);
|
||||
if (answer != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,8 +404,7 @@ void MasterPropertiesWidget::showElementFromTWI(QTreeWidgetItem *qtwi, int colum
|
||||
Q_UNUSED(column);
|
||||
if (m_showed_element)
|
||||
{
|
||||
disconnect(m_showed_element, SIGNAL(destroyed()),
|
||||
this, SLOT(showedElementWasDeleted()));
|
||||
disconnect(m_showed_element, &QObject::destroyed, this, &MasterPropertiesWidget::showedElementWasDeleted);
|
||||
m_showed_element -> setHighlighted(false);
|
||||
}
|
||||
if (m_element)
|
||||
@@ -407,8 +413,7 @@ void MasterPropertiesWidget::showElementFromTWI(QTreeWidgetItem *qtwi, int colum
|
||||
m_showed_element = m_qtwi_hash[qtwi];
|
||||
m_showed_element->diagram()->showMe();
|
||||
m_showed_element->setHighlighted(true);
|
||||
connect(m_showed_element, SIGNAL(destroyed()),
|
||||
this, SLOT(showedElementWasDeleted()));
|
||||
connect(m_showed_element, &QObject::destroyed, this, &MasterPropertiesWidget::showedElementWasDeleted);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -431,7 +436,7 @@ void MasterPropertiesWidget::diagramWasdeletedFromProject()
|
||||
// contains slave element linked to the edited element
|
||||
// we must wait for this elements be unlinked,
|
||||
// or else the linked list provides deleted elements.
|
||||
QTimer::singleShot(10, this, SLOT(updateUi()));
|
||||
QTimer::singleShot(10, this, &MasterPropertiesWidget::updateUi);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -529,13 +534,13 @@ void MasterPropertiesWidget::updateUi()
|
||||
|
||||
// Table
|
||||
m_plc_table = new QTableWidget(m_plc_widget);
|
||||
m_plc_table->setColumnCount(5);
|
||||
m_plc_table->setColumnCount(6);
|
||||
m_plc_table->setHorizontalHeaderLabels({
|
||||
tr("Type"), tr("Adresse"), tr("Fonction"),
|
||||
tr("Commentaire"), tr("Réf. croisée")
|
||||
tr("Commentaire"), tr("Réf. croisée"), tr("Bornes")
|
||||
});
|
||||
m_plc_table->horizontalHeader()->setSectionResizeMode(QHeaderView::Stretch);
|
||||
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectRows);
|
||||
m_plc_table->setSelectionBehavior(QAbstractItemView::SelectItems);
|
||||
m_plc_table->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
m_plc_table->setMinimumHeight(200);
|
||||
|
||||
@@ -596,6 +601,11 @@ void MasterPropertiesWidget::updateUi()
|
||||
auto *crossref_item = new QTableWidgetItem(io.crossRef);
|
||||
crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable);
|
||||
m_plc_table->setItem(row, 4, crossref_item);
|
||||
|
||||
// Anschlüsse (read-only)
|
||||
auto *terminals_item = new QTableWidgetItem(io.terminals.join(QStringLiteral(", ")));
|
||||
terminals_item->setFlags(terminals_item->flags() & ~Qt::ItemIsEditable);
|
||||
m_plc_table->setItem(row, 5, terminals_item);
|
||||
}
|
||||
|
||||
m_plc_table->blockSignals(false);
|
||||
@@ -810,23 +820,7 @@ void MasterPropertiesWidget::setCellFromValue(int row, int col, const QString &v
|
||||
}
|
||||
}
|
||||
else if (col == 5) {
|
||||
// Terminal count spinbox
|
||||
auto *tc_sb = qobject_cast<QSpinBox*>(m_plc_table->cellWidget(row, col));
|
||||
if (!tc_sb) {
|
||||
tc_sb = new QSpinBox(m_plc_table);
|
||||
tc_sb->setMinimum(1);
|
||||
tc_sb->setMaximum(4);
|
||||
m_plc_table->setCellWidget(row, col, tc_sb);
|
||||
connect(tc_sb, QOverload<int>::of(&QSpinBox::valueChanged),
|
||||
this, [this, row](int) { plcIOCellChanged(row, 5); });
|
||||
}
|
||||
bool ok;
|
||||
int v = val.toInt(&ok);
|
||||
if (ok && v >= 1 && v <= 4)
|
||||
tc_sb->setValue(v);
|
||||
}
|
||||
else if (col == 6) {
|
||||
// CrossRef - read-only
|
||||
// Anschlüsse (read-only)
|
||||
auto *item = new QTableWidgetItem(val);
|
||||
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
|
||||
m_plc_table->setItem(row, col, item);
|
||||
@@ -864,17 +858,10 @@ void MasterPropertiesWidget::plcAddRow()
|
||||
m_plc_table->setItem(row, 3, new QTableWidgetItem());
|
||||
m_plc_table->setItem(row, 4, new QTableWidgetItem());
|
||||
|
||||
auto *tc_sb = new QSpinBox(m_plc_table);
|
||||
tc_sb->setMinimum(1);
|
||||
tc_sb->setMaximum(4);
|
||||
tc_sb->setValue(1);
|
||||
m_plc_table->setCellWidget(row, 5, tc_sb);
|
||||
connect(tc_sb, QOverload<int>::of(&QSpinBox::valueChanged),
|
||||
this, [this, row](int) { plcIOCellChanged(row, 5); });
|
||||
|
||||
auto *crossref_item = new QTableWidgetItem();
|
||||
crossref_item->setFlags(crossref_item->flags() & ~Qt::ItemIsEditable);
|
||||
m_plc_table->setItem(row, 6, crossref_item);
|
||||
// Anschlüsse (read-only)
|
||||
auto *terminals_item = new QTableWidgetItem();
|
||||
terminals_item->setFlags(terminals_item->flags() & ~Qt::ItemIsEditable);
|
||||
m_plc_table->setItem(row, 5, terminals_item);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -998,17 +985,11 @@ void MasterPropertiesWidget::plcUpdateDisplaySettings()
|
||||
|
||||
// Preserve existing display settings
|
||||
ElementData ed = m_element->elementData();
|
||||
ElementData::PlcMasterData plc_data = ed.plcMasterData();
|
||||
const auto orig_plc = ed.plcMasterData();
|
||||
ElementData::PlcMasterData plc_data = orig_plc;
|
||||
plc_data.ios.clear();
|
||||
|
||||
// Build address -> original IO lookup to correctly reattach terminal data
|
||||
// after row reorder (move up/down) or row removal
|
||||
QHash<QString, int> addr_to_orig_idx;
|
||||
for (int i = 0; i < ed.plcMasterData().ios.size(); ++i) {
|
||||
addr_to_orig_idx[ed.plcMasterData().ios.at(i).address] = i;
|
||||
}
|
||||
|
||||
// Read IOs from table, preserving terminal data from original IOs
|
||||
// Read IOs from table, preserving terminal data from original IOs by row index
|
||||
for (int row = 0; row < m_plc_table->rowCount(); ++row) {
|
||||
ElementData::PlcIO io;
|
||||
|
||||
@@ -1032,9 +1013,12 @@ void MasterPropertiesWidget::plcUpdateDisplaySettings()
|
||||
if (crossref_item)
|
||||
io.crossRef = crossref_item->text();
|
||||
|
||||
// Preserve terminal data by looking up original IO via address
|
||||
if (addr_to_orig_idx.contains(io.address)) {
|
||||
const auto &orig_io = ed.plcMasterData().ios.at(addr_to_orig_idx.value(io.address));
|
||||
// Preserve terminal data by matching row index directly.
|
||||
// This avoids address-based lookup which fails when all addresses
|
||||
// are empty (common in PLC masters) — a hash collision would cause
|
||||
// only the last IO's terminals to be used for all rows.
|
||||
if (row < orig_plc.ios.size()) {
|
||||
const auto &orig_io = orig_plc.ios.at(row);
|
||||
io.terminalCount = orig_io.terminalCount;
|
||||
io.terminals = orig_io.terminals;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -29,6 +29,7 @@
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QLabel>
|
||||
#include <QCheckBox>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QGridLayout>
|
||||
@@ -36,6 +37,7 @@
|
||||
#include <QAction>
|
||||
#include <QFont>
|
||||
#include <QTimer>
|
||||
#include <QSettings>
|
||||
#include <QVBoxLayout>
|
||||
#include <QHBoxLayout>
|
||||
#include <QGridLayout>
|
||||
@@ -56,16 +58,20 @@ PlcLinkWidget::PlcLinkWidget(Element *elmt, QWidget *parent)
|
||||
main_layout->addWidget(m_unlink_pb, 0, 1);
|
||||
main_layout->addWidget(m_show_this_pb, 0, 2);
|
||||
|
||||
// Row 1: Search field
|
||||
// Row 1: Hide linked elements checkbox
|
||||
m_hide_linked_cb = new QCheckBox(tr("Masquer les éléments connectés"), this);
|
||||
main_layout->addWidget(m_hide_linked_cb, 1, 0, 1, 3);
|
||||
|
||||
// Row 2: Search field
|
||||
m_search_field = new QLineEdit(this);
|
||||
m_search_field->setPlaceholderText(tr("Recherche"));
|
||||
main_layout->addWidget(m_search_field, 1, 0, 1, 3);
|
||||
main_layout->addWidget(m_search_field, 2, 0, 1, 3);
|
||||
|
||||
// Row 2: Tree widget
|
||||
// Row 3: Tree widget
|
||||
m_tree_widget = new QTreeWidget(this);
|
||||
m_tree_widget->setHeaderLabels({
|
||||
tr("Label"), tr("Type"), tr("Adresse"),
|
||||
tr("Fonction"), tr("Commentaire"), tr("Anschlüsse")
|
||||
tr("Fonction"), tr("Commentaire"), tr("Bornes")
|
||||
});
|
||||
m_tree_widget->setRootIsDecorated(true);
|
||||
m_tree_widget->setIndentation(20);
|
||||
@@ -78,9 +84,9 @@ PlcLinkWidget::PlcLinkWidget(Element *elmt, QWidget *parent)
|
||||
m_tree_widget->header()->setSectionResizeMode(3, QHeaderView::ResizeToContents);
|
||||
m_tree_widget->header()->setSectionResizeMode(4, QHeaderView::ResizeToContents);
|
||||
m_tree_widget->header()->setSectionResizeMode(5, QHeaderView::ResizeToContents);
|
||||
main_layout->addWidget(m_tree_widget, 2, 0, 1, 3);
|
||||
main_layout->addWidget(m_tree_widget, 3, 0, 1, 3);
|
||||
|
||||
// Row 3: Hidden masters note
|
||||
// Row 4: Hidden masters note
|
||||
m_hidden_masters_label = new QLabel(
|
||||
tr("Remarque : les éléments maîtres ayant atteint leur nombre maximal "
|
||||
"d'esclaves sont masqués."), this);
|
||||
@@ -89,9 +95,9 @@ PlcLinkWidget::PlcLinkWidget(Element *elmt, QWidget *parent)
|
||||
italic_font.setItalic(true);
|
||||
m_hidden_masters_label->setFont(italic_font);
|
||||
m_hidden_masters_label->hide();
|
||||
main_layout->addWidget(m_hidden_masters_label, 3, 0, 1, 3);
|
||||
main_layout->addWidget(m_hidden_masters_label, 4, 0, 1, 3);
|
||||
|
||||
main_layout->setRowStretch(2, 1);
|
||||
main_layout->setRowStretch(3, 1);
|
||||
|
||||
setMinimumWidth(500);
|
||||
|
||||
@@ -104,6 +110,12 @@ PlcLinkWidget::PlcLinkWidget(Element *elmt, QWidget *parent)
|
||||
this, &PlcLinkWidget::on_m_search_field_textEdited);
|
||||
connect(m_tree_widget, &QTreeWidget::customContextMenuRequested,
|
||||
this, &PlcLinkWidget::on_m_tree_widget_customContextMenuRequested);
|
||||
connect(m_hide_linked_cb, &QCheckBox::toggled,
|
||||
this, &PlcLinkWidget::on_m_hide_linked_cb_toggled);
|
||||
|
||||
QSettings settings;
|
||||
m_hide_linked_cb->setChecked(
|
||||
settings.value(QStringLiteral("plclinkwidget/hideLinked"), false).toBool());
|
||||
|
||||
if (elmt)
|
||||
setElement(elmt);
|
||||
@@ -163,6 +175,7 @@ void PlcLinkWidget::buildPlcTree()
|
||||
{
|
||||
m_tree_widget->clear();
|
||||
m_io_entry_hash.clear();
|
||||
m_linked_children.clear();
|
||||
|
||||
if (!m_element || !m_element->diagram() || !m_element->diagram()->project())
|
||||
return;
|
||||
@@ -216,6 +229,7 @@ void PlcLinkWidget::buildPlcTree()
|
||||
parent_item->setExpanded(false);
|
||||
|
||||
// Add child items for each IO entry
|
||||
bool all_children_hidden = true;
|
||||
for (int i = 0; i < plc_data.ios.size(); ++i) {
|
||||
const auto &io = plc_data.ios.at(i);
|
||||
auto *child_item = new QTreeWidgetItem(parent_item);
|
||||
@@ -225,44 +239,62 @@ void PlcLinkWidget::buildPlcTree()
|
||||
child_item->setText(3, io.functionText);
|
||||
child_item->setText(4, io.comment);
|
||||
|
||||
// Terminal count
|
||||
child_item->setText(5, QString::number(io.terminalCount));
|
||||
// Terminal names
|
||||
child_item->setText(5, io.terminals.join(QStringLiteral(", ")));
|
||||
|
||||
PlcIoEntry entry;
|
||||
entry.master = elmt;
|
||||
entry.ioIndex = i;
|
||||
m_io_entry_hash.insert(child_item, entry);
|
||||
|
||||
// If this IO is already linked to a slave, grey it out and strike through
|
||||
// If this IO is already linked to a slave
|
||||
if (used_io_indices.contains(i)) {
|
||||
QFont strike_font = child_item->font(0);
|
||||
strike_font.setStrikeOut(true);
|
||||
child_item->setFont(0, strike_font);
|
||||
child_item->setFont(1, strike_font);
|
||||
child_item->setFont(2, strike_font);
|
||||
child_item->setFont(3, strike_font);
|
||||
child_item->setFont(4, strike_font);
|
||||
child_item->setFont(5, strike_font);
|
||||
m_linked_children.insert(child_item);
|
||||
if (m_hide_linked_cb->isChecked()) {
|
||||
child_item->setHidden(true);
|
||||
} else {
|
||||
all_children_hidden = false;
|
||||
QFont strike_font = child_item->font(0);
|
||||
strike_font.setStrikeOut(true);
|
||||
child_item->setFont(0, strike_font);
|
||||
child_item->setFont(1, strike_font);
|
||||
child_item->setFont(2, strike_font);
|
||||
child_item->setFont(3, strike_font);
|
||||
child_item->setFont(4, strike_font);
|
||||
child_item->setFont(5, strike_font);
|
||||
|
||||
QBrush grey_brush(Qt::gray);
|
||||
for (int col = 0; col < 6; ++col)
|
||||
child_item->setForeground(col, grey_brush);
|
||||
QBrush grey_brush(Qt::gray);
|
||||
for (int col = 0; col < 6; ++col)
|
||||
child_item->setForeground(col, grey_brush);
|
||||
|
||||
// Show which slave is linked
|
||||
for (Element *linked : elmt->linkedElements()) {
|
||||
if (elmt->groupIndexForElement(linked) == i) {
|
||||
child_item->setToolTip(0,
|
||||
tr("Lié à: %1").arg(linked->actualLabel()));
|
||||
break;
|
||||
// Show which slave is linked
|
||||
for (Element *linked : elmt->linkedElements()) {
|
||||
if (elmt->groupIndexForElement(linked) == i) {
|
||||
child_item->setToolTip(0,
|
||||
tr("Lié à: %1").arg(linked->actualLabel()));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
child_item->setFlags(child_item->flags() & ~Qt::ItemIsSelectable);
|
||||
child_item->setFlags(child_item->flags() & ~Qt::ItemIsSelectable);
|
||||
}
|
||||
} else {
|
||||
all_children_hidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
// If checkbox is on and every child is linked (hidden), hide the master too
|
||||
if (m_hide_linked_cb->isChecked() && all_children_hidden) {
|
||||
parent_item->setHidden(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool PlcLinkWidget::isChildLinked(QTreeWidgetItem *child) const
|
||||
{
|
||||
return m_linked_children.contains(child);
|
||||
}
|
||||
|
||||
void PlcLinkWidget::hideButtons()
|
||||
{
|
||||
m_label->hide();
|
||||
@@ -296,15 +328,21 @@ void PlcLinkWidget::on_m_search_field_textEdited(const QString &text)
|
||||
}
|
||||
}
|
||||
}
|
||||
child->setHidden(!match);
|
||||
if (match) any_child_visible = true;
|
||||
bool hidden = m_hide_linked_cb->isChecked() && isChildLinked(child);
|
||||
child->setHidden(!match || hidden);
|
||||
if (match && !hidden) any_child_visible = true;
|
||||
}
|
||||
|
||||
// Also check if parent label matches
|
||||
if (!text.isEmpty() && parent->text(0).contains(text, Qt::CaseInsensitive)) {
|
||||
any_child_visible = true;
|
||||
for (int j = 0; j < parent->childCount(); ++j)
|
||||
parent->child(j)->setHidden(false);
|
||||
for (int j = 0; j < parent->childCount(); ++j) {
|
||||
QTreeWidgetItem *child = parent->child(j);
|
||||
bool hidden = m_hide_linked_cb->isChecked() && isChildLinked(child);
|
||||
if (!hidden) {
|
||||
any_child_visible = true;
|
||||
child->setHidden(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parent->setHidden(!any_child_visible);
|
||||
@@ -355,6 +393,20 @@ void PlcLinkWidget::on_m_unlink_pb_clicked()
|
||||
return;
|
||||
|
||||
LinkElementCommand *undo = new LinkElementCommand(m_element);
|
||||
|
||||
// Store the current group index so makeLink() can
|
||||
// restore PLC variables and terminal labels on undo.
|
||||
for (Element *elmt : m_element->linkedElements())
|
||||
{
|
||||
if (elmt->linkType() == Element::Master)
|
||||
{
|
||||
int idx = elmt->groupIndexForElement(m_element);
|
||||
if (idx >= 0)
|
||||
undo->setGroupIndex(idx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
undo->unlinkAll();
|
||||
m_element->diagram()->undoStack().push(undo);
|
||||
|
||||
@@ -369,3 +421,10 @@ void PlcLinkWidget::on_m_show_this_pb_clicked()
|
||||
m_element->diagram()->showMe();
|
||||
m_element->setHighlighted(true);
|
||||
}
|
||||
|
||||
void PlcLinkWidget::on_m_hide_linked_cb_toggled(bool checked)
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue(QStringLiteral("plclinkwidget/hideLinked"), checked);
|
||||
buildPlcTree();
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ class QTreeWidget;
|
||||
class QLineEdit;
|
||||
class QPushButton;
|
||||
class QLabel;
|
||||
class QCheckBox;
|
||||
class Element;
|
||||
|
||||
/**
|
||||
@@ -58,17 +59,20 @@ class PlcLinkWidget : public AbstractElementPropertiesEditorWidget
|
||||
void buildPlcTree();
|
||||
void hideButtons();
|
||||
void showButtons();
|
||||
bool isChildLinked(QTreeWidgetItem *child) const;
|
||||
|
||||
private slots:
|
||||
void on_m_search_field_textEdited(const QString &text);
|
||||
void on_m_tree_widget_customContextMenuRequested(const QPoint &pos);
|
||||
void on_m_unlink_pb_clicked();
|
||||
void on_m_show_this_pb_clicked();
|
||||
void on_m_hide_linked_cb_toggled(bool checked);
|
||||
|
||||
private:
|
||||
QLabel *m_label{nullptr};
|
||||
QPushButton *m_unlink_pb{nullptr};
|
||||
QPushButton *m_show_this_pb{nullptr};
|
||||
QCheckBox *m_hide_linked_cb{nullptr};
|
||||
QLineEdit *m_search_field{nullptr};
|
||||
QTreeWidget *m_tree_widget{nullptr};
|
||||
QLabel *m_hidden_masters_label{nullptr};
|
||||
@@ -79,6 +83,7 @@ class PlcLinkWidget : public AbstractElementPropertiesEditorWidget
|
||||
int ioIndex = -1;
|
||||
};
|
||||
QHash<QTreeWidgetItem*, PlcIoEntry> m_io_entry_hash;
|
||||
QSet<QTreeWidgetItem*> m_linked_children;
|
||||
|
||||
Element *m_element_to_link = nullptr;
|
||||
int m_pending_io_index = -1;
|
||||
|
||||
@@ -44,9 +44,9 @@ ProjectPropertiesDialog::ProjectPropertiesDialog(QETProject *project, QWidget *p
|
||||
|
||||
m_properties_dialog->addPage(new TerminalStripProjectConfigPage { project, parent });
|
||||
|
||||
connect(projectAutoNumConfigPage,SIGNAL(setAutoNum(QString)),newDiagramPage,SLOT(setFolioAutonum(QString)));
|
||||
connect(projectAutoNumConfigPage,SIGNAL(saveCurrentTbp()),newDiagramPage,SLOT(saveCurrentTbp()));
|
||||
connect(projectAutoNumConfigPage,SIGNAL(loadSavedTbp()),newDiagramPage,SLOT(loadSavedTbp()));
|
||||
connect(projectAutoNumConfigPage, qOverload<QString>(&ProjectAutoNumConfigPage::setAutoNum), newDiagramPage, &NewDiagramPage::setFolioAutonum);
|
||||
connect(projectAutoNumConfigPage, &ProjectAutoNumConfigPage::saveCurrentTbp, newDiagramPage, &NewDiagramPage::saveCurrentTbp);
|
||||
connect(projectAutoNumConfigPage, &ProjectAutoNumConfigPage::loadSavedTbp, newDiagramPage, &NewDiagramPage::loadSavedTbp);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#include "../QPropertyUndoCommand/qpropertyundocommand.h"
|
||||
#include "../diagram.h"
|
||||
#include "../lastusedstyle.h"
|
||||
#include "../qetgraphicsitem/qetshapeitem.h"
|
||||
#include "../ui_shapegraphicsitempropertieswidget.h"
|
||||
|
||||
@@ -72,7 +73,8 @@ void ShapeGraphicsItemPropertiesWidget::setItem(QetShapeItem *shape)
|
||||
}
|
||||
|
||||
m_shape = shape;
|
||||
ui->m_close_polygon->setVisible(m_shape->shapeType() == QetShapeItem::Polygon);
|
||||
ui->m_close_polygon->setVisible(m_shape->shapeType() == QetShapeItem::Polygon
|
||||
|| m_shape->shapeType() == QetShapeItem::Path);
|
||||
ui->m_filling_gb->setHidden(m_shape->shapeType() == QetShapeItem::Line);
|
||||
|
||||
updateUi();
|
||||
@@ -163,6 +165,58 @@ QUndoCommand* ShapeGraphicsItemPropertiesWidget::associatedUndo() const
|
||||
{
|
||||
QPropertyUndoCommand *undo = nullptr;
|
||||
|
||||
//Geometry
|
||||
const bool hasGeometry = (m_shape->shapeType() == QetShapeItem::Line
|
||||
|| m_shape->shapeType() == QetShapeItem::Rectangle
|
||||
|| m_shape->shapeType() == QetShapeItem::Ellipse);
|
||||
if (hasGeometry)
|
||||
{
|
||||
if (m_shape->shapeType() == QetShapeItem::Line)
|
||||
{
|
||||
const QLineF old_line = m_shape->line();
|
||||
QLineF new_line = old_line;
|
||||
new_line.setLength(ui->m_geom_dim1_dsb->value());
|
||||
|
||||
if (new_line != old_line)
|
||||
{
|
||||
undo = new QPropertyUndoCommand(m_shape, "line", old_line, new_line);
|
||||
undo->setText(tr("Modifier la longueur d'une ligne"));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
const QRectF old_rect = m_shape->rect().normalized();
|
||||
const bool isEllipse = (m_shape->shapeType() == QetShapeItem::Ellipse);
|
||||
const qreal new_width = isEllipse ? ui->m_geom_dim1_dsb->value() * 2.0 : ui->m_geom_dim1_dsb->value();
|
||||
const qreal new_height = isEllipse ? ui->m_geom_dim2_dsb->value() * 2.0 : ui->m_geom_dim2_dsb->value();
|
||||
// Anchored on the shape's own current top-left, not
|
||||
// re-centered -- matches how the resize handle itself
|
||||
// behaves (the opposite corner stays put while the
|
||||
// dragged one moves), so typing a width/height here
|
||||
// and dragging a handle agree on what "resizing" means.
|
||||
const QRectF new_rect(old_rect.topLeft(), QSizeF(new_width, new_height));
|
||||
|
||||
if (new_rect != old_rect)
|
||||
{
|
||||
undo = new QPropertyUndoCommand(m_shape, "rect", old_rect, new_rect);
|
||||
undo->setText(tr("Modifier la taille d'une forme"));
|
||||
}
|
||||
}
|
||||
|
||||
const qreal old_angle = m_shape->rotation();
|
||||
const qreal new_angle = ui->m_geom_angle_dsb->value();
|
||||
if (qAbs(old_angle - new_angle) > 0.05)
|
||||
{
|
||||
if (undo)
|
||||
new QPropertyUndoCommand(m_shape, "rotation", old_angle, new_angle, undo);
|
||||
else
|
||||
{
|
||||
undo = new QPropertyUndoCommand(m_shape, "rotation", old_angle, new_angle);
|
||||
undo->setText(tr("Modifier l'angle d'une forme"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QPen old_pen = m_shape->pen();
|
||||
QPen new_pen = old_pen;
|
||||
|
||||
@@ -180,6 +234,7 @@ QUndoCommand* ShapeGraphicsItemPropertiesWidget::associatedUndo() const
|
||||
{
|
||||
undo = new QPropertyUndoCommand(m_shape, "pen", old_pen, new_pen);
|
||||
undo->setText(tr("Modifier le trait d'une forme"));
|
||||
LastUsedStyle::setShapePen(new_pen);
|
||||
}
|
||||
|
||||
QBrush old_brush = m_shape->brush();
|
||||
@@ -196,6 +251,7 @@ QUndoCommand* ShapeGraphicsItemPropertiesWidget::associatedUndo() const
|
||||
undo = new QPropertyUndoCommand(m_shape, "brush", old_brush, new_brush);
|
||||
undo->setText(tr("Modifier le remplissage d'une forme"));
|
||||
}
|
||||
LastUsedStyle::setShapeBrush(new_brush);
|
||||
}
|
||||
|
||||
if (ui->m_close_polygon->isChecked() != m_shape->isClosed())
|
||||
@@ -321,6 +377,7 @@ QUndoCommand* ShapeGraphicsItemPropertiesWidget::associatedUndo() const
|
||||
|
||||
if (new_pen != old_pen) {
|
||||
new QPropertyUndoCommand(m_shape, "pen", old_pen, new_pen, undo);
|
||||
LastUsedStyle::setShapePen(new_pen);
|
||||
}
|
||||
|
||||
QBrush old_brush = m_shape->brush();
|
||||
@@ -330,6 +387,7 @@ QUndoCommand* ShapeGraphicsItemPropertiesWidget::associatedUndo() const
|
||||
|
||||
if (new_brush != old_brush) {
|
||||
new QPropertyUndoCommand(m_shape, "brush", old_brush, new_brush, undo);
|
||||
LastUsedStyle::setShapeBrush(new_brush);
|
||||
}
|
||||
|
||||
if (ui->m_close_polygon->isChecked() != m_shape->isClosed()) {
|
||||
@@ -361,6 +419,35 @@ void ShapeGraphicsItemPropertiesWidget::updateUi()
|
||||
|
||||
if (m_shape)
|
||||
{
|
||||
//Geometry
|
||||
const bool hasGeometry = (m_shape->shapeType() == QetShapeItem::Line
|
||||
|| m_shape->shapeType() == QetShapeItem::Rectangle
|
||||
|| m_shape->shapeType() == QetShapeItem::Ellipse);
|
||||
ui->m_geometry_gb->setVisible(hasGeometry);
|
||||
if (hasGeometry)
|
||||
{
|
||||
ui->m_geom_angle_dsb->setValue(m_shape->rotation());
|
||||
|
||||
if (m_shape->shapeType() == QetShapeItem::Line)
|
||||
{
|
||||
ui->m_geom_dim1_label->setText(tr("Longueur"));
|
||||
ui->m_geom_dim1_dsb->setValue(m_shape->line().length());
|
||||
ui->m_geom_dim2_label->setVisible(false);
|
||||
ui->m_geom_dim2_dsb->setVisible(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
const QRectF r = m_shape->rect().normalized();
|
||||
const bool isEllipse = (m_shape->shapeType() == QetShapeItem::Ellipse);
|
||||
ui->m_geom_dim1_label->setText(isEllipse ? tr("Rayon X") : tr("Largeur"));
|
||||
ui->m_geom_dim2_label->setText(isEllipse ? tr("Rayon Y") : tr("Hauteur"));
|
||||
ui->m_geom_dim1_dsb->setValue(isEllipse ? r.width() / 2.0 : r.width());
|
||||
ui->m_geom_dim2_dsb->setValue(isEllipse ? r.height() / 2.0 : r.height());
|
||||
ui->m_geom_dim2_label->setVisible(true);
|
||||
ui->m_geom_dim2_dsb->setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
//Pen
|
||||
ui->m_style_cb->setCurrentIndex(static_cast<int>(m_shape->pen().style()) - 1);
|
||||
ui->m_size_dsb ->setValue(m_shape->pen().widthF());
|
||||
@@ -368,7 +455,7 @@ void ShapeGraphicsItemPropertiesWidget::updateUi()
|
||||
ui->m_color_kpb->setColor(m_shape->pen().color());
|
||||
|
||||
//Brush
|
||||
if (m_shape->shapeType() == QetShapeItem::Polygon)
|
||||
if (m_shape->shapeType() == QetShapeItem::Polygon || m_shape->shapeType() == QetShapeItem::Path)
|
||||
ui->m_filling_gb->setVisible(m_shape->isClosed());
|
||||
|
||||
ui->m_brush_style_cb->setCurrentIndex(static_cast<int>(m_shape->brush().style()));
|
||||
@@ -379,6 +466,7 @@ void ShapeGraphicsItemPropertiesWidget::updateUi()
|
||||
}
|
||||
else if (m_shapes_list.size() >= 2)
|
||||
{
|
||||
ui->m_geometry_gb->setVisible(false);
|
||||
ui->m_close_polygon->setHidden(true);
|
||||
bool same = true;
|
||||
//Pen
|
||||
@@ -472,6 +560,15 @@ void ShapeGraphicsItemPropertiesWidget::setUpEditConnection()
|
||||
|
||||
if (m_shape || !m_shapes_list.isEmpty())
|
||||
{
|
||||
m_edit_connection << connect (ui->m_geom_dim1_dsb, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
||||
this, &ShapeGraphicsItemPropertiesWidget::apply);
|
||||
|
||||
m_edit_connection << connect (ui->m_geom_dim2_dsb, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
||||
this, &ShapeGraphicsItemPropertiesWidget::apply);
|
||||
|
||||
m_edit_connection << connect (ui->m_geom_angle_dsb, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
|
||||
this, &ShapeGraphicsItemPropertiesWidget::apply);
|
||||
|
||||
m_edit_connection << connect (ui->m_style_cb, QOverload<int>::of(&QComboBox::activated),
|
||||
this, &ShapeGraphicsItemPropertiesWidget::apply);
|
||||
|
||||
@@ -490,14 +587,23 @@ void ShapeGraphicsItemPropertiesWidget::setUpEditConnection()
|
||||
m_edit_connection << connect (ui->m_close_polygon, &QCheckBox::clicked,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::apply);
|
||||
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::penChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
if (m_shape)
|
||||
{
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::penChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::closeChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::closeChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::brushChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::brushChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::geometryChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
|
||||
m_edit_connection << connect (m_shape, &QetShapeItem::transformChanged,
|
||||
this, &ShapeGraphicsItemPropertiesWidget::updateUi);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,78 @@
|
||||
<string>Form</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="m_geometry_gb">
|
||||
<property name="title">
|
||||
<string>Géométrie</string>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout_3">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="m_geom_dim1_label">
|
||||
<property name="text">
|
||||
<string>Largeur</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QDoubleSpinBox" name="m_geom_dim1_dsb">
|
||||
<property name="decimals">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.010000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>100000.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="m_geom_dim2_label">
|
||||
<property name="text">
|
||||
<string>Hauteur</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QDoubleSpinBox" name="m_geom_dim2_dsb">
|
||||
<property name="decimals">
|
||||
<number>2</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>0.010000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>100000.000000000000000</double>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="m_geom_angle_label">
|
||||
<property name="text">
|
||||
<string>Angle</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QDoubleSpinBox" name="m_geom_angle_dsb">
|
||||
<property name="decimals">
|
||||
<number>1</number>
|
||||
</property>
|
||||
<property name="minimum">
|
||||
<double>-360.000000000000000</double>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<double>360.000000000000000</double>
|
||||
</property>
|
||||
<property name="suffix">
|
||||
<string> °</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
@@ -230,7 +302,7 @@
|
||||
<item>
|
||||
<widget class="QCheckBox" name="m_close_polygon">
|
||||
<property name="text">
|
||||
<string>Polygone fermé</string>
|
||||
<string>Forme fermée</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
@@ -257,6 +329,9 @@
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<tabstops>
|
||||
<tabstop>m_geom_dim1_dsb</tabstop>
|
||||
<tabstop>m_geom_dim2_dsb</tabstop>
|
||||
<tabstop>m_geom_angle_dsb</tabstop>
|
||||
<tabstop>m_style_cb</tabstop>
|
||||
<tabstop>m_size_dsb</tabstop>
|
||||
<tabstop>m_color_kpb</tabstop>
|
||||
|
||||
@@ -5,18 +5,67 @@
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "../undocommand/changeelementinformationcommand.h"
|
||||
#include <QUndoCommand>
|
||||
#include <QCheckBox>
|
||||
#include <QVBoxLayout>
|
||||
#include <QSet>
|
||||
#include <algorithm>
|
||||
|
||||
/**
|
||||
* @brief TerminalNumberingDialog::TerminalNumberingDialog
|
||||
* Constructor
|
||||
* @param parent
|
||||
* @param project Pointer to the current project (used to populate terminal strips)
|
||||
*/
|
||||
TerminalNumberingDialog::TerminalNumberingDialog(QWidget *parent) :
|
||||
QDialog(parent),
|
||||
ui(new Ui::TerminalNumberingDialog)
|
||||
TerminalNumberingDialog::TerminalNumberingDialog(QWidget *parent, QETProject *project) :
|
||||
QDialog(parent),
|
||||
ui(new Ui::TerminalNumberingDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// Connect radio button to enable/disable the "also number letters" checkbox
|
||||
connect(ui->rb_type_alpha, &QRadioButton::toggled, ui->cb_also_alpha, &QCheckBox::setEnabled);
|
||||
|
||||
// Collect all unique terminal strip prefixes from the project
|
||||
if (project) {
|
||||
QSet<QString> prefixes;
|
||||
foreach (Diagram *diagram, project->diagrams()) {
|
||||
foreach (QGraphicsItem *qgi, diagram->items()) {
|
||||
if (Element *elmt = qgraphicsitem_cast<Element *>(qgi)) {
|
||||
if (elmt->elementData().m_type == ElementData::Terminal) {
|
||||
// Ignore locked terminals
|
||||
DiagramContext info = elmt->elementInformations();
|
||||
if (info.value(QStringLiteral("auto_num_locked")).toString() == QLatin1String("true")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QString label = elmt->actualLabel();
|
||||
if (label.isEmpty()) continue;
|
||||
|
||||
// Handle labels with and without colon
|
||||
int colonIndex = label.lastIndexOf(':');
|
||||
QString prefix;
|
||||
if (colonIndex != -1) {
|
||||
prefix = label.left(colonIndex);
|
||||
} else {
|
||||
prefix = label;
|
||||
}
|
||||
prefixes.insert(prefix);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort prefixes alphabetically and create checkboxes
|
||||
QStringList sortedPrefixes = prefixes.values();
|
||||
sortedPrefixes.sort(Qt::CaseInsensitive);
|
||||
|
||||
foreach (const QString &prefix, sortedPrefixes) {
|
||||
QCheckBox *cb = new QCheckBox(prefix);
|
||||
cb->setChecked(true);
|
||||
ui->verticalLayout_strips_content->addWidget(cb);
|
||||
m_stripCheckboxes.insert(prefix, cb);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -46,11 +95,37 @@ bool TerminalNumberingDialog::isAlphanumeric() const
|
||||
return ui->rb_type_alpha->isChecked();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalNumberingDialog::alsoNumberLetters
|
||||
* @return true if the "also number letters" checkbox is checked
|
||||
*/
|
||||
bool TerminalNumberingDialog::alsoNumberLetters() const
|
||||
{
|
||||
return ui->cb_also_alpha->isChecked();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalNumberingDialog::excludedStrips
|
||||
* @return List of terminal strip prefixes that should be excluded from numbering
|
||||
*/
|
||||
QStringList TerminalNumberingDialog::excludedStrips() const
|
||||
{
|
||||
QStringList excluded;
|
||||
QMapIterator<QString, QCheckBox*> it(m_stripCheckboxes);
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
if (!it.value()->isChecked()) {
|
||||
excluded.append(it.key());
|
||||
}
|
||||
}
|
||||
return excluded;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief TerminalNumberingDialog::getUndoCommand
|
||||
* Scans the given project for terminals, sorts them according to user preferences
|
||||
* (X/Y axis, alphanumeric rules), and generates an undo command containing all label changes.
|
||||
* * @param project Pointer to the current QETProject
|
||||
* @param project Pointer to the current QETProject
|
||||
* @return QUndoCommand* containing the modifications, or nullptr if no changes are needed.
|
||||
*/
|
||||
QUndoCommand* TerminalNumberingDialog::getUndoCommand(QETProject *project) const {
|
||||
@@ -58,6 +133,8 @@ QUndoCommand* TerminalNumberingDialog::getUndoCommand(QETProject *project) const
|
||||
|
||||
bool axisX = isXAxisPriority();
|
||||
bool alpha = isAlphanumeric();
|
||||
bool alsoAlpha = alsoNumberLetters();
|
||||
QStringList excluded = excludedStrips();
|
||||
|
||||
// 1. Helper structure to store and sort terminal data
|
||||
struct TermInfo {
|
||||
@@ -97,6 +174,9 @@ QUndoCommand* TerminalNumberingDialog::getUndoCommand(QETProject *project) const
|
||||
suffix = label.mid(colonIndex + 1);
|
||||
}
|
||||
|
||||
// Skip excluded terminal strips
|
||||
if (excluded.contains(prefix)) continue;
|
||||
|
||||
// If user chose purely numeric, skip terminals with alphabetical suffixes
|
||||
if (!alpha && !suffix.isEmpty()) {
|
||||
bool isNum;
|
||||
@@ -154,8 +234,16 @@ QUndoCommand* TerminalNumberingDialog::getUndoCommand(QETProject *project) const
|
||||
// If it was a number (e.g., "1") or empty, update it with the new counter
|
||||
newLabel = ti.prefix + ":" + QString::number(newNum);
|
||||
} else {
|
||||
// If it was alphabetical (e.g., "N", "PE"), keep the original text but consume the count!
|
||||
newLabel = ti.prefix + ":" + ti.suffix;
|
||||
// Only append to purely alphabetic suffixes (N, PE), so re-running is a
|
||||
// no-op and already-numbered suffixes (L1, L2, L3) keep their meaning.
|
||||
const bool allLetters = !ti.suffix.isEmpty()
|
||||
&& std::all_of(ti.suffix.cbegin(), ti.suffix.cend(),
|
||||
[](QChar c){ return c.isLetter(); });
|
||||
if (alsoAlpha && allLetters) {
|
||||
newLabel = ti.prefix + ":" + ti.suffix + QString::number(newNum);
|
||||
} else {
|
||||
newLabel = ti.prefix + ":" + ti.suffix;
|
||||
}
|
||||
}
|
||||
|
||||
DiagramContext oldInfo = ti.elmt->elementInformations();
|
||||
|
||||
@@ -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,9 @@ XRefPropertiesWidget::XRefPropertiesWidget(QHash <QString, XRefProperties> prope
|
||||
{
|
||||
ui->setupUi(this);
|
||||
buildUi();
|
||||
connect(ui->m_display_has_cross_rb, SIGNAL(toggled(bool)), ui->m_cross_properties_gb, SLOT(setEnabled(bool)));
|
||||
connect(ui->m_type_cb, SIGNAL(currentIndexChanged(int)), this, SLOT(typeChanged()));
|
||||
connect(ui->m_snap_to_cb, SIGNAL(currentIndexChanged(int)), this, SLOT(enableOffsetSB(int)));
|
||||
connect(ui->m_display_has_cross_rb, &QRadioButton::toggled, ui->m_cross_properties_gb, &QWidget::setEnabled);
|
||||
connect(ui->m_type_cb, qOverload<int>(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::typeChanged);
|
||||
connect(ui->m_snap_to_cb, qOverload<int>(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::enableOffsetSB);
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
@@ -49,9 +49,9 @@ XRefPropertiesWidget::XRefPropertiesWidget(QHash <QString, XRefProperties> prope
|
||||
*/
|
||||
XRefPropertiesWidget::~XRefPropertiesWidget()
|
||||
{
|
||||
disconnect(ui->m_display_has_cross_rb, SIGNAL(toggled(bool)), ui->m_cross_properties_gb, SLOT(setEnabled(bool)));
|
||||
disconnect(ui->m_type_cb, SIGNAL(currentIndexChanged(int)), this, SLOT(typeChanged()));
|
||||
disconnect(ui->m_snap_to_cb, SIGNAL(currentIndexChanged(int)), this, SLOT(enableOffsetSB(int)));
|
||||
disconnect(ui->m_display_has_cross_rb, &QRadioButton::toggled, ui->m_cross_properties_gb, &QWidget::setEnabled);
|
||||
disconnect(ui->m_type_cb, qOverload<int>(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::typeChanged);
|
||||
disconnect(ui->m_snap_to_cb, qOverload<int>(&QComboBox::currentIndexChanged), this, &XRefPropertiesWidget::enableOffsetSB);
|
||||
delete ui;
|
||||
}
|
||||
|
||||
@@ -111,6 +111,7 @@ void XRefPropertiesWidget::buildUi()
|
||||
ui -> m_xrefpos_cb -> addItem(tr("Left"),"left");
|
||||
ui -> m_xrefpos_cb -> addItem(tr("Right"),"right");
|
||||
ui -> m_xrefpos_cb -> addItem(tr("Text alignment"),"alignment");
|
||||
ui -> m_xrefpos_cb -> addItem(tr("Champ de texte"),"text_field");
|
||||
m_previous_type_index = ui -> m_type_cb -> currentIndex();
|
||||
}
|
||||
|
||||
@@ -139,6 +140,7 @@ void XRefPropertiesWidget::saveProperties(int index) {
|
||||
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "left") xrp.setXrefPos(Qt::AlignLeft);
|
||||
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "right") xrp.setXrefPos(Qt::AlignRight);
|
||||
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "alignment") xrp.setXrefPos(Qt::AlignBaseline);
|
||||
else if(ui->m_xrefpos_cb->itemData(ui->m_xrefpos_cb->currentIndex()).toString() == "text_field") xrp.setXrefPos(Qt::AlignHCenter);
|
||||
xrp.setShowPowerContac(ui->m_show_power_cb->isChecked());
|
||||
xrp.setShowTerminalName(ui->m_show_terminal_name_cb->isChecked());
|
||||
xrp.setPrefix("power", ui->m_power_prefix_le->text());
|
||||
@@ -147,6 +149,7 @@ void XRefPropertiesWidget::saveProperties(int index) {
|
||||
xrp.setMasterLabel(ui->m_master_le->text());
|
||||
xrp.setSlaveLabel(ui->m_slave_le->text());
|
||||
xrp.setOffset(ui->m_offset_sb->value());
|
||||
xrp.setSlaveOffset(ui->m_slave_offset_sb->value());
|
||||
|
||||
m_properties.insert(type, xrp);
|
||||
}
|
||||
@@ -177,6 +180,9 @@ void XRefPropertiesWidget::updateDisplay()
|
||||
int offset = xrp.offset();
|
||||
ui->m_offset_sb->setValue(offset);
|
||||
|
||||
int slave_offset = xrp.slaveOffset();
|
||||
ui->m_slave_offset_sb->setValue(slave_offset);
|
||||
|
||||
if (xrp.snapTo() == XRefProperties::Bottom){
|
||||
ui->m_snap_to_cb->setCurrentIndex(ui->m_snap_to_cb->findData("bottom"));
|
||||
ui->m_offset_sb->setEnabled(true);
|
||||
@@ -191,6 +197,7 @@ void XRefPropertiesWidget::updateDisplay()
|
||||
else if(xrp.getXrefPos() == Qt::AlignRight) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("right"));
|
||||
else if(xrp.getXrefPos() == Qt::AlignBaseline) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("alignment"));
|
||||
else if(xrp.getXrefPos() == Qt::AlignBottom) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("bottom"));
|
||||
else if(xrp.getXrefPos() == Qt::AlignHCenter) ui->m_xrefpos_cb->setCurrentIndex(ui->m_xrefpos_cb->findData("text_field"));
|
||||
ui->m_show_power_cb->setChecked(xrp.showPowerContact());
|
||||
ui->m_show_terminal_name_cb->setChecked(xrp.showTerminalName());
|
||||
ui->m_power_prefix_le-> setText(xrp.prefix("power"));
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user