// 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 * * . * * * ***************************************************************************/ #include "PreCompiled.h" #ifndef _PreComp_ #include #include #include #include #include #include #include #endif #include "DisplayedFilesModel.h" #include #include #include #include using namespace Start; namespace { std::string humanReadableSize(unsigned int bytes) { static const std::vector 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(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(_fileInfoCache.size()); } QVariant DisplayedFilesModel::data(const QModelIndex& index, int roleAsInt) const { int row = index.row(); if (row < 0 || row >= static_cast(_fileInfoCache.size())) { return {}; } auto mapEntry = _fileInfoCache.at(row); auto role = static_cast(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 DisplayedFilesModel::roleNames() const { static QHash 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; }