Files
qelectrotech-source-mirror/sources/ElementsCollection/fileelementcollectionitem.cpp
T
ispyisail 87a010eb75 Never leave a collection folder without a name, and say when it is broken
Fixes https://qelectrotech.org/bugtracker/view.php?id=332

localName() set a non-root folder's label only inside the success path of
loading its qet_directory file. If that load failed -- file missing,
malformed, or unopenable because of the Windows path-encoding problem with
accented characters that plc-user diagnosed on the tracker -- nothing was
set at all, and since a fresh item's text() is null the folder rendered
with a completely blank label. That is the reported symptom.

Resolve the name into a local and always fall back to the folder's own
directory name, so the label is never empty whatever went wrong.

The fallback is applied *after* NamesList::name() rather than passed into
it. This matters: name() returns a caller-supplied fallback before it
reaches its "first available translation" step, so passing m_path in would
replace a perfectly good name in some other language with the raw
directory name. A folder named only in French, viewed under an English
locale, previously showed "Accentué" and must keep doing so.

Falling back on its own would then hide the broken file -- the user sees a
plausible name and never learns there is anything to repair. So a folder
whose qet_directory could not be read now says so in its tooltip, naming
the file, above the collection path that tooltip already carried.
Suggested by plc-user on PR #622. The flag is recorded in localName() and
consumed in setUpData(), because setUpData() assigns the tooltip after
localName() runs and would otherwise discard it.

Only a file-level failure is flagged. A readable qet-directory with no
entry for the current language is not an error; NamesList::name() resolves
that itself and no warning is shown.

Verified on a fixture collection of four folders -- valid, malformed,
missing, and one named only in French:

                  master        this patch
  fr-only         Accentué      Accentué        (no warning)
  malformed       <blank>       malformed       (warning)
  no qet_directory <blank>      no_file         (warning)
  valid           Valid Folder  Valid Folder    (no warning)
2026-08-02 18:13:38 +12:00

501 lines
14 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 "fileelementcollectionitem.h"
#include "../qetapp.h"
#include "../qeticons.h"
#include "elementslocation.h"
#include <QDir>
/**
@brief FileElementCollectionItem::FileElementCollectionItem
Constructor
*/
FileElementCollectionItem::FileElementCollectionItem()
{}
/**
@brief FileElementCollectionItem::setRootPath
Set path has root path for this file item.
Use this function only to set the beginning of a file collection.
@param path
@param set_data
@param hide_element
@return true if path exist.
*/
bool FileElementCollectionItem::setRootPath(const QString& path,
bool set_data,
bool hide_element)
{
QDir dir(path);
if (dir.exists())
{
m_path = path;
populate(set_data, hide_element);
return true;
}
return false;
}
/**
@brief FileElementCollectionItem::fileSystemPath
@return the file system path of this item
*/
QString FileElementCollectionItem::fileSystemPath() const
{
if (isCollectionRoot())
return m_path;
FileElementCollectionItem *feci =
static_cast<FileElementCollectionItem *> (parent());
if (feci)
return feci->fileSystemPath() % "/" % m_path;
else
return QString();//Null string
}
/**
@brief FileElementCollectionItem::dirPath
@return the dir path of this item (if this item is a dir return the path,
if item is an element return the path of the parent directory)
*/
QString FileElementCollectionItem::dirPath() const
{
if (isDir())
return fileSystemPath();
else if (parent() && parent()->type() == FileElementCollectionItem::Type)
return static_cast<FileElementCollectionItem*>(parent())->fileSystemPath();
else
return QString();//Null string
}
/**
@brief FileElementCollectionItem::isDir
@return true if this item represent a directory
*/
bool FileElementCollectionItem::isDir() const
{
if (m_path.endsWith(".elmt") || m_path.endsWith(".qetmak"))
return false;
else
return true;
}
/**
@brief FileElementCollectionItem::isElement
@return true if this item represent an element
*/
bool FileElementCollectionItem::isElement() const
{
return (!isDir());
}
/**
* @brief FileElementCollectionItem::localName
* @return the located name of this item
*/
QString FileElementCollectionItem::localName()
{
if (!text().isNull())
return text();
else if (isDir()) {
if (isCollectionRoot()) {
QString macrosPath = QETApp::userMacrosDir();
if (macrosPath.endsWith("/")) macrosPath.remove(macrosPath.length() - 1, 1);
if (m_path == QETApp::commonElementsDirN())
setText(QObject::tr("Collection QET"));
else if (m_path == QETApp::companyElementsDirN())
setText(QObject::tr("Collection Company"));
else if (m_path == QETApp::customElementsDirN())
setText(QObject::tr("Collection utilisateur"));
else if (m_path == macrosPath)
setText(QObject::tr("Makros"));
else
setText(QObject::tr("Collection inconnue"));
}
else
{
// Fall back to the raw directory name (m_path) whenever the
// translated name can't be obtained -- qet_directory missing,
// unreadable (e.g. a Windows path-encoding issue with special
// characters, see bugtracker #332), malformed, or present but
// without a usable name entry -- rather than leaving the item
// blank.
QString display_name;
bool readable = false;
QString str(fileSystemPath() % "/qet_directory");
pugi::xml_document docu;
if (docu.load_file(str.toStdWString().c_str()))
{
if (QString(docu.document_element().name())
== "qet-directory")
{
readable = true;
NamesList nl;
nl.fromXml(docu.document_element());
// Deliberately no fallback argument: a non-empty one
// is returned *before* NamesList::name() reaches its
// "first available translation" step, so passing
// m_path here would replace a perfectly good name in
// some other language with the raw directory name.
// The fallback belongs after the chain, not inside it.
display_name = nl.name();
}
}
setText(display_name.isEmpty() ? m_path : display_name);
// Only a file-level failure counts: a readable qet-directory
// with no entry for the current language is not an error,
// NamesList::name() resolves that on its own. Recorded here
// and reported by setUpData(), which sets the tooltip after
// this runs.
m_qet_directory_unreadable = !readable;
}
}
else if (isElement()) {
ElementsLocation loc(collectionPath());
QString display_name = loc.name();
if (display_name.endsWith(".qetmak")) {
display_name.remove(".qetmak");
}
setText(display_name);
}
return text();
}
/**
@brief FileElementCollectionItem::localName
Surcharged method, unlike the default method,
avoid to create an elementLocation and so, gain time.
@param location
@return
*/
QString FileElementCollectionItem::localName(const ElementsLocation &location)
{
if (!text().isNull())
return text();
else if (isDir()) {
localName();
}
else if (isElement()) {
QString display_name = location.name();
if (display_name.endsWith(".qetmak")) {
display_name.remove(".qetmak");
}
setText(display_name);
}
return text();
}
/**
@brief FileElementCollectionItem::name
@return The collection name of this item
*/
QString FileElementCollectionItem::name() const
{
if (isCollectionRoot())
return QString();
else
return m_path;
}
/**
@brief FileElementCollectionItem::collectionPath
@return The path of this item relative to the collection.
*/
QString FileElementCollectionItem::collectionPath() const
{
if (isCollectionRoot()) {
QString macrosPath = QETApp::userMacrosDir();
if (macrosPath.endsWith("/")) macrosPath.remove(macrosPath.length() - 1, 1);
if (m_path == QETApp::commonElementsDirN())
return "common://";
else if (m_path == QETApp::companyElementsDirN())
return "company://";
else if (m_path == macrosPath)
return "macros://"; //
else
return "custom://";
}
else if (parent() && parent()->type()
== FileElementCollectionItem::Type) {
ElementCollectionItem *eci =
static_cast<ElementCollectionItem*>(parent());
if (eci->isCollectionRoot())
return eci->collectionPath() + m_path;
else
return eci->collectionPath() % "/" % m_path;
}
else
return QString();
}
/**
@brief FileElementCollectionItem::isCollectionRoot
@return true if this item represent the root of collection
*/
bool FileElementCollectionItem::isCollectionRoot() const
{
QString macrosPath = QETApp::userMacrosDir();
if (macrosPath.endsWith("/")) macrosPath.remove(macrosPath.length() - 1, 1);
if (m_path == QETApp::commonElementsDirN()
|| m_path == QETApp::companyElementsDirN()
|| m_path == QETApp::customElementsDirN()
|| m_path == macrosPath)
return true;
else
return false;
}
/**
@brief FileElementCollectionItem::isCommonCollection
@return True if this item represent the common collection
*/
bool FileElementCollectionItem::isCommonCollection() const
{
return fileSystemPath().startsWith(QETApp::commonElementsDirN());
}
/**
@brief FileElementCollectionItem::isCompanyCollection
@return True if this item represent the company collection
*/
bool FileElementCollectionItem::isCompanyCollection() const
{
return fileSystemPath().startsWith(QETApp::companyElementsDirN());
}
/**
@brief FileElementCollectionItem::isCustomCollection
@return True if this item represent the custom collection
*/
bool FileElementCollectionItem::isCustomCollection() const
{
const QString dir = QETApp::customElementsDirN();
const QString path = fileSystemPath();
return path == dir || path.startsWith(dir + QLatin1Char('/'));
}
/**
@brief FileElementCollectionItem::addChildAtPath
Ask to this item item to add a child
with collection name collection_name
@param collection_name
*/
void FileElementCollectionItem::addChildAtPath(const QString &collection_name)
{
if (collection_name.isEmpty())
return;
FileElementCollectionItem *feci = new FileElementCollectionItem();
insertRow(rowForInsertItem(collection_name), feci);
feci->setPathName(collection_name);
feci->setUpData();
}
/**
* @brief FileElementCollectionItem::setUpData
* SetUp the data of this item
*/
void FileElementCollectionItem::setUpData()
{
if (isDir())
{
localName();
setFlags(Qt::ItemIsSelectable
| Qt::ItemIsDragEnabled
| Qt::ItemIsDropEnabled
| Qt::ItemIsEnabled);
}
else
{
setFlags(Qt::ItemIsSelectable
| Qt::ItemIsDragEnabled
| Qt::ItemIsEnabled);
if (m_path.endsWith(".qetmak")) {
setData(localName());
} else {
// Parse standard element information for search
ElementsLocation loc(collectionPath());
DiagramContext context = loc.elementInformations();
QStringList search_list;
for (QString& key : context.keys())
{ search_list.append(context.value(key).toString()); }
search_list.append(localName(loc));
setData(search_list.join(" "));
// Tooltip: show what a truncated tree label can't - the full
// localized name and the descriptive element information.
// Reuses the location/context already parsed for the search
// index above; the collection path stays as the last line
// (it used to be the whole tooltip).
QStringList tip;
tip << localName(loc);
for (const auto &key : { QStringLiteral("description"),
QStringLiteral("manufacturer"),
QStringLiteral("manufacturer_reference") })
{
const QString value = context.value(key).toString();
if (!value.isEmpty())
tip << value;
}
tip << collectionPath();
tip.removeDuplicates();
setToolTip(tip.join(QLatin1Char('\n')));
return;
}
}
// Falling back to the raw directory name keeps the folder usable, but
// on its own it hides the fact that a file is broken: the user sees a
// plausible name and never learns there is anything to repair. Say so
// above the collection path, which stays as the last line the way the
// element tooltip above builds it.
QStringList tip;
if (isDir() && m_qet_directory_unreadable)
{
tip << QObject::tr("Le fichier « %1 » est absent ou illisible : "
"le nom traduit de ce dossier n'a pas pu être lu, "
"son nom de dossier est affiché à la place.")
.arg(fileSystemPath() % "/qet_directory");
}
tip << collectionPath();
setToolTip(tip.join(QLatin1Char('\n')));
}
/**
* @brief FileElementCollectionItem::setUpIcon
* SetUp the icon of this item.
* Because icon use several memory,
* we use this method for setup icon instead setUpData.
*/
void FileElementCollectionItem::setUpIcon()
{
if (!icon().isNull())
return;
if (isCollectionRoot()) {
QString macrosPath = QETApp::userMacrosDir();
if (macrosPath.endsWith("/")) macrosPath.remove(macrosPath.length() - 1, 1);
if (m_path == QETApp::commonElementsDirN())
setIcon(QIcon(":/ico/16x16/qet.png"));
else if (m_path == QETApp::companyElementsDirN())
setIcon(QIcon(":/ico/16x16/go-company.png"));
else if (m_path == macrosPath)
setIcon(QIcon(":/ico/16x16/go-home.png"));
else
setIcon(QIcon(":/ico/16x16/go-home.png"));
}
else
{
if (isDir()) {
setIcon(QET::Icons::Folder);
} else {
if (m_path.endsWith(".qetmak")) {
setIcon(QIcon());
} else {
ElementsLocation loc(collectionPath());
setIcon(loc.icon());
}
}
}
}
/**
@brief FileElementCollectionItem::setPathName
Set the name of this item in the file system path.
This item must have a parent,
because they should be a child item of another.
For create a new file collection see setRootPath.
@param path_name
@param set_data
@param hide_element
*/
void FileElementCollectionItem::setPathName(const QString& path_name,
bool set_data,
bool hide_element)
{
m_path = path_name;
//This isn't an element or template, we create the childs
if (!path_name.endsWith(".elmt") && !path_name.endsWith(".qetmak"))
populate(set_data, hide_element);
}
/**
@brief FileElementCollectionItem::populate
Create the childs of this item
@param set_data : if true, call setUpData for every child of this item
@param hide_element
*/
void FileElementCollectionItem::populate(bool set_data, bool hide_element)
{
QDir dir (fileSystemPath());
//Get all directory in this directory.
for (auto& str :
dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name))
{
FileElementCollectionItem *feci = new FileElementCollectionItem();
appendRow(feci);
feci->setPathName(str, set_data, hide_element);
if (set_data)
feci->setUpData();
}
if (hide_element)
return;
//Get all elmt file in this directory
dir.setNameFilters(QStringList() << "*.elmt" << "*.qetmak");
for (auto& str :
dir.entryList(QDir::Files | QDir::NoDotAndDotDot, QDir::Name))
{
FileElementCollectionItem *feci = new FileElementCollectionItem();
appendRow(feci);
feci->setPathName(str, set_data);
if (set_data)
feci->setUpData();
}
}
/**
* @brief FileElementCollectionItem::isMacrosCollection
* @return True if this item represent the macros collection
*/
bool FileElementCollectionItem::isMacrosCollection() const
{
QString macrosPath = QETApp::userMacrosDir();
if (macrosPath.endsWith("/")) macrosPath.remove(macrosPath.length() - 1, 1);
return fileSystemPath().startsWith(macrosPath);
}