diff --git a/cmake/qet_compilation_vars.cmake b/cmake/qet_compilation_vars.cmake
index 9297115f7..41380463c 100644
--- a/cmake/qet_compilation_vars.cmake
+++ b/cmake/qet_compilation_vars.cmake
@@ -475,6 +475,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ElementsCollection/elementstreeview.h
${QET_DIR}/sources/ElementsCollection/fileelementcollectionitem.cpp
${QET_DIR}/sources/ElementsCollection/fileelementcollectionitem.h
+ ${QET_DIR}/sources/ElementsCollection/qetlabelsfile.cpp
+ ${QET_DIR}/sources/ElementsCollection/qetlabelsfile.h
${QET_DIR}/sources/ElementsCollection/terminaluuids.cpp
${QET_DIR}/sources/ElementsCollection/terminaluuids.h
${QET_DIR}/sources/ElementsCollection/xmlelementcollection.cpp
@@ -800,6 +802,8 @@ set(QET_SRC_FILES
${QET_DIR}/sources/ui/multipastedialog.h
${QET_DIR}/sources/ui/potentialselectordialog.cpp
${QET_DIR}/sources/ui/potentialselectordialog.h
+ ${QET_DIR}/sources/ui/prefixconfigurationdialog.cpp
+ ${QET_DIR}/sources/ui/prefixconfigurationdialog.h
${QET_DIR}/sources/ui/projectpropertiesdialog.cpp
${QET_DIR}/sources/ui/projectpropertiesdialog.h
${QET_DIR}/sources/ui/reportpropertiewidget.cpp
diff --git a/sources/ElementsCollection/qetlabelsfile.cpp b/sources/ElementsCollection/qetlabelsfile.cpp
new file mode 100644
index 000000000..6c2fdd357
--- /dev/null
+++ b/sources/ElementsCollection/qetlabelsfile.cpp
@@ -0,0 +1,663 @@
+/*
+ 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 .
+*/
+#include "qetlabelsfile.h"
+
+#include
+#include
+#include
+#include
+
+/**
+ @brief QetLabelsFile::labelsFilePath
+ @param collection_dir : an elements collection directory
+ @return the path of the qet_labels.xml file of that collection
+*/
+QString QetLabelsFile::labelsFilePath(const QString &collection_dir)
+{
+ return QDir(collection_dir).filePath(QStringLiteral("qet_labels.xml"));
+}
+
+/**
+ @brief QetLabelsFile::scanFolders
+ @param collection_dir : an elements collection directory
+ @return every directory of the collection, each entry being the list of
+ the directory names from the collection root down to the directory
+ itself (["level_one", "level_two", ...]), in tree order : a parent
+ always comes before its children, siblings are sorted by name.
+ Only plain, visible directories are reported : symbolic links are
+ skipped (a link pointing back up the tree would never end) as are
+ hidden directories such as a stray .git.
+*/
+QList QetLabelsFile::scanFolders(const QString &collection_dir)
+{
+ QList out;
+ if (collection_dir.isEmpty()) {
+ return out;
+ }
+ scanRec(collection_dir, QStringList(), out);
+ return out;
+}
+
+/**
+ @brief QetLabelsFile::scanRec
+ Recursive helper of scanFolders()
+*/
+void QetLabelsFile::scanRec(const QString &path, const QStringList &relative_path, QList &out)
+{
+ const QFileInfoList sub_dirs = QDir(path).entryInfoList(
+ QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks,
+ QDir::Name);
+ for (const QFileInfo &info : sub_dirs) {
+ QStringList current = relative_path;
+ current << info.fileName();
+ out.append(current);
+ scanRec(info.absoluteFilePath(), current, out);
+ }
+}
+
+/**
+ @brief QetLabelsFile::prefixForPath
+ Look up a prefix for @a path (path[dirLevel] outermost, path[1] the
+ deepest directory; path[0], the element's own file name, is never
+ matched) in the qet_labels.xml at @a filepath.
+
+ @return the prefix that applies, or a null QString if the file
+ cannot be read, is not well-formed, or does not describe this
+ path at all (as opposed to describing it with no prefix
+ anywhere along it, which is a non-null empty string).
+
+ @see prefixInDocument() for the description of the lookup itself
+*/
+QString QetLabelsFile::prefixForPath(const QString &filepath, const QStringList &path, int dirLevel)
+{
+ QFile file(filepath);
+ if (!file.open(QFile::ReadOnly | QFile::Text)) {
+ return QString();
+ }
+
+ QDomDocument document;
+ if (!document.setContent(&file)) {
+ return QString();
+ }
+
+ return prefixInDocument(document, path, dirLevel);
+}
+
+/**
+ @brief QetLabelsFile::prefixInDocument
+ Look up a prefix for @a path (path[dirLevel] outermost, path[1] the
+ deepest directory; path[0], the element's own file name, is never
+ matched) in @a document.
+
+ Descends through nested \ elements matching
+ path[dirLevel], path[dirLevel-1], ..., path[1] in turn, considering
+ only *direct* children at each step -- unlike a flat token scan,
+ this cannot be fooled by a same-named category living elsewhere in
+ the document at the wrong nesting depth (bugtracker #671 item 5).
+
+ At each matched level, that category's own \ child -- even
+ an empty one -- overrides whatever a shallower ancestor already
+ provided, so an explicit empty \ cancels inheritance
+ rather than silently falling back to it (the behaviour requested in
+ PR #686 review). A category with no \ child at all leaves
+ the inherited value untouched, which is how a directory with no
+ prefix of its own comes to inherit its parent's, as the file's own
+ header comment documents.
+
+ @return the prefix that applies, or a null QString if the document
+ does not describe this path at all (as opposed to describing it
+ with no prefix anywhere along it, which is a non-null empty
+ string).
+*/
+QString QetLabelsFile::prefixInDocument(const QDomDocument &document, const QStringList &path, int dirLevel)
+{
+ QDomElement node = document.documentElement();
+ if (node.isNull()) {
+ return QString();
+ }
+
+ QString prefix;
+ for (int i = dirLevel ; i >= 1 ; --i) {
+ QDomElement child = node.firstChildElement(QStringLiteral("category"));
+ while (!child.isNull()
+ && child.attribute(QStringLiteral("name")) != path[i]) {
+ child = child.nextSiblingElement(QStringLiteral("category"));
+ }
+ if (child.isNull()) {
+ return QString();
+ }
+ node = child;
+
+ const QDomElement own = node.firstChildElement(QStringLiteral("prefix"));
+ if (!own.isNull()) {
+ //readElementText()'s null-vs-empty distinction that PR
+ //#686 needed for the old QXmlStreamReader-based lookup
+ //has a QDomElement equivalent: text() on an empty
+ //element can itself come back null depending on how the
+ //XML was written, so the same explicit fallback applies
+ //-- an empty QString here means "found, deliberately
+ //blank", not "not found".
+ prefix = own.text();
+ if (prefix.isNull()) {
+ prefix = QString("");
+ }
+ }
+ }
+ return prefix;
+}
+
+/**
+ @brief QetLabelsFile::parse
+ Read @a device into @a document and, when it cannot be read as XML,
+ tell what is wrong with it in @a reason.
+ The Qt version split is the same one used by edzpart.cpp and
+ cli_export.cpp : QDomDocument::ParseResult only exists since Qt 6.5.
+ @return true when the document is well formed.
+*/
+bool QetLabelsFile::parse(QIODevice &device, QDomDocument &document, QString *reason)
+{
+#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
+ const auto result = document.setContent(&device);
+ if (result) {
+ return true;
+ }
+ if (reason != nullptr) {
+ *reason = tr("erreur de syntaxe à la ligne %1, colonne %2 :\n%3")
+ .arg(result.errorLine)
+ .arg(result.errorColumn)
+ .arg(result.errorMessage);
+ }
+ return false;
+#else
+ QString message;
+ int line = 0;
+ int column = 0;
+ if (document.setContent(&device, &message, &line, &column)) {
+ return true;
+ }
+ if (reason != nullptr) {
+ *reason = tr("erreur de syntaxe à la ligne %1, colonne %2 :\n%3")
+ .arg(line)
+ .arg(column)
+ .arg(message);
+ }
+ return false;
+#endif
+}
+
+/**
+ @brief QetLabelsFile::load
+ Read the qet_labels.xml of the collection @a collection_dir.
+ Nothing is written :
+ - when the file does not exist yet, an empty \ document is
+ kept in memory and will only be created on the first save()
+ - when the file exists but cannot be read, is not well formed or does
+ not have \ as its root element, nothing is written : an
+ empty document is used instead, and a copy of the unusable file is
+ only made by save(), right before it is replaced. isBroken() and
+ brokenReason() tell what is wrong with it, so the caller can offer
+ repairing the file rather than silently discarding its content.
+ @return false only when no collection directory was given. It is
+ save() that reports a file it could not copy aside.
+*/
+bool QetLabelsFile::load(const QString &collection_dir)
+{
+ m_file_path.clear();
+ m_backup_path.clear();
+ m_error.clear();
+ m_broken_reason.clear();
+ m_snapshot.clear();
+ m_file_exists = false;
+ m_force_save = false;
+ m_broken = false;
+ m_document = QDomDocument();
+
+ if (collection_dir.isEmpty()) {
+ m_error = tr("Aucun répertoire de collection n'a été donné.");
+ return false;
+ }
+ m_file_path = labelsFilePath(collection_dir);
+
+ QFile file(m_file_path);
+ if (!file.exists()) {
+ createEmptyDocument();
+ m_snapshot = serialize(m_document);
+ return true;
+ }
+ m_file_exists = true;
+
+ QDomDocument document;
+ bool well_formed = false;
+ if (file.open(QFile::ReadOnly | QFile::Text)) {
+ well_formed = parse(file, document, &m_broken_reason);
+ if (well_formed
+ && document.documentElement().tagName() != QLatin1String("labels")) {
+ well_formed = false;
+ m_broken_reason = tr("l'élément racine <%1> n'est pas .")
+ .arg(document.documentElement().tagName());
+ }
+ file.close();
+ } else {
+ m_broken_reason = tr("le fichier n'a pas pu être ouvert : %1")
+ .arg(file.errorString());
+ }
+
+ if (well_formed) {
+ m_document = document;
+ } else {
+ //The file is unreadable or malformed : leave it alone and work
+ //on an empty document. m_force_save makes sure the file is
+ //replaced as soon as the caller validates, even though the
+ //empty document we start from serializes just fine - and it is
+ //also what tells save() to copy the file aside first.
+ m_broken = true;
+ createEmptyDocument();
+ m_force_save = true;
+ }
+ m_snapshot = serialize(m_document);
+ return true;
+}
+
+/**
+ @brief QetLabelsFile::createEmptyDocument
+ Reset the in memory document to an empty \ document, preceded
+ by the comment explaining how the prefixes are meant to be written.
+*/
+void QetLabelsFile::createEmptyDocument()
+{
+ m_document = QDomDocument();
+ m_document.appendChild(m_document.createProcessingInstruction(
+ QStringLiteral("xml"),
+ QStringLiteral("version=\"1.0\" encoding=\"utf-8\"")));
+
+ const QString comment = tr(
+ "Fichier de préfixes (étiquettes) de la collection utilisateur.\n"
+ "Un préfixe est attribué à chaque dossier : les éléments d'un dossier\n"
+ "reprennent le préfixe de ce dossier, sauf s'ils portent eux-mêmes une\n"
+ "étiquette. Un dossier sans préfixe reprend celui de son dossier parent.\n"
+ "Ce fichier est créé et modifié par QElectroTech (Programme de réglages :\n"
+ "Configurer les préfixes…), mais reste modifiable à la main.");
+ m_document.appendChild(m_document.createComment(comment));
+
+ m_document.appendChild(m_document.createElement(QStringLiteral("labels")));
+}
+
+/**
+ @brief QetLabelsFile::backupBrokenFile
+ Keep a copy of the unreadable file before it gets overwritten by the
+ next save().
+ @return the path of the created backup, or an empty QString if the
+ copy failed.
+*/
+QString QetLabelsFile::backupBrokenFile()
+{
+ QString candidate = m_file_path + QStringLiteral(".bak");
+ int counter = 1;
+ while (QFile::exists(candidate) && counter < 1000) {
+ candidate = m_file_path + QStringLiteral(".bak.") + QString::number(counter++);
+ }
+ if (!QFile::copy(m_file_path, candidate)) {
+ return QString();
+ }
+ return candidate;
+}
+
+/**
+ @brief QetLabelsFile::prefix
+ @param relative_path : a directory of the collection, from the
+ collection root down to the directory itself
+ @return the prefix stored for that very directory, an empty QString
+ when it has no prefix of its own -- which means it inherits the
+ prefix of its parent directory, and is shown with an empty field
+ by the editor. The inherited value is never reported here: a
+ field the user leaves empty must keep inheriting.
+*/
+QString QetLabelsFile::prefix(const QStringList &relative_path) const
+{
+ QDomElement node = m_document.documentElement();
+ for (const QString &name : relative_path) {
+ node = directChildCategory(node, name);
+ if (node.isNull()) {
+ return QString();
+ }
+ }
+
+ const QDomElement own = node.firstChildElement(QStringLiteral("prefix"));
+ if (own.isNull()) {
+ return QString();
+ }
+ const QString value = own.text();
+ return value.isNull() ? QString() : value;
+}
+
+/**
+ @brief QetLabelsFile::hasPrefix
+ @return true when @a relative_path owns a \ child of its own,
+ even an empty one. prefix() alone cannot tell that apart from a
+ category without any \ : both give it no value, while an
+ empty \ cancels the inheritance where a category
+ without one inherits.
+*/
+bool QetLabelsFile::hasPrefix(const QStringList &relative_path) const
+{
+ QDomElement node = m_document.documentElement();
+ for (const QString &name : relative_path) {
+ node = directChildCategory(node, name);
+ if (node.isNull()) {
+ return false;
+ }
+ }
+
+ return !node.firstChildElement(QStringLiteral("prefix")).isNull();
+}
+
+/**
+ @brief QetLabelsFile::directChildCategory
+ @return the \ child of @a parent named @a name, or a null
+ element when there is none. Only direct children are considered,
+ the same way the prefix lookup descends the document.
+*/
+QDomElement QetLabelsFile::directChildCategory(const QDomElement &parent, const QString &name)
+{
+ QDomElement child = parent.firstChildElement(QStringLiteral("category"));
+ while (!child.isNull()
+ && child.attribute(QStringLiteral("name")) != name) {
+ child = child.nextSiblingElement(QStringLiteral("category"));
+ }
+ return child;
+}
+
+/**
+ @brief QetLabelsFile::categoryForPath
+ @param relative_path : a directory of the collection, from the
+ collection root down to the directory itself
+ @param create : when true, the missing \ elements are
+ created on the way down
+ @return the \ element describing @a relative_path, or a
+ null element when it does not exist and @a create is false.
+*/
+QDomElement QetLabelsFile::categoryForPath(const QStringList &relative_path, bool create)
+{
+ QDomElement node = m_document.documentElement();
+ for (const QString &name : relative_path) {
+ if (node.isNull()) {
+ break;
+ }
+ QDomElement child = directChildCategory(node, name);
+ if (child.isNull()) {
+ if (!create) {
+ return QDomElement();
+ }
+ child = m_document.createElement(QStringLiteral("category"));
+ child.setAttribute(QStringLiteral("name"), name);
+ //A prefix always comes after the child categories, as
+ //documented in the header of the shipped labels files :
+ //insert the new category before the prefix, then push the
+ //prefix back to the end when the file did not follow that
+ //convention in the first place. A node nothing is inserted
+ //into is never repositioned, so a hand formatted file stays
+ //byte for byte the same as long as it is not modified.
+ QDomElement prefix = node.firstChildElement(QStringLiteral("prefix"));
+ if (prefix.isNull()) {
+ node.appendChild(child);
+ } else {
+ node.insertBefore(child, prefix);
+ if (!prefix.nextSibling().isNull()) {
+ node.appendChild(prefix);
+ }
+ }
+ }
+ node = child;
+ }
+ return node;
+}
+
+/**
+ @brief QetLabelsFile::ensureStructure
+ Create, in memory, one \ element for every directory of the
+ collection, nesting them the way the directories are nested.
+ Existing elements -- categories, prefixes and comments alike -- are
+ left where and how they are: only missing categories are added, and
+ those get no \ child, which is exactly "inherit the parent's
+ prefix".
+ @param folders : as returned by scanFolders()
+*/
+void QetLabelsFile::ensureStructure(const QList &folders)
+{
+ for (const QStringList &folder : folders) {
+ categoryForPath(folder, true);
+ }
+}
+
+/**
+ @brief QetLabelsFile::setPrefix
+ Store @a prefix as the prefix of the directory @a relative_path,
+ creating the missing categories on the way.
+ An empty @a prefix means "that directory has no prefix of its own" :
+ any \ element it had is removed so the lookup falls back to
+ the prefix of the parent directory.
+*/
+void QetLabelsFile::setPrefix(const QStringList &relative_path, const QString &prefix)
+{
+ if (relative_path.isEmpty()) {
+ return;
+ }
+ QDomElement node = categoryForPath(relative_path, true);
+ if (node.isNull()) {
+ return;
+ }
+
+ QDomElement element = node.firstChildElement(QStringLiteral("prefix"));
+ if (prefix.isEmpty()) {
+ if (!element.isNull()) {
+ node.removeChild(element);
+ }
+ return;
+ }
+
+ if (element.isNull()) {
+ element = m_document.createElement(QStringLiteral("prefix"));
+ node.appendChild(element);
+ }
+ while (!element.firstChild().isNull()) {
+ element.removeChild(element.firstChild());
+ }
+ element.appendChild(m_document.createTextNode(prefix));
+ //keep the convention : the prefix stays after the child categories
+ if (!element.nextSibling().isNull()) {
+ node.appendChild(element);
+ }
+}
+
+/**
+ @brief QetLabelsFile::collectOrphans
+ Recursive helper shared by orphanPaths() and removeOrphans().
+ Appends to @a orphans the top most \ elements whose path is
+ not in @a folders_key -- their children go away with them.
+*/
+void QetLabelsFile::collectOrphans(const QDomElement &parent, const QString &relative_path, const QSet &folders_key, QList &orphans)
+{
+ for (QDomElement child = parent.firstChildElement(QStringLiteral("category"));
+ !child.isNull();
+ child = child.nextSiblingElement(QStringLiteral("category"))) {
+ const QString path = relative_path.isEmpty()
+ ? child.attribute(QStringLiteral("name"))
+ : relative_path + QLatin1Char('/') + child.attribute(QStringLiteral("name"));
+ if (folders_key.contains(path)) {
+ collectOrphans(child, path, folders_key, orphans);
+ } else {
+ orphans.append(child);
+ }
+ }
+}
+
+/**
+ @brief QetLabelsFile::orphanPaths
+ @param folders : as returned by scanFolders()
+ @return the paths of the \ elements of the document that do
+ not describe any existing directory of the collection, e.g. a
+ directory the user deleted or renamed, or entries written by hand
+ in another layout. The editor asks the user what to do with them.
+*/
+QStringList QetLabelsFile::orphanPaths(const QList &folders) const
+{
+ QSet folders_key;
+ for (const QStringList &folder : folders) {
+ folders_key.insert(folder.join(QLatin1Char('/')));
+ }
+
+ QList orphans;
+ const QDomElement root = m_document.documentElement();
+ if (!root.isNull()) {
+ collectOrphans(root, QString(), folders_key, orphans);
+ }
+
+ QStringList out;
+ for (const QDomElement &element : orphans) {
+ QStringList names;
+ QDomElement node = element;
+ while (!node.isNull() && node.tagName() != QLatin1String("labels")) {
+ names.prepend(node.attribute(QStringLiteral("name")));
+ node = node.parentNode().toElement();
+ }
+ out.append(names.join(QLatin1Char('/')));
+ }
+ return out;
+}
+
+/**
+ @brief QetLabelsFile::removeOrphans
+ Remove from the document every category orphanPaths() reports.
+ @param folders : as returned by scanFolders()
+ @return the number of removed categories (children included)
+*/
+int QetLabelsFile::removeOrphans(const QList &folders)
+{
+ QStringList orphans = orphanPaths(folders);
+ if (orphans.isEmpty()) {
+ return 0;
+ }
+
+ int removed = 0;
+ for (const QString &path : orphans) {
+ QDomElement node = m_document.documentElement();
+ const QStringList names = path.split(QLatin1Char('/'));
+ for (const QString &name : names) {
+ node = directChildCategory(node, name);
+ if (node.isNull()) {
+ break;
+ }
+ }
+ if (node.isNull()) {
+ continue;
+ }
+ node.parentNode().removeChild(node);
+ ++removed;
+ }
+ return removed;
+}
+
+/**
+ @brief QetLabelsFile::serialize
+ @return @a document as the text written to the disk : a canonical xml
+ declaration followed by a two space per level indentation, the
+ same as the shipped labels files.
+*/
+QString QetLabelsFile::serialize(const QDomDocument &document)
+{
+ QString content = document.toString(2);
+
+ //QDomDocument may or may not emit the xml declaration itself,
+ //depending on how the document was built, so drop it to always
+ //write the very same header. ".
+ const int declaration_end = content.indexOf(QLatin1String("?>"));
+ if (content.startsWith(QLatin1String(" 0) {
+ content.remove(0, declaration_end + 2);
+ }
+ while (content.startsWith(QLatin1Char('\n'))) {
+ content.remove(0, 1);
+ }
+
+ return QStringLiteral("\n") + content;
+}
+
+/**
+ @brief QetLabelsFile::save
+ Write the document back to its qet_labels.xml.
+ The file is only rewritten when the document actually differs from
+ what was loaded, so a file the user formatted by hand is left alone
+ when nothing was changed. When the file does not exist yet it is of
+ course always created.
+ @return true when there was nothing to write or the file was
+ successfully written; otherwise false and errorString() tells why.
+*/
+bool QetLabelsFile::save()
+{
+ m_error.clear();
+
+ if (m_file_path.isEmpty()) {
+ m_error = tr("Aucun fichier de préfixes à enregistrer.");
+ return false;
+ }
+
+ const QString content = serialize(m_document);
+ if (m_file_exists && !m_force_save && content == m_snapshot) {
+ return true;
+ }
+
+ const QDir directory(QFileInfo(m_file_path).absolutePath());
+ if (!directory.exists() && !QDir().mkpath(directory.absolutePath())) {
+ m_error = tr("Le répertoire %1 n'a pas pu être créé.").arg(directory.absolutePath());
+ return false;
+ }
+
+ if (m_force_save && m_file_exists) {
+ //The file on disk could not be read : copy it aside before it
+ //is replaced. Doing it here, and not when it is loaded, means
+ //no copy is left behind when the user chooses to repair the
+ //file instead of rebuilding it - or simply changes their mind
+ //and cancels.
+ m_backup_path = backupBrokenFile();
+ if (m_backup_path.isEmpty()) {
+ m_error = tr("Le fichier %1 n'a pas pu être copié à côté avant d'être remplacé :\nrien n'a été modifié.")
+ .arg(m_file_path);
+ return false;
+ }
+ }
+
+ QSaveFile file(m_file_path);
+ if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
+ m_error = file.errorString();
+ return false;
+ }
+ if (file.write(content.toUtf8()) < 0) {
+ m_error = file.errorString();
+ file.cancelWriting();
+ return false;
+ }
+ if (!file.commit()) {
+ m_error = file.errorString();
+ return false;
+ }
+
+ m_file_exists = true;
+ m_force_save = false;
+ m_snapshot = content;
+ return true;
+}
diff --git a/sources/ElementsCollection/qetlabelsfile.h b/sources/ElementsCollection/qetlabelsfile.h
new file mode 100644
index 000000000..2030c2db5
--- /dev/null
+++ b/sources/ElementsCollection/qetlabelsfile.h
@@ -0,0 +1,107 @@
+/*
+ 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 .
+*/
+#ifndef QETLABELSFILE_H
+#define QETLABELSFILE_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+class QIODevice;
+
+/**
+ @brief The QetLabelsFile class
+
+ Represents the qet_labels.xml file of an elements collection : the file
+ that maps every directory of the collection to the prefix (the ISO/IEC
+ 81346 label) given to the elements it contains.
+
+ Two kind of users use this class :
+ - the prefix lookup performed when an element is placed in a project
+ (prefixForPath(), see also autonum::elementPrefixForLocation())
+ - the "Configurer les préfixes…" editor, which loads a file (creating a
+ skeleton in memory when there is none yet), shows the folder tree of
+ the collection next to the stored prefixes, and writes everything back
+ in one go when the user validates.
+
+ Nothing is written to the disk before save() is called, so cancelling the
+ editor really leaves the collection untouched.
+*/
+class QetLabelsFile
+{
+ Q_DECLARE_TR_FUNCTIONS(QetLabelsFile)
+
+ public:
+ QetLabelsFile() = default;
+
+ // methods
+ public:
+ static QString labelsFilePath(const QString &collection_dir);
+ static QList scanFolders(const QString &collection_dir);
+ static QString prefixForPath(const QString &filepath, const QStringList &path, int dirLevel);
+ static QString prefixInDocument(const QDomDocument &document, const QStringList &path, int dirLevel);
+
+ bool load(const QString &collection_dir);
+ QString prefix(const QStringList &relative_path) const;
+ bool hasPrefix(const QStringList &relative_path) const;
+ QStringList orphanPaths(const QList &folders) const;
+ void ensureStructure(const QList &folders);
+ void setPrefix(const QStringList &relative_path, const QString &prefix);
+ int removeOrphans(const QList &folders);
+ bool save();
+
+ QString filePath() const {return m_file_path;}
+ QString backupPath() const {return m_backup_path;}
+ QString errorString() const {return m_error;}
+ ///true when the existing file was found unusable and an
+ ///empty document is used instead. Its copy is only made by
+ ///save(), right before the file is replaced, so repairing
+ ///the file instead of rebuilding it leaves no copy behind
+ bool isBroken() const {return m_broken;}
+ ///what exactly is wrong with that file (line and column of
+ ///the syntax error for instance), so the caller can tell the
+ ///user how to repair it instead of silently rebuilding it
+ QString brokenReason() const {return m_broken_reason;}
+
+ private:
+ static bool parse(QIODevice &device, QDomDocument &document, QString *reason);
+ QDomElement categoryForPath(const QStringList &relative_path, bool create);
+ void createEmptyDocument();
+ QString backupBrokenFile();
+ static void scanRec(const QString &path, const QStringList &relative_path, QList &out);
+ static QDomElement directChildCategory(const QDomElement &parent, const QString &name);
+ static void collectOrphans(const QDomElement &parent, const QString &relative_path, const QSet &folders_key, QList &orphans);
+ static QString serialize(const QDomDocument &document);
+
+ // attributes
+ private:
+ QString m_file_path;
+ QString m_backup_path;
+ QString m_error;
+ QString m_broken_reason;
+ QString m_snapshot;
+ bool m_file_exists = false;
+ bool m_force_save = false;
+ bool m_broken = false;
+ QDomDocument m_document;
+};
+
+#endif // QETLABELSFILE_H
diff --git a/sources/autoNum/assignvariables.cpp b/sources/autoNum/assignvariables.cpp
index 1cd48953e..3aa46b54c 100644
--- a/sources/autoNum/assignvariables.cpp
+++ b/sources/autoNum/assignvariables.cpp
@@ -24,6 +24,7 @@
#include "../qetgraphicsitem/element.h"
#include "../qetxml.h"
#include "../qetproject.h"
+#include "../ElementsCollection/qetlabelsfile.h"
#include
#include
#include
@@ -728,74 +729,6 @@ namespace autonum
return formula;
}
- /**
- @brief prefixFromLabelFile
- Look up a prefix for @a path (path[dirLevel] outermost, path[1] the
- deepest directory; path[0], the element's own file name, is never
- matched) in the qet_labels.xml at @a filepath.
-
- Descends through nested \ elements matching
- path[dirLevel], path[dirLevel-1], ..., path[1] in turn, considering
- only *direct* children at each step -- unlike a flat token scan,
- this cannot be fooled by a same-named category living elsewhere in
- the document at the wrong nesting depth (bugtracker #671 item 5).
-
- At each matched level, that category's own \ child -- even
- an empty one -- overrides whatever a shallower ancestor already
- provided, so an explicit empty \ cancels inheritance
- rather than silently falling back to it (the behaviour requested in
- PR #686 review). A category with no \ child at all leaves
- the inherited value untouched, which is how a directory with no
- prefix of its own comes to inherit its parent's, as the file's own
- header comment documents.
-
- @return the prefix that applies, or a null QString if the file
- cannot be read, is not well-formed, or does not describe this
- path at all (as opposed to describing it with no prefix
- anywhere along it, which is a non-null empty string).
- */
- static QString prefixFromLabelFile(const QString &filepath, const QStringList &path, int dirLevel)
- {
- QFile file(filepath);
- if (!file.open(QFile::ReadOnly | QFile::Text))
- return QString();
-
- QDomDocument document;
- if (!document.setContent(&file))
- return QString();
-
- QDomElement node = document.documentElement();
- if (node.isNull())
- return QString();
-
- QString prefix;
- for (int i = dirLevel ; i >= 1 ; --i) {
- QDomElement child = node.firstChildElement(QStringLiteral("category"));
- while (!child.isNull()
- && child.attribute(QStringLiteral("name")) != path[i]) {
- child = child.nextSiblingElement(QStringLiteral("category"));
- }
- if (child.isNull())
- return QString();
- node = child;
-
- const QDomElement own = node.firstChildElement(QStringLiteral("prefix"));
- if (!own.isNull()) {
- //readElementText()'s null-vs-empty distinction that PR
- //#686 needed for the old QXmlStreamReader-based lookup
- //has a QDomElement equivalent: text() on an empty
- //element can itself come back null depending on how the
- //XML was written, so the same explicit fallback applies
- //-- an empty QString here means "found, deliberately
- //blank", not "not found".
- prefix = own.text();
- if (prefix.isNull())
- prefix = QString("");
- }
- }
- return prefix;
- }
-
/**
@brief elementPrefixForLocation
@param location
@@ -854,7 +787,7 @@ namespace autonum
{
const QString common_file = QDir(QETApp::commonElementsDir())
.filePath(collection_root + QStringLiteral("/qet_labels.xml"));
- const QString prefix = prefixFromLabelFile(common_file, path, dirLevel);
+ const QString prefix = QetLabelsFile::prefixForPath(common_file, path, dirLevel);
if (!prefix.isNull()) {
return prefix;
}
@@ -882,7 +815,7 @@ namespace autonum
const QString candidate =
QDir(dir).filePath(QStringLiteral("qet_labels.xml"));
for (const QStringList &segments : {path_from_root, path}) {
- const QString prefix = prefixFromLabelFile(
+ const QString prefix = QetLabelsFile::prefixForPath(
candidate, segments, segments.size() - 1);
if (!prefix.isNull()) {
return prefix;
diff --git a/sources/ui/configpage/generalconfigurationpage.cpp b/sources/ui/configpage/generalconfigurationpage.cpp
index af63fba0e..206735c64 100644
--- a/sources/ui/configpage/generalconfigurationpage.cpp
+++ b/sources/ui/configpage/generalconfigurationpage.cpp
@@ -26,10 +26,14 @@
#include "../../qetmessagebox.h"
#include "../../textgrid.h"
#include "../../editor/terminalnamecheck.h"
+#include "../../ElementsCollection/qetlabelsfile.h"
+#include "../prefixconfigurationdialog.h"
#include "../nokde/kcolorbutton.h"
+#include
#include
#include
#include
+#include
#include
/**
@@ -639,6 +643,90 @@ void GeneralConfigurationPage::on_m_user_macros_path_cb_currentIndexChanged(int
}
}
+/**
+ @brief GeneralConfigurationPage::on_m_prefix_pb_clicked
+ Open the dialog where the prefixes of the user collection folders are
+ configured, creating the qet_labels.xml of that collection when it
+ does not exist yet.
+ Nothing is written until that dialog is validated : cancelling it
+ leaves the collection exactly as it was.
+*/
+void GeneralConfigurationPage::on_m_prefix_pb_clicked()
+{
+ //The directory the page displays, even when the change has not
+ //been applied yet : QETApp::customElementsDir() still answers with
+ //the previously saved path, which is not what is shown when the
+ //combo has been put back on "Par defaut".
+ QString directory;
+ switch (ui->m_custom_elmt_path_cb->currentIndex()) {
+ case 1: //"Parcourir..." : the item itself holds the chosen path
+ directory = ui->m_custom_elmt_path_cb->itemData(1, Qt::DisplayRole).toString();
+ break;
+ case 0: //"Par defaut" : where a default custom collection lives
+ directory = QETApp::dataDir() + QStringLiteral("/elements/");
+ break;
+ default:
+ break;
+ }
+ if (directory.isEmpty()) {
+ directory = QETApp::customElementsDir();
+ }
+ directory = QDir::cleanPath(directory);
+
+ if (!QDir(directory).exists() && !QDir().mkpath(directory)) {
+ QMessageBox::warning(this,
+ tr("Répertoire introuvable"),
+ tr("Le répertoire de la collection utilisateur :\n%1\nn'existe pas et n'a pas pu être créé.")
+ .arg(directory));
+ return;
+ }
+
+ const QList folders = QetLabelsFile::scanFolders(directory);
+ if (folders.isEmpty()) {
+ QMessageBox::information(this,
+ tr("Aucun sous-dossier"),
+ tr("La collection utilisateur :\n%1\nne contient aucun sous-dossier : il n'y a donc aucun préfixe à configurer.")
+ .arg(directory));
+ return;
+ }
+
+ QetLabelsFile labels;
+ if (!labels.load(directory)) {
+ QMessageBox::warning(this,
+ tr("Fichier de préfixes illisible"),
+ labels.errorString());
+ return;
+ }
+ if (labels.isBroken()) {
+ //A broken file may only be one forgotten tag away from being
+ //perfectly valid : tell what is wrong and let the user decide,
+ //rebuilding would drop every prefix the file still holds.
+ QMessageBox box(QMessageBox::Warning,
+ tr("Fichier de préfixes endommagé"),
+ tr("Le fichier %1 n'est pas un fichier XML valide :\n%2")
+ .arg(labels.filePath(), labels.brokenReason()),
+ QMessageBox::NoButton,
+ this);
+ box.addButton(tr("Corriger le fichier"), QMessageBox::AcceptRole);
+ auto *rebuild_button = box.addButton(tr("Reconstruire"), QMessageBox::DestructiveRole);
+ box.setInformativeText(tr("Rien n'a encore été modifié.\n\n"
+ "« Corriger le fichier » : cette fenêtre se ferme sans rien changer. "
+ "Ouvrez le fichier dans un éditeur de texte à l'endroit indiqué, "
+ "corrigez-le puis relancez cette commande.\n\n"
+ "« Reconstruire » : l'arborescence des dossiers est recréée, "
+ "mais tous les préfixes actuels sont perdus. Le fichier actuel "
+ "est conservé sous le nom qet_labels.xml.bak avant d'être remplacé."));
+ box.setDetailedText(tr("Fichier : %1").arg(labels.filePath()));
+ box.exec();
+ if (box.clickedButton() != rebuild_button) {
+ return;
+ }
+ }
+
+ PrefixConfigurationDialog dialog(labels, folders, this);
+ dialog.exec();
+}
+
/**
@brief GeneralConfigurationPage::on_m_material_list_browse_pb_clicked
Let the user pick an existing material file.
diff --git a/sources/ui/configpage/generalconfigurationpage.h b/sources/ui/configpage/generalconfigurationpage.h
index 9a3702805..4954a6d3c 100644
--- a/sources/ui/configpage/generalconfigurationpage.h
+++ b/sources/ui/configpage/generalconfigurationpage.h
@@ -47,6 +47,7 @@ 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_prefix_pb_clicked();
void on_m_material_list_browse_pb_clicked();
void on_m_material_list_create_pb_clicked();
void on_m_indi_text_font_pb_clicked();
diff --git a/sources/ui/configpage/generalconfigurationpage.ui b/sources/ui/configpage/generalconfigurationpage.ui
index 50d46dd6a..2298006ee 100644
--- a/sources/ui/configpage/generalconfigurationpage.ui
+++ b/sources/ui/configpage/generalconfigurationpage.ui
@@ -361,6 +361,16 @@
+ -
+
+
+ Créer ou modifier le préfixe des dossiers de la collection utilisateur (fichier qet_labels.xml)
+
+
+ Configurer les préfixes…
+
+
+
-
diff --git a/sources/ui/prefixconfigurationdialog.cpp b/sources/ui/prefixconfigurationdialog.cpp
new file mode 100644
index 000000000..37eee1608
--- /dev/null
+++ b/sources/ui/prefixconfigurationdialog.cpp
@@ -0,0 +1,254 @@
+/*
+ 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 .
+*/
+#include "prefixconfigurationdialog.h"
+
+#include "../ElementsCollection/qetlabelsfile.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/**
+ @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 &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(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 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);
+ //What the file holds for that folder, kept to tell a row the
+ //user touched from one they left alone (@see accept())
+ parent_item->setData(1, Qt::UserRole, m_labels.prefix(folder).trimmed());
+
+ auto *edit = new QLineEdit(m_labels.prefix(folder).trimmed(), m_tree);
+ edit->setClearButtonEnabled(true);
+ //A folder whose file holds an explicit shows an empty
+ //field too, but does not inherit : say so instead of promising
+ //an inheritance that will not happen
+ edit->setPlaceholderText(m_labels.hasPrefix(folder)
+ ? tr("aucun préfixe : n'hérite pas du parent",
+ "placeholder of an empty prefix field whose folder explicitly has no prefix, which cancels the inheritance")
+ : tr("hériter du dossier parent", "placeholder of an empty prefix field"));
+ connect(edit, &QLineEdit::textEdited, this, [parent_item, edit]() {
+ //Once the user has typed in the field, whatever it holds
+ //when OK is pressed is what the folder gets - an emptied
+ //field then means "inherit" again, even when the file had
+ //an explicit
+ parent_item->setData(1, Qt::UserRole + 1, 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(m_tree->itemWidget(item, 1));
+ if (edit == nullptr) {
+ continue;
+ }
+ const QString text = edit->text().trimmed();
+ //A row the user did not touch is left exactly as the file has
+ //it. Writing every field back would turn an explicit
+ //, which shows empty and cancels the inheritance,
+ //into no at all, and that folder would silently start
+ //inheriting its parent's prefix again.
+ if (!item->data(1, Qt::UserRole + 1).toBool()
+ && text == item->data(1, Qt::UserRole).toString()) {
+ continue;
+ }
+ m_labels.setPrefix(item->data(0, Qt::UserRole).toStringList(), text);
+ }
+
+ 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;
+ }
+
+ if (!m_labels.backupPath().isEmpty()) {
+ QMessageBox::information(this,
+ tr("Fichier endommagé remplacé"),
+ tr("Le fichier %1 était illisible : il a été remplacé.\nSa copie a été conservée sous :\n%2")
+ .arg(m_labels.filePath(), m_labels.backupPath()));
+ }
+
+ 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(watched) != nullptr) {
+ const int key = static_cast(event)->key();
+ if (key == Qt::Key_Return || key == Qt::Key_Enter) {
+ return true;
+ }
+ }
+ return QDialog::eventFilter(watched, event);
+}
diff --git a/sources/ui/prefixconfigurationdialog.h b/sources/ui/prefixconfigurationdialog.h
new file mode 100644
index 000000000..f16d3be8a
--- /dev/null
+++ b/sources/ui/prefixconfigurationdialog.h
@@ -0,0 +1,65 @@
+/*
+ Copyright 2006-2026 The QElectroTech Team
+ This file is part of QElectroTech.
+
+ QElectroTech is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ QElectroTech is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with QElectroTech. If not, see .
+*/
+#ifndef PREFIXCONFIGURATIONDIALOG_H
+#define PREFIXCONFIGURATIONDIALOG_H
+
+#include
+#include
+#include
+
+class QetLabelsFile;
+class QTreeWidget;
+
+/**
+ @brief The PrefixConfigurationDialog class
+
+ Shows the whole directory tree of the user elements collection, one
+ text field per directory, filled with the prefix that directory
+ already has in qet_labels.xml (empty when it has none, which means it
+ inherits the prefix of its parent directory).
+
+ Nothing is written while the dialog is open : validating calls
+ QetLabelsFile::save(), cancelling leaves the file exactly as it was.
+*/
+class PrefixConfigurationDialog : public QDialog
+{
+ Q_OBJECT
+
+ public:
+ explicit PrefixConfigurationDialog(QetLabelsFile &labels, const QList &folders, QWidget *parent = nullptr);
+ ~PrefixConfigurationDialog() override;
+
+ public slots:
+ void accept() override;
+
+ private:
+ void askWhatToDoWithOrphans();
+ void buildTree();
+
+ protected:
+ bool eventFilter(QObject *watched, QEvent *event) override;
+
+ // attributes
+ private:
+ QetLabelsFile &m_labels;
+ QList m_folders;
+ QTreeWidget *m_tree;
+ bool m_remove_orphans = false;
+};
+
+#endif // PREFIXCONFIGURATIONDIALOG_H