It's basically a port of #10951 to the new start page implementation. Note: Icon files are not removed but re-used instead. The commit adds some new functions: * getThumbnailsImage() Returns the name of the PNG inside a project file * getThumbnailsName() Returns the directory name containing the image files * getThumnailsParentDir() Returns the parent directory of the directory containing the image files * getThumbnailsDir() Returns the path to the thumbnail directory. There is no need to always create a unique directory after each restart because it doesn't harm if the thumbnail directoy contains deprecated files. * createThumbnailsDir() Creates the thumbnail directoy if it doesn't exist yet. * getSha1Hash Helper function to compute a SHA-1 hash of a given path. If the same path is passed then the hash value will be the same. This way it can be avoided to create a different image file from a project file after each restart. * getUniquePNG Computes the path of a PNG image file for a given project file. It's also possible to pass an arbitrary string as argument. * useCachedPNG If the PNG image exists and if it's newer than the project file True is returned and False otherwise. For a given project file it is checked if the thumbnail directory already contains a cached image. If it's newer than the project file it will used, otherwise it will be re-created. Fix freecadCanOpen() abd DisplayedFilesModel::addFile() to also check for lower-case file extensions.
311 lines
11 KiB
C++
311 lines
11 KiB
C++
// SPDX-License-Identifier: LGPL-2.1-or-later
|
|
/****************************************************************************
|
|
* *
|
|
* Copyright (c) 2024 The FreeCAD Project Association AISBL *
|
|
* *
|
|
* This file is part of FreeCAD. *
|
|
* *
|
|
* FreeCAD is free software: you can redistribute it and/or modify it *
|
|
* under the terms of the GNU Lesser General Public License as *
|
|
* published by the Free Software Foundation, either version 2.1 of the *
|
|
* License, or (at your option) any later version. *
|
|
* *
|
|
* FreeCAD 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 *
|
|
* Lesser General Public License for more details. *
|
|
* *
|
|
* You should have received a copy of the GNU Lesser General Public *
|
|
* License along with FreeCAD. If not, see *
|
|
* <https://www.gnu.org/licenses/>. *
|
|
* *
|
|
***************************************************************************/
|
|
|
|
#include "PreCompiled.h"
|
|
#ifndef _PreComp_
|
|
#include <boost/algorithm/string/predicate.hpp>
|
|
#include <QByteArray>
|
|
#include <QCryptographicHash>
|
|
#include <QDateTime>
|
|
#include <QDir>
|
|
#include <QFile>
|
|
#include <QFileInfo>
|
|
#endif
|
|
|
|
#include "DisplayedFilesModel.h"
|
|
#include <App/Application.h>
|
|
#include <App/ProjectFile.h>
|
|
#include <Base/FileInfo.h>
|
|
#include <Base/Stream.h>
|
|
|
|
using namespace Start;
|
|
|
|
|
|
namespace
|
|
{
|
|
|
|
std::string humanReadableSize(unsigned int bytes)
|
|
{
|
|
static const std::vector<std::string> siPrefix {
|
|
"b",
|
|
"kb",
|
|
"Mb",
|
|
"Gb",
|
|
"Tb",
|
|
"Pb",
|
|
"Eb" // I think it's safe to stop here (for the time being)...
|
|
};
|
|
size_t base = 0;
|
|
double inUnits = bytes;
|
|
constexpr double siFactor {1000.0};
|
|
while (inUnits > siFactor && base < siPrefix.size() - 1) {
|
|
++base;
|
|
inUnits /= siFactor;
|
|
}
|
|
if (base == 0) {
|
|
// Don't include a decimal point for bytes
|
|
return fmt::format("{:.0f} {}", inUnits, siPrefix[base]);
|
|
}
|
|
// For all others, include one digit after the decimal place
|
|
return fmt::format("{:.1f} {}", inUnits, siPrefix[base]);
|
|
}
|
|
|
|
FileStats fileInfoFromFreeCADFile(const std::string& path)
|
|
{
|
|
App::ProjectFile proj(path);
|
|
proj.loadDocument();
|
|
auto metadata = proj.getMetadata();
|
|
FileStats result;
|
|
result.insert(std::make_pair(DisplayedFilesModelRoles::author, metadata.createdBy));
|
|
result.insert(
|
|
std::make_pair(DisplayedFilesModelRoles::modifiedTime, metadata.lastModifiedDate));
|
|
result.insert(std::make_pair(DisplayedFilesModelRoles::creationTime, metadata.creationDate));
|
|
result.insert(std::make_pair(DisplayedFilesModelRoles::company, metadata.company));
|
|
result.insert(std::make_pair(DisplayedFilesModelRoles::license, metadata.license));
|
|
result.insert(std::make_pair(DisplayedFilesModelRoles::description, metadata.comment));
|
|
return result;
|
|
}
|
|
|
|
std::string getThumbnailsImage()
|
|
{
|
|
return "thumbnails/Thumbnail.png";
|
|
}
|
|
|
|
QString getThumbnailsName()
|
|
{
|
|
return QString::fromLatin1("FreeCADStartThumbnails");
|
|
}
|
|
|
|
QDir getThumnailsParentDir()
|
|
{
|
|
return QDir::temp();
|
|
}
|
|
|
|
QString getThumbnailsDir()
|
|
{
|
|
QDir dir = getThumnailsParentDir();
|
|
return dir.absoluteFilePath(getThumbnailsName());
|
|
}
|
|
|
|
void createThumbnailsDir()
|
|
{
|
|
QString name = getThumbnailsName();
|
|
QDir dir(getThumnailsParentDir());
|
|
if (!dir.exists(name)) {
|
|
dir.mkpath(name);
|
|
}
|
|
}
|
|
|
|
QString getSha1Hash(const std::string& path)
|
|
{
|
|
QCryptographicHash hash(QCryptographicHash::Sha1);
|
|
hash.addData(path.c_str(), static_cast<int>(path.size()));
|
|
QByteArray ba1 = hash.result().toHex();
|
|
hash.reset();
|
|
hash.addData(ba1);
|
|
QByteArray ba2 = hash.result().toHex();
|
|
return QString::fromLatin1(ba2);
|
|
}
|
|
|
|
QString getUniquePNG(const std::string& path)
|
|
{
|
|
QDir dir = getThumbnailsDir();
|
|
QString sha1 = getSha1Hash(path) + QLatin1String(".png");
|
|
return dir.absoluteFilePath(sha1);
|
|
}
|
|
|
|
bool useCachedPNG(const std::string& image, const std::string& project)
|
|
{
|
|
Base::FileInfo f1(image);
|
|
Base::FileInfo f2(project);
|
|
if (!f1.exists()) {
|
|
return false;
|
|
}
|
|
if (!f2.exists()) {
|
|
return false;
|
|
}
|
|
|
|
return f1.lastModified() > f2.lastModified();
|
|
}
|
|
|
|
/// Load the thumbnail image data (if any) that is stored in an FCStd file.
|
|
/// \returns The image bytes, or an empty QByteArray (if no thumbnail was stored)
|
|
QByteArray loadFCStdThumbnail(const std::string& pathToFCStdFile)
|
|
{
|
|
App::ProjectFile proj(pathToFCStdFile);
|
|
if (proj.loadDocument()) {
|
|
try {
|
|
std::string thumbnailFile = getUniquePNG(pathToFCStdFile).toStdString();
|
|
if (!useCachedPNG(thumbnailFile, pathToFCStdFile)) {
|
|
static std::string thumb = getThumbnailsImage();
|
|
if (proj.containsFile(thumb)) {
|
|
createThumbnailsDir();
|
|
Base::FileInfo fi(thumbnailFile);
|
|
Base::ofstream str(fi);
|
|
proj.readInputFileDirect(thumb, str);
|
|
str.close();
|
|
}
|
|
}
|
|
|
|
auto inputFile = QFile(QString::fromStdString(thumbnailFile));
|
|
if (inputFile.exists()) {
|
|
inputFile.open(QIODevice::OpenModeFlag::ReadOnly);
|
|
return inputFile.readAll();
|
|
}
|
|
}
|
|
catch (...) {
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
|
|
FileStats getFileInfo(const std::string& path)
|
|
{
|
|
FileStats result;
|
|
Base::FileInfo file(path);
|
|
if (file.hasExtension("FCStd")) {
|
|
result = fileInfoFromFreeCADFile(path);
|
|
}
|
|
else {
|
|
file.lastModified();
|
|
}
|
|
result.insert(std::make_pair(DisplayedFilesModelRoles::path, path));
|
|
result.insert(std::make_pair(DisplayedFilesModelRoles::size, humanReadableSize(file.size())));
|
|
result.insert(std::make_pair(DisplayedFilesModelRoles::baseName, file.fileName()));
|
|
return result;
|
|
}
|
|
|
|
bool freecadCanOpen(const QString& extension)
|
|
{
|
|
std::string ext = extension.toStdString();
|
|
auto importTypes = App::GetApplication().getImportTypes();
|
|
return std::find_if(importTypes.begin(),
|
|
importTypes.end(),
|
|
[&ext](const auto& item) {
|
|
return boost::iequals(item, ext);
|
|
})
|
|
!= importTypes.end();
|
|
}
|
|
|
|
} // namespace
|
|
|
|
DisplayedFilesModel::DisplayedFilesModel(QObject* parent)
|
|
: QAbstractListModel(parent)
|
|
{}
|
|
|
|
|
|
int DisplayedFilesModel::rowCount(const QModelIndex& parent) const
|
|
{
|
|
Q_UNUSED(parent);
|
|
return static_cast<int>(_fileInfoCache.size());
|
|
}
|
|
|
|
QVariant DisplayedFilesModel::data(const QModelIndex& index, int roleAsInt) const
|
|
{
|
|
int row = index.row();
|
|
if (row < 0 || row >= static_cast<int>(_fileInfoCache.size())) {
|
|
return {};
|
|
}
|
|
auto mapEntry = _fileInfoCache.at(row);
|
|
auto role = static_cast<DisplayedFilesModelRoles>(roleAsInt);
|
|
switch (role) {
|
|
case DisplayedFilesModelRoles::author: // NOLINT(bugprone-branch-clone)
|
|
[[fallthrough]];
|
|
case DisplayedFilesModelRoles::baseName:
|
|
[[fallthrough]];
|
|
case DisplayedFilesModelRoles::company:
|
|
[[fallthrough]];
|
|
case DisplayedFilesModelRoles::creationTime:
|
|
[[fallthrough]];
|
|
case DisplayedFilesModelRoles::description:
|
|
[[fallthrough]];
|
|
case DisplayedFilesModelRoles::license:
|
|
[[fallthrough]];
|
|
case DisplayedFilesModelRoles::modifiedTime:
|
|
[[fallthrough]];
|
|
case DisplayedFilesModelRoles::path:
|
|
[[fallthrough]];
|
|
case DisplayedFilesModelRoles::size:
|
|
if (mapEntry.find(role) != mapEntry.end()) {
|
|
return QString::fromStdString(mapEntry.at(role));
|
|
}
|
|
else {
|
|
return {};
|
|
}
|
|
case DisplayedFilesModelRoles::image: {
|
|
auto path = QString::fromStdString(mapEntry.at(DisplayedFilesModelRoles::path));
|
|
if (_imageCache.contains(path)) {
|
|
return _imageCache[path];
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
break;
|
|
}
|
|
switch (roleAsInt) {
|
|
case Qt::ItemDataRole::ToolTipRole:
|
|
return QString::fromStdString(mapEntry.at(DisplayedFilesModelRoles::path));
|
|
}
|
|
return {};
|
|
}
|
|
|
|
void DisplayedFilesModel::addFile(const QString& filePath)
|
|
{
|
|
QFileInfo qfi(filePath);
|
|
if (!qfi.isReadable()) {
|
|
return;
|
|
}
|
|
if (!freecadCanOpen(qfi.suffix())) {
|
|
return;
|
|
}
|
|
_fileInfoCache.emplace_back(getFileInfo(filePath.toStdString()));
|
|
if (qfi.completeSuffix().toLower() == QLatin1String("fcstd")) {
|
|
auto thumbnail = loadFCStdThumbnail(filePath.toStdString());
|
|
if (!thumbnail.isEmpty()) {
|
|
_imageCache.insert(filePath, thumbnail);
|
|
}
|
|
}
|
|
}
|
|
|
|
void DisplayedFilesModel::clear()
|
|
{
|
|
_fileInfoCache.clear();
|
|
}
|
|
|
|
QHash<int, QByteArray> DisplayedFilesModel::roleNames() const
|
|
{
|
|
static QHash<int, QByteArray> nameMap {
|
|
std::make_pair(int(DisplayedFilesModelRoles::author), "author"),
|
|
std::make_pair(int(DisplayedFilesModelRoles::baseName), "baseName"),
|
|
std::make_pair(int(DisplayedFilesModelRoles::company), "company"),
|
|
std::make_pair(int(DisplayedFilesModelRoles::creationTime), "creationTime"),
|
|
std::make_pair(int(DisplayedFilesModelRoles::description), "description"),
|
|
std::make_pair(int(DisplayedFilesModelRoles::image), "image"),
|
|
std::make_pair(int(DisplayedFilesModelRoles::license), "license"),
|
|
std::make_pair(int(DisplayedFilesModelRoles::modifiedTime), "modifiedTime"),
|
|
std::make_pair(int(DisplayedFilesModelRoles::path), "path"),
|
|
std::make_pair(int(DisplayedFilesModelRoles::size), "size"),
|
|
};
|
|
return nameMap;
|
|
}
|