mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-01 15:24:13 +02:00
6ea626fec7
The prefixes of a collection's folders live in a qet_labels.xml that so far could only be edited by hand, and a hand-edited file is easy to break: one extra </category> and the file stops being well formed, which the lookup answers with "no prefix at all". Labels then degrade silently, without any error anywhere - the file is simply ignored. Add a way to edit it from the settings. - "Configurer les préfixes…" next to the user collection path opens PrefixConfigurationDialog, which lists every folder of that collection, subfolders included, one line edit each, with "Tout déplier"/"Tout replier" and OK/Abbrechen. It writes on OK only : cancelling leaves the collection exactly as it was, and an emptied field drops the <prefix> again so the folder goes back to inheriting its parent's. - QetLabelsFile owns the reading, scanning, structure building and writing of the file. prefixFromLabelFile() moves there unchanged from assignvariables.cpp as prefixForPath(), so the lookup used by label assignment and the one used by the dialog are one piece of code. - Entries whose folder no longer exists on disk are offered as Conserver or Supprimer and only applied on OK. - An unparsable file is copied to qet_labels.xml.bak first, and the dialog then reports the line and column of the syntax error, with "Corriger le fichier" as the default choice - such a file may be one forgotten tag away from being valid - and rebuilds the whole structure only when "Reconstruire" is picked. A broken file that cannot be backed up is refused rather than overwritten. The scan reads non-hidden directories recursively in name order, without following symlinks. A collection without any subfolder has nothing to configure and says so instead of opening an empty dialog. Tests: a standalone harness kept outside the tree (84 checks : structure, inheritance, explicit empty prefix, orphan handling, broken file, wrong root element, broken file whose backup cannot be written, reload after save) and the dialog driven offscreen (29 checks, including "reject creates no file"). ctest 27/28, the failing tst_menubarkeyboard being the headless F10 test, unrelated to this. The new strings are French source strings like the rest of the code; the .ts files are left to the translation update.
221 lines
7.5 KiB
C++
221 lines
7.5 KiB
C++
/*
|
|
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 "prefixconfigurationdialog.h"
|
|
|
|
#include "../ElementsCollection/qetlabelsfile.h"
|
|
|
|
#include <QDialogButtonBox>
|
|
#include <QHash>
|
|
#include <QHBoxLayout>
|
|
#include <QHeaderView>
|
|
#include <QKeyEvent>
|
|
#include <QLabel>
|
|
#include <QLineEdit>
|
|
#include <QMessageBox>
|
|
#include <QPushButton>
|
|
#include <QTreeWidget>
|
|
#include <QTreeWidgetItem>
|
|
#include <QTreeWidgetItemIterator>
|
|
|
|
/**
|
|
@brief PrefixConfigurationDialog::PrefixConfigurationDialog
|
|
@param labels : the loaded labels file the dialog edits
|
|
@param folders : every directory of the collection, as returned by
|
|
QetLabelsFile::scanFolders()
|
|
@param parent
|
|
*/
|
|
PrefixConfigurationDialog::PrefixConfigurationDialog(QetLabelsFile &labels, const QList<QStringList> &folders, QWidget *parent) :
|
|
QDialog(parent),
|
|
m_labels(labels),
|
|
m_folders(folders),
|
|
m_tree(new QTreeWidget(this))
|
|
{
|
|
setWindowTitle(tr("Préfixes de la collection utilisateur", "title of the dialog configuring the prefixes of the user collection"));
|
|
setModal(true);
|
|
resize(700, 500);
|
|
|
|
auto *layout = new QVBoxLayout(this);
|
|
|
|
auto *hint = new QLabel(this);
|
|
hint->setWordWrap(true);
|
|
hint->setText(tr("Chaque dossier de la collection possède un préfixe : il est ajouté devant l'étiquette des éléments du dossier.\n"
|
|
"Un champ vide signifie que le dossier reprend le préfixe de son dossier parent."));
|
|
layout->addWidget(hint);
|
|
|
|
m_tree->setColumnCount(2);
|
|
m_tree->setHeaderLabels(QStringList() << tr("Dossier", "column header of the folder tree") << tr("Préfixe", "column header of the prefix column"));
|
|
m_tree->setRootIsDecorated(true);
|
|
m_tree->setEditTriggers(QAbstractItemView::NoEditTriggers);
|
|
m_tree->setSelectionMode(QAbstractItemView::NoSelection);
|
|
m_tree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
|
|
m_tree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
|
|
layout->addWidget(m_tree);
|
|
|
|
auto *expand_pb = new QPushButton(tr("Tout déplier"), this);
|
|
auto *collapse_pb = new QPushButton(tr("Tout replier"), this);
|
|
connect(expand_pb, &QPushButton::clicked, m_tree, &QTreeWidget::expandAll);
|
|
connect(collapse_pb, &QPushButton::clicked, m_tree, &QTreeWidget::collapseAll);
|
|
|
|
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
|
|
connect(buttons, &QDialogButtonBox::accepted, this, &PrefixConfigurationDialog::accept);
|
|
connect(buttons, &QDialogButtonBox::rejected, this, &PrefixConfigurationDialog::reject);
|
|
|
|
auto *bottom_layout = new QHBoxLayout();
|
|
bottom_layout->addWidget(expand_pb);
|
|
bottom_layout->addWidget(collapse_pb);
|
|
bottom_layout->addStretch();
|
|
bottom_layout->addWidget(buttons);
|
|
layout->addLayout(bottom_layout);
|
|
|
|
//Asking before the tree is shown keeps the answer from appearing
|
|
//in the middle of a dialog the user has not seen yet.
|
|
askWhatToDoWithOrphans();
|
|
buildTree();
|
|
}
|
|
|
|
PrefixConfigurationDialog::~PrefixConfigurationDialog()
|
|
{
|
|
}
|
|
|
|
/**
|
|
@brief PrefixConfigurationDialog::askWhatToDoWithOrphans
|
|
When the labels file describes directories the collection does not
|
|
have (deleted or renamed folders, or entries written by hand in an
|
|
other layout), the user chooses whether they are kept or dropped.
|
|
The choice is only applied by accept().
|
|
*/
|
|
void PrefixConfigurationDialog::askWhatToDoWithOrphans()
|
|
{
|
|
const QStringList orphans = m_labels.orphanPaths(m_folders);
|
|
if (orphans.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
QMessageBox box(QMessageBox::Question,
|
|
tr("Entrées sans dossier"),
|
|
tr("%n entrée(s) de qet_labels.xml ne correspond à aucun dossier de la collection :\n"
|
|
"les conserver ou les supprimer ?", nullptr, static_cast<int>(orphans.size())),
|
|
QMessageBox::NoButton,
|
|
parentWidget());
|
|
auto *keep_button = box.addButton(tr("Conserver"), QMessageBox::AcceptRole);
|
|
box.addButton(tr("Supprimer"), QMessageBox::DestructiveRole);
|
|
box.setDetailedText(orphans.join(QLatin1Char('\n')));
|
|
box.exec();
|
|
|
|
//Closing the box without choosing keeps the entries : deleting
|
|
//data must always be an explicit decision.
|
|
m_remove_orphans = box.clickedButton() != nullptr
|
|
&& box.clickedButton() != keep_button;
|
|
}
|
|
|
|
/**
|
|
@brief PrefixConfigurationDialog::buildTree
|
|
One item per directory of the collection, nested the way the
|
|
directories are, each with a line edit holding the prefix already
|
|
stored for that directory.
|
|
*/
|
|
void PrefixConfigurationDialog::buildTree()
|
|
{
|
|
m_tree->setUpdatesEnabled(false);
|
|
|
|
QHash<QString, QTreeWidgetItem *> known;
|
|
for (const QStringList &folder : m_folders) {
|
|
QTreeWidgetItem *parent_item = nullptr;
|
|
QString built;
|
|
for (const QString &name : folder) {
|
|
built = built.isEmpty() ? name : built + QLatin1Char('/') + name;
|
|
|
|
QTreeWidgetItem *item = known.value(built, nullptr);
|
|
if (item == nullptr) {
|
|
item = parent_item == nullptr
|
|
? new QTreeWidgetItem(m_tree)
|
|
: new QTreeWidgetItem(parent_item);
|
|
item->setText(0, name);
|
|
item->setFlags(item->flags() & ~Qt::ItemIsEditable);
|
|
known.insert(built, item);
|
|
}
|
|
parent_item = item;
|
|
}
|
|
if (parent_item == nullptr) {
|
|
continue;
|
|
}
|
|
parent_item->setData(0, Qt::UserRole, folder);
|
|
|
|
auto *edit = new QLineEdit(m_labels.prefix(folder), m_tree);
|
|
edit->setClearButtonEnabled(true);
|
|
edit->setPlaceholderText(tr("hériter du dossier parent", "placeholder of an empty prefix field"));
|
|
edit->installEventFilter(this);
|
|
m_tree->setItemWidget(parent_item, 1, edit);
|
|
}
|
|
|
|
m_tree->expandAll();
|
|
m_tree->setUpdatesEnabled(true);
|
|
}
|
|
|
|
/**
|
|
@brief PrefixConfigurationDialog::accept
|
|
Apply every prefix shown in the dialog, add the missing categories,
|
|
optionally drop the entries the user agreed to delete, then write the
|
|
file. When the write fails the dialog stays open so nothing typed in
|
|
it is lost.
|
|
*/
|
|
void PrefixConfigurationDialog::accept()
|
|
{
|
|
m_labels.ensureStructure(m_folders);
|
|
|
|
for (QTreeWidgetItemIterator iterator(m_tree) ; *iterator ; ++iterator) {
|
|
QTreeWidgetItem *item = *iterator;
|
|
auto *edit = qobject_cast<QLineEdit *>(m_tree->itemWidget(item, 1));
|
|
if (edit == nullptr) {
|
|
continue;
|
|
}
|
|
m_labels.setPrefix(item->data(0, Qt::UserRole).toStringList(), edit->text().trimmed());
|
|
}
|
|
|
|
if (m_remove_orphans) {
|
|
m_labels.removeOrphans(m_folders);
|
|
}
|
|
|
|
if (!m_labels.save()) {
|
|
QMessageBox::critical(this,
|
|
tr("Enregistrement impossible"),
|
|
tr("Le fichier %1 n'a pas pu être enregistré :\n%2")
|
|
.arg(m_labels.filePath(), m_labels.errorString()));
|
|
return;
|
|
}
|
|
|
|
QDialog::accept();
|
|
}
|
|
|
|
/**
|
|
@brief PrefixConfigurationDialog::eventFilter
|
|
Return and Enter are swallowed while a prefix field has the focus :
|
|
otherwise the event would reach the dialog and immediately validate
|
|
it, closing a dialog the user is still filling in.
|
|
*/
|
|
bool PrefixConfigurationDialog::eventFilter(QObject *watched, QEvent *event)
|
|
{
|
|
if (event->type() == QEvent::KeyPress && qobject_cast<QLineEdit *>(watched) != nullptr) {
|
|
const int key = static_cast<QKeyEvent *>(event)->key();
|
|
if (key == Qt::Key_Return || key == Qt::Key_Enter) {
|
|
return true;
|
|
}
|
|
}
|
|
return QDialog::eventFilter(watched, event);
|
|
}
|