mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-28 04:54:13 +02:00
Merge pull request #1088 from Kellermorph/materiallist
Add a material list lookup for the element article fields
This commit is contained in:
@@ -332,6 +332,7 @@ target_include_directories(
|
||||
${QET_DIR}/sources/factory/ui
|
||||
${QET_DIR}/sources/print
|
||||
${QET_DIR}/sources/svg
|
||||
${QET_DIR}/sources/materiallist
|
||||
)
|
||||
|
||||
if(NOT BUILD_WITH_KF)
|
||||
|
||||
@@ -73,6 +73,7 @@ set(QET_RES_FILES
|
||||
${QET_DIR}/sources/editor/ui/terminaleditor.ui
|
||||
${QET_DIR}/sources/ElementsCollection/ui/renamedialog.ui
|
||||
${QET_DIR}/sources/factory/ui/addtabledialog.ui
|
||||
${QET_DIR}/sources/materiallist/materialselectiondialog.ui
|
||||
${QET_DIR}/sources/NameList/ui/namelistdialog.ui
|
||||
${QET_DIR}/sources/NameList/ui/namelistwidget.ui
|
||||
${QET_DIR}/sources/print/projectprintwindow.ui
|
||||
@@ -863,6 +864,13 @@ set(QET_SRC_FILES
|
||||
${QET_DIR}/sources/utils/qetutils.cpp
|
||||
${QET_DIR}/sources/utils/qetutils.h
|
||||
|
||||
${QET_DIR}/sources/materiallist/materialentrydialog.cpp
|
||||
${QET_DIR}/sources/materiallist/materialentrydialog.h
|
||||
${QET_DIR}/sources/materiallist/materiallist.cpp
|
||||
${QET_DIR}/sources/materiallist/materiallist.h
|
||||
${QET_DIR}/sources/materiallist/materialselectiondialog.cpp
|
||||
${QET_DIR}/sources/materiallist/materialselectiondialog.h
|
||||
|
||||
${QET_DIR}/sources/xml/terminalstripitemxml.cpp
|
||||
${QET_DIR}/sources/xml/terminalstripitemxml.h
|
||||
${QET_DIR}/sources/xml/terminalstriplayoutpatternxml.cpp
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
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 "materialentrydialog.h"
|
||||
|
||||
#include "../qetmessagebox.h"
|
||||
|
||||
#include <QDialogButtonBox>
|
||||
#include <QGridLayout>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QScrollArea>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
/**
|
||||
@brief MaterialEntryDialog::MaterialEntryDialog
|
||||
Build one field per column of the material file.
|
||||
@param columns the columns of the file, in file order
|
||||
@param parent parent widget
|
||||
*/
|
||||
MaterialEntryDialog::MaterialEntryDialog(const QStringList &columns, QWidget *parent) :
|
||||
QDialog(parent),
|
||||
m_columns(columns)
|
||||
{
|
||||
setWindowTitle(tr("Nouvelle entrée"));
|
||||
|
||||
//Two fields per row : the form shows the whole article at once
|
||||
//instead of stacking twenty lines in a narrow column.
|
||||
setMinimumSize(780, 560);
|
||||
resize(1120, 800);
|
||||
|
||||
auto *main_layout = new QVBoxLayout(this);
|
||||
|
||||
auto *intro = new QLabel(
|
||||
tr("Renseignez l'article à ajouter. Les champs laissés vides "
|
||||
"restent vides dans le fichier."), this);
|
||||
intro->setWordWrap(true);
|
||||
main_layout->addWidget(intro);
|
||||
|
||||
auto *scroll_area = new QScrollArea(this);
|
||||
scroll_area->setWidgetResizable(true);
|
||||
scroll_area->setFrameShape(QFrame::NoFrame);
|
||||
|
||||
auto *container = new QWidget(scroll_area);
|
||||
auto *grid = new QGridLayout(container);
|
||||
|
||||
for (int i = 0; i < m_columns.size(); ++i)
|
||||
{
|
||||
const QString column = m_columns.at(i);
|
||||
|
||||
auto *label = new QLabel(MaterialList::translatedColumn(column) + QLatin1Char(' '), container);
|
||||
label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
|
||||
|
||||
auto *edit = new QLineEdit(container);
|
||||
edit->setClearButtonEnabled(true);
|
||||
|
||||
label->setBuddy(edit);
|
||||
const int row = i / 2;
|
||||
const int pair = i % 2;
|
||||
grid->addWidget(label, row, pair * 2);
|
||||
grid->addWidget(edit, row, pair * 2 + 1);
|
||||
m_edits.append(edit);
|
||||
}
|
||||
grid->setColumnStretch(1, 1);
|
||||
grid->setColumnStretch(3, 1);
|
||||
|
||||
scroll_area->setWidget(container);
|
||||
main_layout->addWidget(scroll_area, 1);
|
||||
|
||||
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
||||
buttons->button(QDialogButtonBox::Ok)->setText(tr("Enregistrer"));
|
||||
buttons->button(QDialogButtonBox::Cancel)->setText(tr("Annuler"));
|
||||
connect(buttons, &QDialogButtonBox::accepted, this, &MaterialEntryDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, this, &MaterialEntryDialog::reject);
|
||||
main_layout->addWidget(buttons);
|
||||
|
||||
if (!m_edits.isEmpty()) {
|
||||
m_edits.first()->setFocus();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialEntryDialog::~MaterialEntryDialog
|
||||
Destructor
|
||||
*/
|
||||
MaterialEntryDialog::~MaterialEntryDialog() = default;
|
||||
|
||||
/**
|
||||
@brief MaterialEntryDialog::record
|
||||
@return the article described by the form. Empty fields are left out :
|
||||
an empty column never means anything for an article which is being
|
||||
created.
|
||||
*/
|
||||
MaterialRecord MaterialEntryDialog::record() const
|
||||
{
|
||||
MaterialRecord record;
|
||||
|
||||
for (int i = 0; i < m_columns.size() && i < m_edits.size(); ++i)
|
||||
{
|
||||
QString value = m_edits.at(i)->text();
|
||||
value.remove(QLatin1Char('\r'));
|
||||
value.remove(QLatin1Char('\n'));
|
||||
value = value.trimmed();
|
||||
if (!value.isEmpty()) {
|
||||
record.setValue(m_columns.at(i), value);
|
||||
}
|
||||
}
|
||||
|
||||
return record;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialEntryDialog::accept
|
||||
Refuse an empty form: an article without any information is not worth
|
||||
a line in the file.
|
||||
*/
|
||||
void MaterialEntryDialog::accept()
|
||||
{
|
||||
if (record().values.isEmpty())
|
||||
{
|
||||
QET::QetMessageBox::warning(this,
|
||||
tr("Aucun renseignement"),
|
||||
tr("Saisissez au moins un renseignement "
|
||||
"pour créer une entrée."));
|
||||
return;
|
||||
}
|
||||
|
||||
QDialog::accept();
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
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 MATERIALENTRYDIALOG_H
|
||||
#define MATERIALENTRYDIALOG_H
|
||||
|
||||
#include "../materiallist/materiallist.h"
|
||||
|
||||
#include <QDialog>
|
||||
#include <QList>
|
||||
|
||||
class QLineEdit;
|
||||
|
||||
/**
|
||||
@brief The form used to add an article to the material file.
|
||||
|
||||
One field per column of the file, including the columns QElectroTech
|
||||
does not know : whatever the user typed there is written back to the
|
||||
file untouched when a later row is appended.
|
||||
*/
|
||||
class MaterialEntryDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MaterialEntryDialog(const QStringList &columns, QWidget *parent = nullptr);
|
||||
~MaterialEntryDialog() override;
|
||||
|
||||
/** @brief the article described by the form */
|
||||
MaterialRecord record() const;
|
||||
|
||||
void accept() override;
|
||||
|
||||
//ATTRIBUTES
|
||||
private:
|
||||
QStringList m_columns;
|
||||
QList<QLineEdit*> m_edits;
|
||||
};
|
||||
|
||||
#endif // MATERIALENTRYDIALOG_H
|
||||
@@ -0,0 +1,778 @@
|
||||
/*
|
||||
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 "materiallist.h"
|
||||
|
||||
#include "../qetinformation.h"
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <QSaveFile>
|
||||
#include <QSettings>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include <utility>
|
||||
|
||||
namespace {
|
||||
|
||||
/**
|
||||
@brief Decode a csv file content.
|
||||
The material file is written in utf-8 with a BOM, the same way the bill
|
||||
of material export does, so spreadsheets open it with the right
|
||||
encoding. A file written by hand in the local encoding is still
|
||||
decodable: fromUtf8() is only used when the bytes are valid utf-8.
|
||||
@param raw
|
||||
@return
|
||||
*/
|
||||
QString decode(const QByteArray &raw)
|
||||
{
|
||||
if (raw.startsWith("\xEF\xBB\xBF")) {
|
||||
return QString::fromUtf8(raw.constData() + 3, raw.size() - 3);
|
||||
}
|
||||
|
||||
const QByteArray utf8 = QString::fromUtf8(raw).toUtf8();
|
||||
if (utf8 == raw) {
|
||||
return QString::fromUtf8(raw);
|
||||
}
|
||||
return QString::fromLocal8Bit(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Tell whether a file exists and holds something.
|
||||
An existing but empty file is the same as no file at all: it can be
|
||||
filled with the template without losing anything.
|
||||
@param path
|
||||
@return
|
||||
*/
|
||||
bool hasContent(const QString &path)
|
||||
{
|
||||
const QFileInfo info(path);
|
||||
return info.exists() && info.isFile() && info.size() > 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
/**
|
||||
@brief MaterialList::settingsKey
|
||||
@return the QSettings key holding the path of the material file
|
||||
*/
|
||||
QString MaterialList::settingsKey()
|
||||
{
|
||||
return QStringLiteral("elements-collections/material-list-path");
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::configuredPath
|
||||
@return the path of the material file as chosen in the preferences,
|
||||
or a null QString when the user never set one.
|
||||
*/
|
||||
QString MaterialList::configuredPath()
|
||||
{
|
||||
QSettings settings;
|
||||
const QString path = settings.value(settingsKey(), QStringLiteral("default")).toString();
|
||||
if (path.isEmpty() || path == QLatin1String("default")) {
|
||||
return QString();
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::setConfiguredPath
|
||||
Store the path of the material file in the preferences.
|
||||
@param path
|
||||
*/
|
||||
void MaterialList::setConfiguredPath(const QString &path)
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue(settingsKey(), path.isEmpty() ? QStringLiteral("default") : path);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::defaultFileName
|
||||
@return the file name used when the material file is created without an
|
||||
explicit location.
|
||||
*/
|
||||
QString MaterialList::defaultFileName()
|
||||
{
|
||||
return QStringLiteral("qet_material_list.csv");
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::defaultPath
|
||||
@return where the material file is created when no path is configured
|
||||
yet : the standard documents folder.
|
||||
*/
|
||||
QString MaterialList::defaultPath()
|
||||
{
|
||||
QString dir = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
|
||||
while (dir.endsWith(QLatin1Char('/'))) {
|
||||
dir.chop(1);
|
||||
}
|
||||
if (dir.isEmpty()) {
|
||||
dir = QDir::currentPath();
|
||||
}
|
||||
return dir + QLatin1Char('/') + defaultFileName();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::savedHeaderState
|
||||
@return the saved column layout of the selection window, empty when the
|
||||
window was never opened or the layout could not be read back.
|
||||
*/
|
||||
QByteArray MaterialList::savedHeaderState()
|
||||
{
|
||||
QSettings settings;
|
||||
return settings.value(QStringLiteral("elements-collections/material-list-header")).toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::saveHeaderState
|
||||
Remember the column layout of the selection window.
|
||||
@param state the header state as saved by QHeaderView::saveState()
|
||||
*/
|
||||
void MaterialList::saveHeaderState(const QByteArray &state)
|
||||
{
|
||||
QSettings settings;
|
||||
settings.setValue(QStringLiteral("elements-collections/material-list-header"), state);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::savedDialogSize
|
||||
@return the size the selection window had the last time it was closed,
|
||||
an invalid size when it was never opened.
|
||||
*/
|
||||
QSize MaterialList::savedDialogSize()
|
||||
{
|
||||
QSettings settings;
|
||||
const QSize size(settings.value(QStringLiteral("elements-collections/material-list-width")).toInt(),
|
||||
settings.value(QStringLiteral("elements-collections/material-list-height")).toInt());
|
||||
//A size which was never written reads back as 0x0, which Qt still
|
||||
//counts as valid : it must not be handed to resize().
|
||||
return size.isEmpty() ? QSize() : size;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::saveDialogSize
|
||||
Remember the size of the selection window.
|
||||
@param size
|
||||
*/
|
||||
void MaterialList::saveDialogSize(const QSize &size)
|
||||
{
|
||||
if (size.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
QSettings settings;
|
||||
settings.setValue(QStringLiteral("elements-collections/material-list-width"), size.width());
|
||||
settings.setValue(QStringLiteral("elements-collections/material-list-height"), size.height());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::defaultColumns
|
||||
@return the columns of a brand new material file, in writing order.
|
||||
It is every information of the main article (everything but label and
|
||||
formula, which QElectroTech computes itself) plus the one column that
|
||||
describes an auxiliary block.
|
||||
*/
|
||||
QStringList MaterialList::defaultColumns()
|
||||
{
|
||||
QStringList columns = columnsForBlock(0);
|
||||
columns << QStringLiteral("auxiliary");
|
||||
return columns;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::columnsForBlock
|
||||
Columns of the material file that can be transferred to a block of the
|
||||
element informations.
|
||||
@param block 0 for the main article, 1 to 4 for an auxiliary article
|
||||
@return
|
||||
*/
|
||||
QStringList MaterialList::columnsForBlock(int block)
|
||||
{
|
||||
static const QStringList main_columns = {
|
||||
QStringLiteral("description"),
|
||||
QStringLiteral("designation"),
|
||||
QStringLiteral("manufacturer"),
|
||||
QStringLiteral("manufacturer_reference"),
|
||||
QStringLiteral("machine_manufacturer_reference"),
|
||||
QStringLiteral("supplier"),
|
||||
QStringLiteral("quantity"),
|
||||
QStringLiteral("unity"),
|
||||
QStringLiteral("function"),
|
||||
QStringLiteral("comment"),
|
||||
QStringLiteral("notes"),
|
||||
QStringLiteral("model"),
|
||||
QStringLiteral("category"),
|
||||
QStringLiteral("voltage_rating"),
|
||||
QStringLiteral("current_rating"),
|
||||
QStringLiteral("plant"),
|
||||
QStringLiteral("location"),
|
||||
QStringLiteral("width"),
|
||||
QStringLiteral("height"),
|
||||
QStringLiteral("depth")
|
||||
};
|
||||
|
||||
static const QStringList auxiliary_columns = {
|
||||
QStringLiteral("description"),
|
||||
QStringLiteral("designation"),
|
||||
QStringLiteral("manufacturer"),
|
||||
QStringLiteral("manufacturer_reference"),
|
||||
QStringLiteral("machine_manufacturer_reference"),
|
||||
QStringLiteral("supplier"),
|
||||
QStringLiteral("quantity"),
|
||||
QStringLiteral("unity"),
|
||||
QStringLiteral("auxiliary")
|
||||
};
|
||||
|
||||
if (block <= 0) {
|
||||
return main_columns;
|
||||
}
|
||||
if (block > 4) {
|
||||
return QStringList();
|
||||
}
|
||||
return auxiliary_columns;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::elementInfoKey
|
||||
Tell which element information a column of the material file feeds
|
||||
when it is applied to a block.
|
||||
@param column a column of the material file
|
||||
@param block 0 for the main article, 1 to 4 for an auxiliary article
|
||||
@return the element information key, or a null QString when that column
|
||||
does not belong to that block
|
||||
*/
|
||||
QString MaterialList::elementInfoKey(const QString &column, int block)
|
||||
{
|
||||
if (!columnsForBlock(block).contains(column)) {
|
||||
return QString();
|
||||
}
|
||||
|
||||
if (block <= 0) {
|
||||
return column;
|
||||
}
|
||||
|
||||
//Every auxiliary column carries the same name as its main counterpart,
|
||||
//only the suffix tells the block apart. The block description column
|
||||
//is the exception: it is named auxiliary in the file and auxiliary1 to
|
||||
//auxiliary4 in the element informations.
|
||||
if (column == QLatin1String("auxiliary")) {
|
||||
return QStringLiteral("auxiliary%1").arg(block);
|
||||
}
|
||||
return column + QStringLiteral("_auxiliary%1").arg(block);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::translatedColumn
|
||||
@param column a column of the material file
|
||||
@return that column named the way it is shown to the user, in the
|
||||
current language. A column QElectroTech doesn't know is returned as it
|
||||
is, since it can only have been named by the user.
|
||||
*/
|
||||
QString MaterialList::translatedColumn(const QString &column)
|
||||
{
|
||||
if (column == QLatin1String("auxiliary")) {
|
||||
return QCoreApplication::translate("MaterialList", "Bloc auxiliaire");
|
||||
}
|
||||
|
||||
const QString translated = QETInformation::translatedInfoKey(column);
|
||||
return translated.isEmpty() ? column : translated;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::translatedHeader
|
||||
The label line of a material file : the columns the way the user reads
|
||||
them, in the current language.
|
||||
@param columns
|
||||
@return
|
||||
*/
|
||||
QStringList MaterialList::translatedHeader(const QStringList &columns)
|
||||
{
|
||||
QStringList header;
|
||||
header.reserve(columns.size());
|
||||
for (const QString &column : columns) {
|
||||
header.append(translatedColumn(column));
|
||||
}
|
||||
return header;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::headerAliasMap
|
||||
@return every accepted header cell of a material file, lowercased,
|
||||
mapped to the canonical column it means.
|
||||
Beside the key itself, a header may use the translated name of the
|
||||
information ("Artikelbeschreibung" for description), so a file written
|
||||
by hand in the user's language is understood as well as the canonical
|
||||
one.
|
||||
*/
|
||||
QMap<QString, QString> MaterialList::headerAliasMap()
|
||||
{
|
||||
QMap<QString, QString> map;
|
||||
const QStringList keys = QETInformation::elementInfoKeys();
|
||||
|
||||
for (const QString &key : keys)
|
||||
{
|
||||
map.insert(key.toLower(), key);
|
||||
const QString translated = QETInformation::translatedInfoKey(key);
|
||||
if (!translated.isEmpty()) {
|
||||
map.insert(translated.toLower(), key);
|
||||
}
|
||||
}
|
||||
|
||||
map.insert(QStringLiteral("auxiliary"), QStringLiteral("auxiliary"));
|
||||
const QString auxiliary_label = QCoreApplication::translate("MaterialList", "Bloc auxiliaire").toLower();
|
||||
map.insert(auxiliary_label, QStringLiteral("auxiliary"));
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::canonicalColumn
|
||||
Turn one header cell into a canonical column name.
|
||||
@param header_cell
|
||||
@param already_used the names chosen for the previous cells, to keep
|
||||
the header free of duplicates
|
||||
@param alias_map lowercased alias -> canonical name
|
||||
@return
|
||||
*/
|
||||
QString MaterialList::canonicalColumn(const QString &header_cell,
|
||||
const QStringList &already_used,
|
||||
const QMap<QString, QString> &alias_map)
|
||||
{
|
||||
const QString raw = header_cell.trimmed();
|
||||
|
||||
if (raw.isEmpty()) {
|
||||
//A nameless column keeps its data and its place, it just can't be
|
||||
//matched to an element information.
|
||||
QString name = QStringLiteral("column_%1").arg(already_used.size() + 1);
|
||||
int i = already_used.size() + 1;
|
||||
while (already_used.contains(name)) {
|
||||
name = QStringLiteral("column_%1").arg(++i);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
const auto it = alias_map.constFind(raw.toLower());
|
||||
if (it != alias_map.constEnd() && !already_used.contains(it.value())) {
|
||||
return it.value();
|
||||
}
|
||||
|
||||
//Either an unknown name, or a second cell meaning something which is
|
||||
//already used : the cell is kept as it is written, its data is never
|
||||
//thrown away.
|
||||
QString name = raw;
|
||||
int i = 2;
|
||||
while (already_used.contains(name)) {
|
||||
name = raw + QStringLiteral("_%1").arg(i++);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::isMachineHeaderLine
|
||||
Tell whether the second line of a material file is the machine header
|
||||
QElectroTech writes under the label line : the canonical column names,
|
||||
kept so that a file written in one language is still understood in
|
||||
another one.
|
||||
|
||||
The check is deliberately strict about what it recognises and lenient
|
||||
about the rest : every cell is either a known column name, or the very
|
||||
same text as the label above it (which is how an unknown column shows
|
||||
up in both lines). A data row holding free text never passes.
|
||||
@param label_line the first line of the file, the one the user reads
|
||||
@param candidate the line which may be the machine header
|
||||
@param alias_map lowercased alias -> canonical name
|
||||
@return true when candidate must not be read as a record
|
||||
*/
|
||||
bool MaterialList::isMachineHeaderLine(const QStringList &label_line,
|
||||
const QStringList &candidate,
|
||||
const QMap<QString, QString> &alias_map)
|
||||
{
|
||||
if (candidate.isEmpty() || candidate.size() != label_line.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool has_known_column = false;
|
||||
for (int i = 0; i < candidate.size(); ++i)
|
||||
{
|
||||
const QString cell = candidate.at(i).trimmed();
|
||||
if (cell.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
if (alias_map.contains(cell.toLower()))
|
||||
{
|
||||
has_known_column = true;
|
||||
continue;
|
||||
}
|
||||
if (cell == label_line.at(i).trimmed()) {
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return has_known_column;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::detectSeparator
|
||||
Tell which separator a csv text uses. Spreadsheets of the german
|
||||
region write a semicolon, the english ones a comma, some tools a tab.
|
||||
@param first_line the header line
|
||||
@return
|
||||
*/
|
||||
QChar MaterialList::detectSeparator(const QString &first_line)
|
||||
{
|
||||
const int semicolons = first_line.count(QLatin1Char(';'));
|
||||
const int tabs = first_line.count(QLatin1Char('\t'));
|
||||
const int commas = first_line.count(QLatin1Char(','));
|
||||
|
||||
if (semicolons == 0 && tabs == 0 && commas == 0) {
|
||||
return QLatin1Char(';');
|
||||
}
|
||||
if (tabs >= semicolons && tabs >= commas && tabs > 0) {
|
||||
return QLatin1Char('\t');
|
||||
}
|
||||
if (commas > semicolons) {
|
||||
return QLatin1Char(',');
|
||||
}
|
||||
return QLatin1Char(';');
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::parseCsv
|
||||
Split a csv text into rows. Quoted fields may contain the separator
|
||||
and line breaks, doubled quotes mean one quote.
|
||||
@param content
|
||||
@param separator
|
||||
@return
|
||||
*/
|
||||
QList<QStringList> MaterialList::parseCsv(const QString &content, QChar separator)
|
||||
{
|
||||
QList<QStringList> rows;
|
||||
QStringList row;
|
||||
QString field;
|
||||
bool quoted = false;
|
||||
|
||||
for (int i = 0; i < content.size(); ++i)
|
||||
{
|
||||
const QChar c = content.at(i);
|
||||
|
||||
if (quoted)
|
||||
{
|
||||
if (c == QLatin1Char('"'))
|
||||
{
|
||||
if (i + 1 < content.size() && content.at(i + 1) == QLatin1Char('"')) {
|
||||
field += QLatin1Char('"');
|
||||
++i;
|
||||
} else {
|
||||
quoted = false;
|
||||
}
|
||||
} else {
|
||||
field += c;
|
||||
}
|
||||
}
|
||||
else if (c == QLatin1Char('"') && field.isEmpty())
|
||||
{
|
||||
quoted = true;
|
||||
}
|
||||
else if (c == separator)
|
||||
{
|
||||
row.append(field);
|
||||
field.clear();
|
||||
}
|
||||
else if (c == QLatin1Char('\n'))
|
||||
{
|
||||
row.append(field);
|
||||
field.clear();
|
||||
rows.append(row);
|
||||
row.clear();
|
||||
}
|
||||
else if (c != QLatin1Char('\r'))
|
||||
{
|
||||
field += c;
|
||||
}
|
||||
}
|
||||
|
||||
if (!field.isEmpty() || !row.isEmpty()) {
|
||||
row.append(field);
|
||||
rows.append(row);
|
||||
}
|
||||
|
||||
//A trailing line break produces a last, empty row : without this it
|
||||
//would show up as a blank line in the selection dialog.
|
||||
while (!rows.isEmpty() && rows.last().size() == 1 && rows.last().first().isEmpty()) {
|
||||
rows.removeLast();
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::serializeCsv
|
||||
Build the content of a csv file : utf-8 BOM, quoted fields, one line
|
||||
per record. Same dialect as the bill of material export.
|
||||
The file opens with two header lines : the columns the way the user
|
||||
reads them, then the canonical names QElectroTech matches against the
|
||||
element informations.
|
||||
@param header_lines the header lines, in writing order
|
||||
@param rows
|
||||
@param separator
|
||||
@return
|
||||
*/
|
||||
QByteArray MaterialList::serializeCsv(const QList<QStringList> &header_lines, const QList<QStringList> &rows, QChar separator)
|
||||
{
|
||||
//Helpers duplicated from the bill of material export : every field is
|
||||
//quoted, so a semicolon inside a value is never read as a separator.
|
||||
auto escape = [](QString value) {
|
||||
value.replace(QLatin1Char('"'), QStringLiteral("\"\""));
|
||||
return QLatin1Char('"') + value + QLatin1Char('"');
|
||||
};
|
||||
|
||||
auto line = [&](const QStringList &values) {
|
||||
QStringList escaped;
|
||||
escaped.reserve(values.size());
|
||||
for (const QString &value : values) {
|
||||
escaped.append(escape(value));
|
||||
}
|
||||
return escaped.join(separator) + QLatin1Char('\n');
|
||||
};
|
||||
|
||||
QByteArray out("\xEF\xBB\xBF");
|
||||
for (const QStringList &header_line : header_lines) {
|
||||
out += line(header_line).toUtf8();
|
||||
}
|
||||
for (const QStringList &row : rows) {
|
||||
out += line(row).toUtf8();
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::load
|
||||
Read a material file.
|
||||
@param path
|
||||
@param data receives the columns and the records
|
||||
@param error receives a message when the file can't be read
|
||||
@return true on success
|
||||
*/
|
||||
bool MaterialList::load(const QString &path, MaterialListData *data, QString *error)
|
||||
{
|
||||
if (error) {
|
||||
error->clear();
|
||||
}
|
||||
if (!data) {
|
||||
if (error) {
|
||||
*error = QCoreApplication::translate("MaterialList", "Aucun récepteur pour le répertoire de matériaux.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
data->columns.clear();
|
||||
data->records.clear();
|
||||
data->separator = QLatin1Char(';');
|
||||
|
||||
QFile file(path);
|
||||
if (!file.open(QIODevice::ReadOnly))
|
||||
{
|
||||
if (error) {
|
||||
*error = file.errorString();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const QByteArray raw = file.readAll();
|
||||
file.close();
|
||||
|
||||
const QString content = decode(raw);
|
||||
if (content.trimmed().isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const int eol = content.indexOf(QLatin1Char('\n'));
|
||||
data->separator = detectSeparator(eol < 0 ? content : content.left(eol));
|
||||
|
||||
QList<QStringList> rows = parseCsv(content, data->separator);
|
||||
if (rows.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const QStringList header = rows.takeFirst();
|
||||
const QMap<QString, QString> alias_map = headerAliasMap();
|
||||
|
||||
//The label line is followed by the machine header QElectroTech
|
||||
//writes itself : the canonical names, so that the file is read the
|
||||
//same way whatever the language it was written in.
|
||||
if (!rows.isEmpty() && isMachineHeaderLine(header, rows.first(), alias_map)) {
|
||||
rows.takeFirst();
|
||||
}
|
||||
|
||||
QStringList used;
|
||||
for (const QString &cell : header)
|
||||
{
|
||||
const QString column = canonicalColumn(cell, used, alias_map);
|
||||
used.append(column);
|
||||
data->columns.append(column);
|
||||
}
|
||||
|
||||
for (const QStringList &row : std::as_const(rows))
|
||||
{
|
||||
MaterialRecord record;
|
||||
bool empty = true;
|
||||
for (int i = 0; i < data->columns.size(); ++i)
|
||||
{
|
||||
const QString value = i < row.size() ? row.at(i) : QString();
|
||||
if (!value.isEmpty()) {
|
||||
empty = false;
|
||||
}
|
||||
record.setValue(data->columns.at(i), value);
|
||||
}
|
||||
if (!empty) {
|
||||
data->records.append(record);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::writeFile
|
||||
Write a whole material file atomically : a crash or a full disk never
|
||||
leaves a half written catalogue behind.
|
||||
@param path
|
||||
@param data
|
||||
@param error receives a message when the file can't be written
|
||||
@return true on success
|
||||
*/
|
||||
bool MaterialList::writeFile(const QString &path, const MaterialListData &data, QString *error)
|
||||
{
|
||||
if (error) {
|
||||
error->clear();
|
||||
}
|
||||
|
||||
QList<QStringList> rows;
|
||||
rows.reserve(data.records.size());
|
||||
for (const MaterialRecord &record : data.records)
|
||||
{
|
||||
QStringList row;
|
||||
row.reserve(data.columns.size());
|
||||
for (const QString &column : data.columns) {
|
||||
row.append(record.value(column));
|
||||
}
|
||||
rows.append(row);
|
||||
}
|
||||
|
||||
QList<QStringList> header_lines;
|
||||
header_lines.append(translatedHeader(data.columns));
|
||||
header_lines.append(data.columns);
|
||||
const QByteArray content = serializeCsv(header_lines, rows, data.separator);
|
||||
|
||||
QSaveFile file(path);
|
||||
if (!file.open(QIODevice::WriteOnly) || file.write(content) != content.size())
|
||||
{
|
||||
if (error) {
|
||||
*error = file.errorString();
|
||||
}
|
||||
file.cancelWriting();
|
||||
return false;
|
||||
}
|
||||
if (!file.commit())
|
||||
{
|
||||
if (error) {
|
||||
*error = file.errorString();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::appendRecord
|
||||
Add one article at the end of a material file.
|
||||
The file is read again first: changes the user just saved from his
|
||||
spreadsheet are part of what gets rewritten, never dropped.
|
||||
@param path
|
||||
@param record
|
||||
@param error receives a message when the file can't be written
|
||||
@return true on success
|
||||
*/
|
||||
bool MaterialList::appendRecord(const QString &path, const MaterialRecord &record, QString *error)
|
||||
{
|
||||
if (error) {
|
||||
error->clear();
|
||||
}
|
||||
|
||||
MaterialListData data;
|
||||
if (hasContent(path))
|
||||
{
|
||||
if (!load(path, &data, error)) {
|
||||
return false;
|
||||
}
|
||||
if (data.columns.isEmpty())
|
||||
{
|
||||
if (error) {
|
||||
*error = QCoreApplication::translate("MaterialList", "Le fichier ne contient pas d'en-tête : colonnes manquantes.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
data.columns = defaultColumns();
|
||||
data.separator = QLatin1Char(';');
|
||||
}
|
||||
|
||||
data.records.append(record);
|
||||
return writeFile(path, data, error);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::createFile
|
||||
Create an empty material file holding only the header line.
|
||||
@param path
|
||||
@param error receives a message when the file can't be written
|
||||
@return true on success
|
||||
*/
|
||||
bool MaterialList::createFile(const QString &path, QString *error)
|
||||
{
|
||||
if (error) {
|
||||
error->clear();
|
||||
}
|
||||
|
||||
if (hasContent(path))
|
||||
{
|
||||
if (error) {
|
||||
*error = QCoreApplication::translate("MaterialList", "Le fichier existe déjà et n'est pas vide.");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
MaterialListData data;
|
||||
data.columns = defaultColumns();
|
||||
data.separator = QLatin1Char(';');
|
||||
return writeFile(path, data, error);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialList::isEmptyFile
|
||||
@param path
|
||||
@return true when no file, or an empty one, exists at path
|
||||
*/
|
||||
bool MaterialList::isEmptyFile(const QString &path)
|
||||
{
|
||||
return !hasContent(path);
|
||||
}
|
||||
@@ -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 MATERIALLIST_H
|
||||
#define MATERIALLIST_H
|
||||
|
||||
#include <QByteArray>
|
||||
#include <QChar>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QSize>
|
||||
#include <QString>
|
||||
#include <QStringList>
|
||||
|
||||
/**
|
||||
@brief One line of the material file.
|
||||
|
||||
Keys are column names : either a canonical element information key
|
||||
(description, manufacturer...) or the raw name of a column added by
|
||||
the user, which QElectroTech does not know about and therefore never
|
||||
Transfers to an element, but keeps intact when a row is appended.
|
||||
*/
|
||||
struct MaterialRecord
|
||||
{
|
||||
QMap<QString, QString> values;
|
||||
|
||||
QString value(const QString &column) const {return values.value(column);}
|
||||
void setValue(const QString &column, const QString &value) {values.insert(column, value);}
|
||||
bool operator==(const MaterialRecord &other) const {return values == other.values;}
|
||||
};
|
||||
|
||||
/**
|
||||
@brief The whole content of a material file.
|
||||
|
||||
Both the column names and the separator are read from the file rather
|
||||
than imposed, so a file edited in a spreadsheet keeps working : the
|
||||
separator is detected (;, tab or ,), unknown columns are preserved, and
|
||||
only the header cells that can be matched to a known element
|
||||
information are canonicalized.
|
||||
*/
|
||||
struct MaterialListData
|
||||
{
|
||||
QStringList columns;
|
||||
QList<MaterialRecord> records;
|
||||
QChar separator = QLatin1Char(';');
|
||||
|
||||
bool isEmpty() const {return columns.isEmpty() && records.isEmpty();}
|
||||
};
|
||||
|
||||
/**
|
||||
@brief The MaterialList class reads and writes the material file (a CSV
|
||||
file listing purchasable articles) and maps its columns onto element
|
||||
informations.
|
||||
|
||||
This is a pure data class : it does not know anything about widgets,
|
||||
so it stays usable from the tests and from the command line.
|
||||
*/
|
||||
class MaterialList
|
||||
{
|
||||
public:
|
||||
//Settings
|
||||
static QString settingsKey();
|
||||
static QString configuredPath();
|
||||
static void setConfiguredPath(const QString &path);
|
||||
static QString defaultPath();
|
||||
static QString defaultFileName();
|
||||
|
||||
//Geometry of the selection window, remembered from one opening
|
||||
//to the next : column widths are part of how the user reads his
|
||||
//own catalogue and must not be lost when the window closes.
|
||||
static QByteArray savedHeaderState();
|
||||
static void saveHeaderState(const QByteArray &state);
|
||||
static QSize savedDialogSize();
|
||||
static void saveDialogSize(const QSize &size);
|
||||
|
||||
//Columns
|
||||
static QStringList defaultColumns();
|
||||
static QStringList columnsForBlock(int block);
|
||||
static QString elementInfoKey(const QString &column, int block);
|
||||
static QString translatedColumn(const QString &column);
|
||||
static QStringList translatedHeader(const QStringList &columns);
|
||||
|
||||
//File io
|
||||
static bool load(const QString &path, MaterialListData *data, QString *error = nullptr);
|
||||
static bool writeFile(const QString &path, const MaterialListData &data, QString *error = nullptr);
|
||||
static bool appendRecord(const QString &path, const MaterialRecord &record, QString *error = nullptr);
|
||||
static bool createFile(const QString &path, QString *error = nullptr);
|
||||
static bool isEmptyFile(const QString &path);
|
||||
|
||||
//Csv helpers, public only to keep them testable
|
||||
static QList<QStringList> parseCsv(const QString &content, QChar separator);
|
||||
static QByteArray serializeCsv(const QList<QStringList> &header_lines,
|
||||
const QList<QStringList> &rows,
|
||||
QChar separator);
|
||||
static QChar detectSeparator(const QString &first_line);
|
||||
|
||||
private:
|
||||
static QString canonicalColumn(const QString &header_cell,
|
||||
const QStringList &already_used,
|
||||
const QMap<QString, QString> &alias_map);
|
||||
static QMap<QString, QString> headerAliasMap();
|
||||
static bool isMachineHeaderLine(const QStringList &label_line,
|
||||
const QStringList &candidate,
|
||||
const QMap<QString, QString> &alias_map);
|
||||
};
|
||||
|
||||
#endif // MATERIALLIST_H
|
||||
@@ -0,0 +1,610 @@
|
||||
/*
|
||||
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 "materialselectiondialog.h"
|
||||
|
||||
#include "materialentrydialog.h"
|
||||
#include "../qetmessagebox.h"
|
||||
#include "ui_materialselectiondialog.h"
|
||||
|
||||
#include <QAbstractButton>
|
||||
#include <QAbstractItemView>
|
||||
#include <QHeaderView>
|
||||
#include <QItemSelectionModel>
|
||||
#include <QLineEdit>
|
||||
#include <QPushButton>
|
||||
#include <QScrollBar>
|
||||
#include <QWheelEvent>
|
||||
|
||||
/**
|
||||
@brief MaterialTableModel::MaterialTableModel
|
||||
Constructor
|
||||
@param parent parent object
|
||||
*/
|
||||
MaterialTableModel::MaterialTableModel(QObject *parent) :
|
||||
QAbstractTableModel(parent)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialTableModel::setMaterial
|
||||
Replace the displayed file content
|
||||
@param columns
|
||||
@param records
|
||||
*/
|
||||
void MaterialTableModel::setMaterial(const QStringList &columns, const QList<MaterialRecord> &records)
|
||||
{
|
||||
beginResetModel();
|
||||
m_columns = columns;
|
||||
m_records = records;
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialTableModel::record
|
||||
@param row
|
||||
@return the record of that row, a null one when out of range
|
||||
*/
|
||||
MaterialRecord MaterialTableModel::record(int row) const
|
||||
{
|
||||
return m_records.value(row);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialTableModel::rowCount
|
||||
@param parent
|
||||
@return
|
||||
*/
|
||||
int MaterialTableModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
return parent.isValid() ? 0 : m_records.size();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialTableModel::columnCount
|
||||
@param parent
|
||||
@return
|
||||
*/
|
||||
int MaterialTableModel::columnCount(const QModelIndex &parent) const
|
||||
{
|
||||
return parent.isValid() ? 0 : m_columns.size();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialTableModel::data
|
||||
@param index
|
||||
@param role
|
||||
@return
|
||||
*/
|
||||
QVariant MaterialTableModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid()
|
||||
|| index.row() < 0 || index.row() >= m_records.size()
|
||||
|| index.column() < 0 || index.column() >= m_columns.size())
|
||||
{
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
const QString value = m_records.at(index.row()).value(m_columns.at(index.column()));
|
||||
|
||||
switch (role)
|
||||
{
|
||||
case Qt::DisplayRole:
|
||||
case Qt::ToolTipRole:
|
||||
//The tooltip shows the whole cell once the column is too narrow
|
||||
return value;
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialTableModel::headerData
|
||||
@param section
|
||||
@param orientation
|
||||
@param role
|
||||
@return
|
||||
*/
|
||||
QVariant MaterialTableModel::headerData(int section, Qt::Orientation orientation, int role) const
|
||||
{
|
||||
if (role != Qt::DisplayRole) {
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
if (orientation == Qt::Horizontal)
|
||||
{
|
||||
if (section >= 0 && section < m_columns.size()) {
|
||||
return MaterialList::translatedColumn(m_columns.at(section));
|
||||
}
|
||||
return QVariant();
|
||||
}
|
||||
|
||||
return section + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialFilterProxy::MaterialFilterProxy
|
||||
Constructor
|
||||
@param model the material table model, also used as source model
|
||||
@param parent parent object
|
||||
*/
|
||||
MaterialFilterProxy::MaterialFilterProxy(MaterialTableModel *model, QObject *parent) :
|
||||
QSortFilterProxyModel(parent),
|
||||
m_model(model)
|
||||
{
|
||||
setSourceModel(model);
|
||||
setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||
setDynamicSortFilter(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialFilterProxy::setTokens
|
||||
Set the words every row must hold to be shown.
|
||||
@param tokens
|
||||
*/
|
||||
void MaterialFilterProxy::setTokens(const QStringList &tokens)
|
||||
{
|
||||
if (m_tokens == tokens) {
|
||||
return;
|
||||
}
|
||||
m_tokens = tokens;
|
||||
//The plain string filter of the base class is not what keeps the
|
||||
//rows (filterAcceptsRow() below is), setting it is only how the
|
||||
//base class is told to run the filter again.
|
||||
setFilterFixedString(tokens.join(QLatin1Char(' ')));
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialFilterProxy::filterAcceptsRow
|
||||
A row is kept when each token is found in at least one of its cells :
|
||||
the words may be spread over different columns, as a search for
|
||||
"Hilfsschalter Schneider" expects.
|
||||
@param source_row
|
||||
@param source_parent
|
||||
@return
|
||||
*/
|
||||
bool MaterialFilterProxy::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
|
||||
{
|
||||
Q_UNUSED(source_parent)
|
||||
|
||||
if (m_tokens.isEmpty() || !m_model) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const QStringList columns = m_model->columns();
|
||||
for (const QString &token : m_tokens)
|
||||
{
|
||||
bool found = false;
|
||||
for (int column = 0; column < columns.size(); ++column)
|
||||
{
|
||||
const QModelIndex index = m_model->index(source_row, column);
|
||||
const QString value = m_model->data(index, Qt::DisplayRole).toString();
|
||||
if (value.contains(token, Qt::CaseInsensitive))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialSelectionDialog::MaterialSelectionDialog
|
||||
Constructor
|
||||
@param path path of the material file
|
||||
@param parent parent widget
|
||||
*/
|
||||
MaterialSelectionDialog::MaterialSelectionDialog(const QString &path,
|
||||
QWidget *parent) :
|
||||
QDialog(parent),
|
||||
ui(new Ui::MaterialSelectionDialog),
|
||||
m_path(path)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
m_model = new MaterialTableModel(this);
|
||||
m_proxy = new MaterialFilterProxy(m_model, this);
|
||||
|
||||
ui->m_table_view->setModel(m_proxy);
|
||||
ui->m_table_view->setSortingEnabled(true);
|
||||
ui->m_table_view->horizontalHeader()->setStretchLastSection(true);
|
||||
|
||||
//The wheel is taken in hand here : it moves the rows, and the
|
||||
//columns when the movement is horizontal or Shift is held.
|
||||
ui->m_table_view->viewport()->installEventFilter(this);
|
||||
|
||||
//The row numbers are the order of the file : clicking them, or the
|
||||
//corner just above them, sorts the table by them, which means the
|
||||
//table is no longer sorted at all.
|
||||
for (QAbstractButton *button : ui->m_table_view->findChildren<QAbstractButton *>())
|
||||
{
|
||||
if (button->inherits("QTableCornerButton")) {
|
||||
connect(button, &QAbstractButton::clicked,
|
||||
this, &MaterialSelectionDialog::resetSorting);
|
||||
}
|
||||
}
|
||||
ui->m_table_view->verticalHeader()->setSectionsClickable(true);
|
||||
connect(ui->m_table_view->verticalHeader(), &QHeaderView::sectionClicked,
|
||||
this, &MaterialSelectionDialog::resetSorting);
|
||||
|
||||
ui->m_button_box->button(QDialogButtonBox::Ok)->setText(tr("Appliquer"));
|
||||
ui->m_button_box->button(QDialogButtonBox::Cancel)->setText(tr("Annuler"));
|
||||
|
||||
connect(ui->m_search_edit, &QLineEdit::textChanged,
|
||||
this, &MaterialSelectionDialog::searchChanged);
|
||||
if (QItemSelectionModel *selection_model = ui->m_table_view->selectionModel())
|
||||
{
|
||||
connect(selection_model, &QItemSelectionModel::selectionChanged, this,
|
||||
[this](const QItemSelection &, const QItemSelection &) { updateApplyButton(); });
|
||||
}
|
||||
|
||||
reload();
|
||||
|
||||
if (const QSize saved_size = MaterialList::savedDialogSize(); saved_size.isValid()) {
|
||||
resize(saved_size);
|
||||
}
|
||||
|
||||
const int description_column = m_data.columns.indexOf(QStringLiteral("description"));
|
||||
if (description_column >= 0) {
|
||||
ui->m_table_view->sortByColumn(description_column, Qt::AscendingOrder);
|
||||
}
|
||||
|
||||
//Last, so that the columns the user widened, moved or sorted last
|
||||
//time are the ones he finds again.
|
||||
restoreViewState();
|
||||
|
||||
updateApplyButton();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialSelectionDialog::~MaterialSelectionDialog
|
||||
Destructor
|
||||
*/
|
||||
MaterialSelectionDialog::~MaterialSelectionDialog()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialSelectionDialog::restoreViewState
|
||||
Give the columns back the width, the place and the order they had the
|
||||
last time the window was closed.
|
||||
*/
|
||||
void MaterialSelectionDialog::restoreViewState()
|
||||
{
|
||||
const QByteArray state = MaterialList::savedHeaderState();
|
||||
if (state.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
ui->m_table_view->horizontalHeader()->restoreState(state);
|
||||
|
||||
//Every column of the file is shown, even one an older version of
|
||||
//the window had hidden because no record used it yet.
|
||||
for (int column = 0; column < ui->m_table_view->model()->columnCount(); ++column) {
|
||||
ui->m_table_view->setColumnHidden(column, false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialSelectionDialog::saveViewState
|
||||
Remember how the columns were laid out, and how big the window was.
|
||||
*/
|
||||
void MaterialSelectionDialog::saveViewState()
|
||||
{
|
||||
MaterialList::saveHeaderState(ui->m_table_view->horizontalHeader()->saveState());
|
||||
MaterialList::saveDialogSize(size());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialSelectionDialog::resetSorting
|
||||
Give the table back the order of the file: no column sorted and no
|
||||
arrow in the header, the rows counting from one down again.
|
||||
*/
|
||||
void MaterialSelectionDialog::resetSorting()
|
||||
{
|
||||
//Sorting by the row numbers is sorting by nothing : without a
|
||||
//column to sort by, the proxy writes the rows the way the file
|
||||
//does.
|
||||
m_proxy->sort(-1, Qt::AscendingOrder);
|
||||
ui->m_table_view->horizontalHeader()->setSortIndicator(-1, Qt::AscendingOrder);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialSelectionDialog::done
|
||||
Every way out of the window goes through here, so a column resized by
|
||||
the user is never lost, be it "Apply", "Cancel" or the window manager
|
||||
close button.
|
||||
@param result
|
||||
*/
|
||||
void MaterialSelectionDialog::done(int result)
|
||||
{
|
||||
saveViewState();
|
||||
QDialog::done(result);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialSelectionDialog::eventFilter
|
||||
The table takes the wheel in hand: the rows must move up and down and
|
||||
the columns left and right, whether the platform does anything with
|
||||
wheel events or not.
|
||||
@param watched
|
||||
@param event
|
||||
@return true when the event was handled here
|
||||
*/
|
||||
bool MaterialSelectionDialog::eventFilter(QObject *watched, QEvent *event)
|
||||
{
|
||||
if (watched == ui->m_table_view->viewport() && event->type() == QEvent::Wheel)
|
||||
{
|
||||
scrollTable(static_cast<QWheelEvent *>(event));
|
||||
return true;
|
||||
}
|
||||
return QDialog::eventFilter(watched, event);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief wheelAmount
|
||||
Turn one wheel movement into an amount to scroll.
|
||||
|
||||
@param delta the movement, in eighths of a degree (120 per notch) or
|
||||
in pixels, depending on the device
|
||||
@param counting_items true when the scroll bar counts rows and columns
|
||||
instead of pixels, which is how the scroll bars of a table work
|
||||
@return how far the scroll bar moves, in its own unit
|
||||
*/
|
||||
static int wheelAmount(int delta, bool counting_items)
|
||||
{
|
||||
if (counting_items) {
|
||||
return 3 * qRound(double(delta) / QWheelEvent::DefaultDeltasPerStep);
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialSelectionDialog::scrollTable
|
||||
Scroll the table with the wheel, in both directions.
|
||||
|
||||
A wheel which only turns moves the rows. A movement reported on the
|
||||
horizontal axis (a wheel which tilts, a touchpad swiped sideways), or
|
||||
Shift held down on a plain wheel, moves the columns instead, which is
|
||||
how the rest of a wide catalogue is reached.
|
||||
|
||||
Three rows or columns travel with every notch, the same amount Qt
|
||||
itself scrolls when it gets the wheel.
|
||||
@param event
|
||||
*/
|
||||
void MaterialSelectionDialog::scrollTable(QWheelEvent *event)
|
||||
{
|
||||
QPoint delta = event->angleDelta();
|
||||
if (delta.isNull()) {
|
||||
delta = event->pixelDelta();
|
||||
}
|
||||
|
||||
//Shift turns the wheel movement sideways.
|
||||
if ((event->modifiers() & Qt::ShiftModifier) && delta.x() == 0 && delta.y() != 0) {
|
||||
delta.setX(delta.y());
|
||||
delta.setY(0);
|
||||
}
|
||||
if (delta.isNull()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (delta.y() != 0 && ui->m_table_view->verticalScrollBar())
|
||||
{
|
||||
const bool counting_rows =
|
||||
ui->m_table_view->verticalScrollMode() == QAbstractItemView::ScrollPerItem;
|
||||
QScrollBar *bar = ui->m_table_view->verticalScrollBar();
|
||||
bar->setValue(bar->value() - wheelAmount(delta.y(), counting_rows));
|
||||
}
|
||||
|
||||
if (delta.x() != 0 && ui->m_table_view->horizontalScrollBar())
|
||||
{
|
||||
const bool counting_columns =
|
||||
ui->m_table_view->horizontalScrollMode() == QAbstractItemView::ScrollPerItem;
|
||||
QScrollBar *bar = ui->m_table_view->horizontalScrollBar();
|
||||
bar->setValue(bar->value() - wheelAmount(delta.x(), counting_columns));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialSelectionDialog::reload
|
||||
Read the material file again, so that anything the user saved from his
|
||||
spreadsheet since the dialog was built shows up.
|
||||
*/
|
||||
void MaterialSelectionDialog::reload()
|
||||
{
|
||||
QString error;
|
||||
MaterialListData data;
|
||||
|
||||
if (!MaterialList::load(m_path, &data, &error))
|
||||
{
|
||||
QET::QetMessageBox::warning(this,
|
||||
tr("Lecture impossible"),
|
||||
tr("Impossible de lire le fichier :\n%1\n%2")
|
||||
.arg(m_path, error));
|
||||
data = MaterialListData();
|
||||
}
|
||||
|
||||
m_data = data;
|
||||
m_model->setMaterial(m_data.columns, m_data.records);
|
||||
updateCount();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialSelectionDialog::updateCount
|
||||
Tell how many entries are shown.
|
||||
*/
|
||||
void MaterialSelectionDialog::updateCount()
|
||||
{
|
||||
const int total = m_model->rowCount();
|
||||
const int shown = m_proxy->rowCount();
|
||||
|
||||
if (m_proxy->tokens().isEmpty()) {
|
||||
ui->m_count_label->setText(tr("Entrées : %1").arg(total));
|
||||
} else {
|
||||
ui->m_count_label->setText(tr("Entrées : %1 sur %2").arg(shown).arg(total));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialSelectionDialog::updateApplyButton
|
||||
Apply is only meaningful with an entry selected.
|
||||
*/
|
||||
void MaterialSelectionDialog::updateApplyButton()
|
||||
{
|
||||
QPushButton *button = ui->m_button_box->button(QDialogButtonBox::Ok);
|
||||
if (button) {
|
||||
button->setEnabled(!selectedRecord().values.isEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialSelectionDialog::searchChanged
|
||||
Refilter the table each time the search field changes.
|
||||
@param text
|
||||
*/
|
||||
void MaterialSelectionDialog::searchChanged(const QString &text)
|
||||
{
|
||||
m_proxy->setTokens(text.simplified().split(QLatin1Char(' '), Qt::SkipEmptyParts));
|
||||
updateCount();
|
||||
updateApplyButton();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialSelectionDialog::selectedRecord
|
||||
@return the selected entry, a null one when nothing is selected
|
||||
*/
|
||||
MaterialRecord MaterialSelectionDialog::selectedRecord() const
|
||||
{
|
||||
if (!m_proxy || !ui->m_table_view->selectionModel()) {
|
||||
return MaterialRecord();
|
||||
}
|
||||
|
||||
const QModelIndexList rows = ui->m_table_view->selectionModel()->selectedRows();
|
||||
if (rows.isEmpty()) {
|
||||
return MaterialRecord();
|
||||
}
|
||||
|
||||
const QModelIndex source = m_proxy->mapToSource(rows.first());
|
||||
if (!source.isValid()) {
|
||||
return MaterialRecord();
|
||||
}
|
||||
|
||||
return m_model->record(source.row());
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialSelectionDialog::selectRecord
|
||||
Select the row holding that record, scrolling to it.
|
||||
@param record
|
||||
@return true when the record was found and is visible
|
||||
*/
|
||||
bool MaterialSelectionDialog::selectRecord(const MaterialRecord &record)
|
||||
{
|
||||
const QList<MaterialRecord> records = m_model->records();
|
||||
int row = -1;
|
||||
//The newest match wins : appending an entry twice in a row selects
|
||||
//the line which was just written.
|
||||
for (int i = records.size() - 1; i >= 0; --i)
|
||||
{
|
||||
if (records.at(i) == record)
|
||||
{
|
||||
row = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (row < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const QModelIndex proxy = m_proxy->mapFromSource(m_model->index(row, 0));
|
||||
if (!proxy.isValid()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ui->m_table_view->setCurrentIndex(proxy);
|
||||
ui->m_table_view->selectionModel()->select(
|
||||
proxy, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
|
||||
ui->m_table_view->scrollTo(proxy);
|
||||
updateApplyButton();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialSelectionDialog::accept
|
||||
Refuse to close the dialog without a selection: "Apply" with nothing
|
||||
chosen would silently do nothing.
|
||||
*/
|
||||
void MaterialSelectionDialog::accept()
|
||||
{
|
||||
if (selectedRecord().values.isEmpty())
|
||||
{
|
||||
QET::QetMessageBox::information(this,
|
||||
tr("Aucune sélection"),
|
||||
tr("Sélectionnez d'abord un article dans la liste."));
|
||||
return;
|
||||
}
|
||||
|
||||
QDialog::accept();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief MaterialSelectionDialog::on_m_new_entry_btn_clicked
|
||||
Open the entry form and, once it is filled, append the article to the
|
||||
material file, then select it in the table.
|
||||
*/
|
||||
void MaterialSelectionDialog::on_m_new_entry_btn_clicked()
|
||||
{
|
||||
QStringList columns = m_data.columns;
|
||||
if (columns.isEmpty()) {
|
||||
columns = MaterialList::defaultColumns();
|
||||
}
|
||||
|
||||
MaterialEntryDialog entry_dialog(columns, this);
|
||||
if (entry_dialog.exec() != QDialog::Accepted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const MaterialRecord record = entry_dialog.record();
|
||||
|
||||
QString error;
|
||||
if (!MaterialList::appendRecord(m_path, record, &error))
|
||||
{
|
||||
QET::QetMessageBox::critical(this,
|
||||
tr("Écriture impossible"),
|
||||
tr("Impossible d'écrire dans le fichier :\n%1\n%2")
|
||||
.arg(m_path, error));
|
||||
return;
|
||||
}
|
||||
|
||||
//The file is read again so the new line is found where it really
|
||||
//is, and not guessed from where it was before the write.
|
||||
reload();
|
||||
|
||||
//A search hiding the new article would leave the user with a form
|
||||
//he just filled and no line to see.
|
||||
if (!selectRecord(record))
|
||||
{
|
||||
ui->m_search_edit->clear();
|
||||
selectRecord(record);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
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 MATERIALSELECTIONDIALOG_H
|
||||
#define MATERIALSELECTIONDIALOG_H
|
||||
|
||||
#include "../materiallist/materiallist.h"
|
||||
|
||||
#include <QAbstractTableModel>
|
||||
#include <QDialog>
|
||||
#include <QSortFilterProxyModel>
|
||||
|
||||
class QModelIndex;
|
||||
class QItemSelectionModel;
|
||||
class QWheelEvent;
|
||||
|
||||
namespace Ui {
|
||||
class MaterialSelectionDialog;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief Table model showing a material file.
|
||||
|
||||
Every column of the file is a column of the model, named the way the
|
||||
user reads it (translated), so the dialog stays a plain read only
|
||||
table whatever the file contains.
|
||||
*/
|
||||
class MaterialTableModel : public QAbstractTableModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MaterialTableModel(QObject *parent = nullptr);
|
||||
|
||||
void setMaterial(const QStringList &columns, const QList<MaterialRecord> &records);
|
||||
QStringList columns() const {return m_columns;}
|
||||
QList<MaterialRecord> records() const {return m_records;}
|
||||
MaterialRecord record(int row) const;
|
||||
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
int columnCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
|
||||
|
||||
private:
|
||||
QStringList m_columns;
|
||||
QList<MaterialRecord> m_records;
|
||||
};
|
||||
|
||||
/**
|
||||
@brief Filter keeping only the rows matching every word typed by the
|
||||
user, whatever the column each word is found in.
|
||||
|
||||
"Hilfsschalter Schneider" keeps the rows holding both words, so adding
|
||||
the manufacturer to the search narrows the result down, which is the
|
||||
behaviour of a bill of material lookup.
|
||||
*/
|
||||
class MaterialFilterProxy : public QSortFilterProxyModel
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MaterialFilterProxy(MaterialTableModel *model, QObject *parent = nullptr);
|
||||
|
||||
void setTokens(const QStringList &tokens);
|
||||
QStringList tokens() const {return m_tokens;}
|
||||
|
||||
protected:
|
||||
bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override;
|
||||
|
||||
private:
|
||||
MaterialTableModel *m_model = nullptr;
|
||||
QStringList m_tokens;
|
||||
};
|
||||
|
||||
/**
|
||||
@brief The dialog listing every article of the material file, letting
|
||||
the user search one and hand it back to the element properties.
|
||||
|
||||
Three buttons: apply the selected row, do nothing, or add a new row to
|
||||
the file.
|
||||
*/
|
||||
class MaterialSelectionDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MaterialSelectionDialog(const QString &path,
|
||||
QWidget *parent = nullptr);
|
||||
~MaterialSelectionDialog() override;
|
||||
|
||||
MaterialRecord selectedRecord() const;
|
||||
void accept() override;
|
||||
void done(int result) override;
|
||||
bool eventFilter(QObject *watched, QEvent *event) override;
|
||||
|
||||
private slots:
|
||||
void on_m_new_entry_btn_clicked();
|
||||
void searchChanged(const QString &text);
|
||||
|
||||
private:
|
||||
void reload();
|
||||
void restoreViewState();
|
||||
void saveViewState();
|
||||
void resetSorting();
|
||||
void scrollTable(QWheelEvent *event);
|
||||
void updateCount();
|
||||
void updateApplyButton();
|
||||
bool selectRecord(const MaterialRecord &record);
|
||||
|
||||
//ATTRIBUTES
|
||||
private:
|
||||
Ui::MaterialSelectionDialog *ui;
|
||||
MaterialTableModel *m_model = nullptr;
|
||||
MaterialFilterProxy *m_proxy = nullptr;
|
||||
QString m_path;
|
||||
MaterialListData m_data;
|
||||
};
|
||||
|
||||
#endif // MATERIALSELECTIONDIALOG_H
|
||||
@@ -0,0 +1,150 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MaterialSelectionDialog</class>
|
||||
<widget class="QDialog" name="MaterialSelectionDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1120</width>
|
||||
<height>800</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>780</width>
|
||||
<height>560</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Liste de matériaux</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="m_search_layout">
|
||||
<item>
|
||||
<widget class="QLabel" name="m_search_label">
|
||||
<property name="text">
|
||||
<string>Rechercher :</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="m_search_edit">
|
||||
<property name="toolTip">
|
||||
<string extracomment="placeholder of the search field of the material list; several words narrow the results down (AND)">Tous les mots saisis doivent apparaître quelque part dans la ligne</string>
|
||||
</property>
|
||||
<property name="clearButtonEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string extracomment="placeholder of the search field of the material list">Rechercher dans toutes les colonnes</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTableView" name="m_table_view">
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="alternatingRowColors">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::SingleSelection</enum>
|
||||
</property>
|
||||
<property name="sortingEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="m_count_label">
|
||||
<property name="text">
|
||||
<string>Entrées : 0</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="m_buttons_layout">
|
||||
<item>
|
||||
<widget class="QPushButton" name="m_new_entry_btn">
|
||||
<property name="toolTip">
|
||||
<string extracomment="button adding a new row to the material list file">Ajouter un nouvel article à la liste</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Nouvelle entrée</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="m_buttons_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>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="m_button_box">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>m_button_box</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>MaterialSelectionDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>800</x>
|
||||
<y>540</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>100</x>
|
||||
<y>100</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>m_button_box</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>MaterialSelectionDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>880</x>
|
||||
<y>540</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>100</x>
|
||||
<y>100</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
@@ -20,12 +20,14 @@
|
||||
#include "../../qetapp.h"
|
||||
#include "../../qeticons.h"
|
||||
#include "ui_generalconfigurationpage.h"
|
||||
#include "../../materiallist/materiallist.h"
|
||||
#include "../../utils/qetsettings.h"
|
||||
#include "../../utils/qetutils.h"
|
||||
#include "../../qetmessagebox.h"
|
||||
#include "../../textgrid.h"
|
||||
#include "../nokde/kcolorbutton.h"
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QFontDialog>
|
||||
#include <QSettings>
|
||||
|
||||
@@ -228,6 +230,17 @@ GeneralConfigurationPage::GeneralConfigurationPage(QWidget *parent) :
|
||||
ui->m_user_macros_path_cb->blockSignals(false);
|
||||
}
|
||||
|
||||
//MATERIAL FILE
|
||||
path = MaterialList::configuredPath();
|
||||
ui->m_material_list_path_le->setText(path);
|
||||
if (path.isEmpty())
|
||||
{
|
||||
ui->m_material_list_path_le->setPlaceholderText(
|
||||
tr("Non configuré (par défaut : %1)",
|
||||
"hint shown in the material file field when no file is configured yet")
|
||||
.arg(MaterialList::defaultPath()));
|
||||
}
|
||||
|
||||
fillLang();
|
||||
}
|
||||
|
||||
@@ -412,6 +425,12 @@ void GeneralConfigurationPage::applyConf()
|
||||
if (path != settings.value("elements-collections/macros-path").toString()) {
|
||||
QETApp::resetCollectionsPath();
|
||||
}
|
||||
|
||||
//MATERIAL FILE
|
||||
//Unlike the collections, the material file is a plain file, it is
|
||||
//kept as chosen even when it doesn't exist yet : the user may well
|
||||
//point QElectroTech at a file he intends to write later.
|
||||
MaterialList::setConfiguredPath(ui->m_material_list_path_le->text().trimmed());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -617,6 +636,70 @@ void GeneralConfigurationPage::on_m_user_macros_path_cb_currentIndexChanged(int
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief GeneralConfigurationPage::on_m_material_list_browse_pb_clicked
|
||||
Let the user pick an existing material file.
|
||||
*/
|
||||
void GeneralConfigurationPage::on_m_material_list_browse_pb_clicked()
|
||||
{
|
||||
QString start_dir = ui->m_material_list_path_le->text();
|
||||
start_dir = start_dir.isEmpty()
|
||||
? QETApp::documentDir()
|
||||
: QFileInfo(start_dir).absolutePath();
|
||||
|
||||
const QString path = QFileDialog::getOpenFileName(
|
||||
this,
|
||||
tr("Sélectionner le fichier de la liste de matériaux"),
|
||||
start_dir,
|
||||
tr("Fichiers csv (*.csv)"));
|
||||
|
||||
if (!path.isEmpty()) {
|
||||
ui->m_material_list_path_le->setText(path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief GeneralConfigurationPage::on_m_material_list_create_pb_clicked
|
||||
Create the material file with its header line, so the columns are
|
||||
known before the user fills them from his spreadsheet.
|
||||
*/
|
||||
void GeneralConfigurationPage::on_m_material_list_create_pb_clicked()
|
||||
{
|
||||
QString path = ui->m_material_list_path_le->text();
|
||||
path = path.isEmpty()
|
||||
? MaterialList::defaultPath()
|
||||
: QFileInfo(path).absolutePath() + QLatin1Char('/') + MaterialList::defaultFileName();
|
||||
|
||||
path = QFileDialog::getSaveFileName(
|
||||
this,
|
||||
tr("Créer le fichier de la liste de matériaux"),
|
||||
path,
|
||||
tr("Fichiers csv (*.csv)"));
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (QFileInfo(path).suffix().isEmpty()) {
|
||||
path += QStringLiteral(".csv");
|
||||
}
|
||||
|
||||
//An existing file is kept as it is : this button creates the
|
||||
//header, it never overwrites a catalogue.
|
||||
if (MaterialList::isEmptyFile(path))
|
||||
{
|
||||
QString error;
|
||||
if (!MaterialList::createFile(path, &error))
|
||||
{
|
||||
QET::QetMessageBox::critical(this,
|
||||
tr("Création impossible"),
|
||||
tr("Impossible de créer le fichier :\n%1\n%2")
|
||||
.arg(path, error));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ui->m_material_list_path_le->setText(path);
|
||||
}
|
||||
|
||||
void GeneralConfigurationPage::on_m_indi_text_font_pb_clicked()
|
||||
{
|
||||
bool ok;
|
||||
|
||||
@@ -47,6 +47,8 @@ class GeneralConfigurationPage : public ConfigPage
|
||||
void on_m_company_tbt_path_cb_currentIndexChanged(int index);
|
||||
void on_m_custom_tbt_path_cb_currentIndexChanged(int index);
|
||||
void on_m_user_macros_path_cb_currentIndexChanged(int index);
|
||||
void on_m_material_list_browse_pb_clicked();
|
||||
void on_m_material_list_create_pb_clicked();
|
||||
void on_m_indi_text_font_pb_clicked();
|
||||
void on_MaxPartsElementEditorList_sb_valueChanged(int value);
|
||||
void on_DiagramEditor_Grid_PointSize_min_sb_valueChanged(int value);
|
||||
|
||||
@@ -459,6 +459,47 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="0">
|
||||
<widget class="QLabel" name="m_material_list_label">
|
||||
<property name="text">
|
||||
<string>Fichier de la liste de matériaux</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="6" column="1">
|
||||
<layout class="QHBoxLayout" name="m_material_list_layout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="m_material_list_path_le">
|
||||
<property name="toolTip">
|
||||
<string extracomment="tooltip of the material file path field">Répertoire de matériaux utilisé pour renseigner les articles d'un élément</string>
|
||||
</property>
|
||||
<property name="readOnly">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string extracomment="hint shown when no material file is configured yet">Non configuré</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="m_material_list_browse_pb">
|
||||
<property name="text">
|
||||
<string>Parcourir...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="m_material_list_create_pb">
|
||||
<property name="toolTip">
|
||||
<string extracomment="tooltip of the button creating the material file with its header line">Créer le fichier de la liste avec ses en-têtes</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Créer...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include <QCompleter>
|
||||
#include <QRegularExpressionValidator>
|
||||
#include <QStringListModel>
|
||||
#include <QToolButton>
|
||||
|
||||
#include <utility>
|
||||
|
||||
@@ -46,6 +47,14 @@ ElementInfoPartWidget::ElementInfoPartWidget(
|
||||
ui->setupUi(this);
|
||||
ui->label_->setText(translated_key);
|
||||
ui->m_erase_text->setVisible(false);
|
||||
ui->m_material_btn->setVisible(false);
|
||||
|
||||
//The line edit must swallow the whole width of the row: without
|
||||
//this the material button column would steal room from it.
|
||||
ui->gridLayout_2->setColumnStretch(0, 1);
|
||||
|
||||
connect(ui->m_material_btn, &QToolButton::clicked,
|
||||
this, &ElementInfoPartWidget::materialButtonClicked);
|
||||
|
||||
if (key_ == QETInformation::ELMT_WIDTH || key_ == QETInformation::ELMT_HEIGHT || key_ == QETInformation::ELMT_DEPTH)
|
||||
{
|
||||
@@ -174,6 +183,16 @@ void ElementInfoPartWidget::setEraseTextVisible(bool visible)
|
||||
m_show_erase = visible;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoPartWidget::setMaterialButtonVisible
|
||||
Show or hide the "..." button opening the material file for this row.
|
||||
@param visible
|
||||
*/
|
||||
void ElementInfoPartWidget::setMaterialButtonVisible(bool visible)
|
||||
{
|
||||
ui->m_material_btn->setVisible(visible);
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoPartWidget::setEraseTextChecked
|
||||
@param check
|
||||
|
||||
@@ -56,10 +56,12 @@ class ElementInfoPartWidget : public QWidget
|
||||
void setEraseTextVisible (bool visible);
|
||||
void setEraseTextChecked (bool check);
|
||||
Qt::CheckState EraseTextCheckState ()const;
|
||||
void setMaterialButtonVisible (bool visible);
|
||||
|
||||
signals:
|
||||
void textEdited (const QString & text);
|
||||
void textChanged (const QString & text);
|
||||
void materialButtonClicked();
|
||||
|
||||
//ATTRIBUTES
|
||||
private slots:
|
||||
|
||||
@@ -65,6 +65,25 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="2">
|
||||
<widget class="QToolButton" name="m_material_btn">
|
||||
<property name="toolTip">
|
||||
<string extracomment="tooltip of the button opening the material file; it fills the fields of this block from a catalogue entry">Choisir un article dans la liste de matériaux</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">…</string>
|
||||
</property>
|
||||
<property name="visible">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="autoRaise">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="toolButtonStyle">
|
||||
<enum>Qt::ToolButtonTextOnly</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
|
||||
@@ -20,8 +20,11 @@
|
||||
#include <QCheckBox>
|
||||
#include <QPushButton>
|
||||
#include "../diagram.h"
|
||||
#include "../materiallist/materiallist.h"
|
||||
#include "../materiallist/materialselectiondialog.h"
|
||||
#include "../qetapp.h"
|
||||
#include "../qetgraphicsitem/element.h"
|
||||
#include "../qetmessagebox.h"
|
||||
#include "../dataBase/projectdatabase.h"
|
||||
#include "../qetinformation.h"
|
||||
#include "../qetproject.h"
|
||||
@@ -222,6 +225,8 @@ void ElementInfoWidget::buildInterface()
|
||||
m_eipw_list << eipw;
|
||||
}
|
||||
|
||||
setupMaterialButtons();
|
||||
|
||||
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);
|
||||
@@ -332,6 +337,150 @@ ElementInfoPartWidget *ElementInfoWidget::infoPartWidgetForKey(const QString &ke
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoWidget::setupMaterialButtons
|
||||
Show the "..." button opening the material file on the description row
|
||||
of each block: one for the main article, one per auxiliary article
|
||||
which really has a description row.
|
||||
|
||||
Only that row carries the button: the same window fills the whole
|
||||
block, so repeating it on every field of the block would just offer
|
||||
five ways to do the same thing.
|
||||
*/
|
||||
void ElementInfoWidget::setupMaterialButtons()
|
||||
{
|
||||
auto register_button = [this](ElementInfoPartWidget *eipw, int block)
|
||||
{
|
||||
eipw->setMaterialButtonVisible(true);
|
||||
connect(eipw, &ElementInfoPartWidget::materialButtonClicked, this,
|
||||
[this, block]() { materialFromFile(block); });
|
||||
};
|
||||
|
||||
//Main article. A terminal has no description row: its article
|
||||
//number is the one which identifies it, so the button sits there.
|
||||
if (ElementInfoPartWidget *eipw = infoPartWidgetForKey(QETInformation::ELMT_DESCRIPTION)) {
|
||||
register_button(eipw, 0);
|
||||
} else if (ElementInfoPartWidget *eipw = infoPartWidgetForKey(QETInformation::ELMT_DESIGNATION)) {
|
||||
register_button(eipw, 0);
|
||||
}
|
||||
|
||||
//Auxiliary articles 1 to 4.
|
||||
const QStringList auxiliary_keys = {
|
||||
QETInformation::ELMT_DESCRIPTION_AUX1,
|
||||
QETInformation::ELMT_DESCRIPTION_AUX2,
|
||||
QETInformation::ELMT_DESCRIPTION_AUX3,
|
||||
QETInformation::ELMT_DESCRIPTION_AUX4
|
||||
};
|
||||
|
||||
for (int block = 1; block <= 4 && block <= auxiliary_keys.size(); ++block)
|
||||
{
|
||||
if (ElementInfoPartWidget *eipw = infoPartWidgetForKey(auxiliary_keys.at(block - 1))) {
|
||||
register_button(eipw, block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoWidget::materialFromFile
|
||||
Open the material file, let the user pick an article, and write it
|
||||
into the block the button belongs to.
|
||||
@param block 0 for the main article, 1 to 4 for an auxiliary article
|
||||
*/
|
||||
void ElementInfoWidget::materialFromFile(int block)
|
||||
{
|
||||
//Nothing configured yet: the standard location is used, the same
|
||||
//one the preferences offer to create, so that the catalogue can be
|
||||
//reached from here without asking for a file to be saved first.
|
||||
QString path = MaterialList::configuredPath();
|
||||
if (path.isEmpty())
|
||||
{
|
||||
path = MaterialList::defaultPath();
|
||||
MaterialList::setConfiguredPath(path);
|
||||
}
|
||||
|
||||
//A missing or empty file has no entry to pick: offer to write the
|
||||
//header line instead of opening an empty window.
|
||||
if (MaterialList::isEmptyFile(path))
|
||||
{
|
||||
const auto answer = QET::QetMessageBox::question(
|
||||
this,
|
||||
tr("Liste de matériaux absente"),
|
||||
tr("Aucun fichier de liste de matériaux n'existe à cet emplacement :\n"
|
||||
"%1\n\nLe créer ?", "message asking to create the material file").arg(path),
|
||||
QMessageBox::Yes | QMessageBox::No,
|
||||
QMessageBox::Yes);
|
||||
|
||||
if (answer != QMessageBox::Yes) {
|
||||
return;
|
||||
}
|
||||
|
||||
QString error;
|
||||
if (!MaterialList::createFile(path, &error))
|
||||
{
|
||||
QET::QetMessageBox::critical(this,
|
||||
tr("Création impossible"),
|
||||
tr("Impossible de créer le fichier :\n%1\n%2")
|
||||
.arg(path, error));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
//The catalogue opens on the whole list : the search field starts
|
||||
//empty, whatever the block already holds.
|
||||
MaterialSelectionDialog dialog(path, this);
|
||||
if (dialog.exec() == QDialog::Accepted) {
|
||||
applyMaterialRecord(dialog.selectedRecord(), block);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoWidget::applyMaterialRecord
|
||||
Write a catalogue entry into the fields of one block.
|
||||
|
||||
Only the fields of that block are touched: applied to an auxiliary
|
||||
article, the entry never reaches the main article. And a cell which is
|
||||
empty in the file leaves the current value alone, so picking an entry
|
||||
describing only the order reference never wipes a comment.
|
||||
@param record the entry taken from the material file
|
||||
@param block 0 for the main article, 1 to 4 for an auxiliary article
|
||||
*/
|
||||
void ElementInfoWidget::applyMaterialRecord(const MaterialRecord &record, int block)
|
||||
{
|
||||
if (record.values.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Fill the fields without letting the live edit push one undo
|
||||
//command per line edit: the whole entry must be undoable at once.
|
||||
const bool live_edit = m_live_edit;
|
||||
if (live_edit) {
|
||||
disableLiveEdit();
|
||||
}
|
||||
|
||||
for (const QString &column : MaterialList::columnsForBlock(block))
|
||||
{
|
||||
const QString value = record.value(column);
|
||||
if (value.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString key = MaterialList::elementInfoKey(column, block);
|
||||
if (key.isEmpty() || !QETInformation::elementInfoKeys().contains(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ElementInfoPartWidget *eipw = infoPartWidgetForKey(key)) {
|
||||
eipw->setText(value);
|
||||
}
|
||||
}
|
||||
|
||||
if (live_edit) {
|
||||
enableLiveEdit();
|
||||
}
|
||||
|
||||
apply();
|
||||
}
|
||||
|
||||
/**
|
||||
@brief ElementInfoWidget::updateSuggestions
|
||||
Offer, for each information, the values already used by the other
|
||||
|
||||
@@ -30,6 +30,7 @@ class CustomElementInfoPartWidget;
|
||||
class ChangeElementInformationCommand;
|
||||
class QCheckBox;
|
||||
class QPushButton;
|
||||
struct MaterialRecord;
|
||||
|
||||
namespace Ui {
|
||||
class ElementInfoWidget;
|
||||
@@ -64,6 +65,9 @@ class ElementInfoWidget : public AbstractElementPropertiesEditorWidget
|
||||
|
||||
private:
|
||||
void buildInterface();
|
||||
void setupMaterialButtons();
|
||||
void materialFromFile(int block);
|
||||
void applyMaterialRecord(const MaterialRecord &record, int block);
|
||||
ElementInfoPartWidget *infoPartWidgetForKey(const QString &key) const;
|
||||
QStringList predefinedKeys() const;
|
||||
void updateSuggestions();
|
||||
|
||||
Reference in New Issue
Block a user