// 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 #include #include #include #include #include #include #include "DisplayedFilesModel.h" #include "FileUtilities.h" #include "ThumbnailSource.h" #include #include #include #include #include using namespace Start; 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; } /// 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 QString& pathToFCStdFile) { if (App::ProjectFile proj(pathToFCStdFile.toStdString()); proj.loadDocument()) { try { const QString pathToCachedThumbnail = getPathToCachedThumbnail(pathToFCStdFile); if (!useCachedThumbnail(pathToCachedThumbnail, pathToFCStdFile)) { static const QString pathToThumbnail = defaultThumbnailPath; if (proj.containsFile(pathToThumbnail.toStdString())) { createThumbnailsDir(); const Base::FileInfo fi(pathToCachedThumbnail.toStdString()); Base::ofstream stream(fi, std::ios::out | std::ios::binary); proj.readInputFileDirect(pathToThumbnail.toStdString(), stream); stream.close(); } } if (auto inputFile = QFile(pathToCachedThumbnail); inputFile.exists()) { inputFile.open(QIODevice::OpenModeFlag::ReadOnly); return inputFile.readAll(); } } catch (...) { Base::Console().log("Failed to load thumbnail for %s\n", pathToFCStdFile.toStdString()); } } return {}; } FileStats getFileInfo(const std::string& path) { FileStats result; const Base::FileInfo file(path); if (file.hasExtension("FCStd")) { result = fileInfoFromFreeCADFile(path); } else { result.insert( std::make_pair(DisplayedFilesModelRoles::modifiedTime, getLastModifiedAsString(file)) ); } 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::ranges::find_if( importTypes, [&ext](const auto& item) { return boost::iequals(item, ext); } ) != importTypes.end(); } 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 role) const { const int row = index.row(); if (row < 0 || row >= static_cast(_fileInfoCache.size())) { return {}; } const auto mapEntry = _fileInfoCache.at(row); switch (const auto roleAsType = static_cast(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.contains(roleAsType)) { return QString::fromStdString(mapEntry.at(roleAsType)); } break; case DisplayedFilesModelRoles::image: { if (const auto path = QString::fromStdString(mapEntry.at(DisplayedFilesModelRoles::path)); _imageCache.contains(path)) { return _imageCache[path]; } break; } default: break; } switch (role) { case Qt::ItemDataRole::ToolTipRole: return QString::fromStdString(mapEntry.at(DisplayedFilesModelRoles::path)); default: // No other role gets handled break; } return {}; } void DisplayedFilesModel::addFile(const QString& filePath) { const QFileInfo qfi(filePath); if (!qfi.isReadable()) { return; } if (!freecadCanOpen(qfi.suffix())) { return; } _fileInfoCache.emplace_back(getFileInfo(filePath.toStdString())); const auto lowercaseExtension = qfi.suffix().toLower(); const QStringList ignoredExtensions { QLatin1String("fcmacro"), QLatin1String("py"), QLatin1String("pyi"), QLatin1String("csv"), QLatin1String("txt") }; if (lowercaseExtension == QLatin1String("fcstd")) { if (const auto thumbnail = loadFCStdThumbnail(filePath); !thumbnail.isEmpty()) { _imageCache.insert(filePath, thumbnail); } } else if (ignoredExtensions.contains(lowercaseExtension)) { // Don't try to generate a thumbnail for things like this: FreeCAD can read them, but // there's not much point in showing anything besides a generic icon } else { const auto runner = new ThumbnailSource(filePath); connect( runner->signals(), &ThumbnailSourceSignals::thumbnailAvailable, this, &DisplayedFilesModel::processNewThumbnail ); QThreadPool::globalInstance()->start(runner); } } void DisplayedFilesModel::clear() { _fileInfoCache.clear(); } QHash DisplayedFilesModel::roleNames() const { static QHash nameMap { std::make_pair(static_cast(DisplayedFilesModelRoles::author), "author"), std::make_pair(static_cast(DisplayedFilesModelRoles::baseName), "baseName"), std::make_pair(static_cast(DisplayedFilesModelRoles::company), "company"), std::make_pair(static_cast(DisplayedFilesModelRoles::creationTime), "creationTime"), std::make_pair(static_cast(DisplayedFilesModelRoles::description), "description"), std::make_pair(static_cast(DisplayedFilesModelRoles::image), "image"), std::make_pair(static_cast(DisplayedFilesModelRoles::license), "license"), std::make_pair(static_cast(DisplayedFilesModelRoles::modifiedTime), "modifiedTime"), std::make_pair(static_cast(DisplayedFilesModelRoles::path), "path"), std::make_pair(static_cast(DisplayedFilesModelRoles::size), "size"), }; return nameMap; } void DisplayedFilesModel::processNewThumbnail(const QString& file, const QByteArray& thumbnail) { if (!thumbnail.isEmpty()) { _imageCache.insert(file, thumbnail); // Figure out the index of this file... auto it = std::ranges::find_if(_fileInfoCache, [file](const FileStats& row) { auto pathIt = row.find(DisplayedFilesModelRoles::path); return pathIt != row.end() && pathIt->second == file.toStdString(); }); if (it != _fileInfoCache.end()) { std::size_t index = std::distance(_fileInfoCache.begin(), it); QModelIndex qmi = createIndex(index, 0); Q_EMIT(dataChanged(qmi, qmi, {static_cast(DisplayedFilesModelRoles::image)})); } else { Base::Console().log("Unrecognized path %s\n", file.toStdString()); } } }