Sat, 11 Jun 2022 14:30:30 +0300
Rewrite dependency loading
/* * LDForge: LDraw parts authoring CAD * Copyright (C) 2013 - 2020 Teemu Piippo * * This program 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 3 of the License, or * (at your option) any later version. * * This program 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 this program. If not, see <http://www.gnu.org/licenses/>. */ #include <QFile> #include <QDir> #include <QFileInfo> #include <QSaveFile> #include "documentmanager.h" #include "parser.h" DocumentManager::DocumentManager() { } /** * @brief Creates a new model. * @returns the ID of the new model */ ModelId DocumentManager::newModel() { const ModelId modelId{++this->modelIdCounter}; this->openModels[modelId].id = modelId; this->openModels[modelId].opentype = OpenType::ManuallyOpened; this->makePolygonCacheForModel(modelId); return modelId; } Model* DocumentManager::findDependencyByName(const ModelId modelId, const QString& name) { const auto modelsIterator = this->openModels.find(modelId); if (modelsIterator != std::end(this->openModels)) { const auto& dependencies = modelsIterator->second.dependencies; const auto dependenciesIterator = dependencies.find(name); if (dependenciesIterator != dependencies.end()) { ModelInfo& modelInfo = this->openModels[dependenciesIterator->second]; return modelInfo.model.get(); } else { return nullptr; } } else { return nullptr; } } /** * @brief Gets a model pointer by id or nullptr if not found * @param modelId id of model to find * @returns model pointer or null */ Model *DocumentManager::getModelById(ModelId modelId) { const auto iterator = this->openModels.find(modelId); if (iterator != this->openModels.end()) { return iterator->second.model.get(); } else { return nullptr; } } QString pathToName(const QFileInfo& path) { static const char* paths[] = { "s", "48" "8" }; const QString baseName = path.fileName(); const QString dirName = QFileInfo{path.dir().path()}.fileName(); QString result; if (std::find(std::begin(paths), std::end(paths), dirName) != std::end(paths)) { result = dirName + "\\" + baseName; } else { result = baseName; } return result; } /** * @brief Tries to open the model at the specified path * @param path Path to the model to open * @param errorStream Where to write any errors * @param openType rationale behind opening this file * @returns model id, or no value on error */ std::optional<ModelId> DocumentManager::openModel( const QString& path, QTextStream& errorStream, const OpenType openType ) { QFile file{path}; const QString name = pathToName(path); file.open(QFile::ReadOnly | QFile::Text); std::unique_ptr<Model> newModel = std::make_unique<Model>(nullptr); QTextStream textStream{&file}; Parser parser{file}; parser.parseBody(*newModel); std::optional<ModelId> result; if (file.error() == QFile::NoError) { const ModelId modelId{++this->modelIdCounter}; this->openModels[modelId] = { .model = std::move(newModel), .id = modelId, .path = path, .opentype = openType, .polygonCache = {}, }; this->makePolygonCacheForModel(modelId); result = modelId; } else { errorStream << file.errorString(); } return result; } void DocumentManager::closeDocument(const ModelId modelId) { ModelInfo* modelInfo = findInMap(this->openModels, modelId); if (modelInfo != nullptr) { modelInfo->opentype = OpenType::AutomaticallyOpened; this->prune(); } } const QString *DocumentManager::modelPath(ModelId modelId) const { const auto iterator = this->openModels.find(modelId); if (iterator != this->openModels.end()) { return &iterator->second.path; } else { return nullptr; } } //! \brief Changes the path of the specified model. This can cause dependencies //! to be resolved differently. As such, dependencies need to be resolved for //! all files after this operation. void DocumentManager::setModelPath( const ModelId modelId, const QString &newPath, const LibraryManager &libraries, QTextStream &errorStream) { ModelInfo* info = findInMap(this->openModels, modelId); if (info != nullptr and info->opentype == OpenType::ManuallyOpened) { info->path = newPath; const MissingDependencies missing = this->loadDependenciesForAllModels(libraries); if (not missing.empty()) { errorStream << errorStringFromMissingDependencies(missing); } } } bool DocumentManager::saveModel(const ModelId modelId, QTextStream &errors) { ModelInfo* info = findInMap(this->openModels, modelId); if (info != nullptr) { QSaveFile file{info->path}; file.setDirectWriteFallback(true); if (file.open(QSaveFile::WriteOnly)) { ::save(info->model.get(), &file); const bool commitSucceeded = file.commit(); if (not commitSucceeded) { errors << QObject::tr("Could not save: %1").arg(file.errorString()); return false; } else { return true; } } else { errors << QObject::tr("Could not open %1 for writing: %2") .arg(file.fileName(), file.errorString()); return false; } } else { errors << QObject::tr("Bad model ID %1").arg(modelId.value); return false; } } /** * @brief Searches the open models for the specified model and returns its id if found * @param model model to look for * @return id or no value if not found */ std::optional<ModelId> DocumentManager::findIdForModel(const Model *model) const { std::optional<ModelId> result; for (auto it = this->openModels.begin(); it != this->openModels.end(); ++it) { if (it->second.model.get() == model) { result = it->first; break; } } return result; } PolygonCache *DocumentManager::getPolygonCacheForModel(ModelId modelId) { ModelInfo* info = findInMap(this->openModels, modelId); if (info != nullptr) { return &info->polygonCache; } else { return nullptr; } } const DocumentManager::ModelInfo *DocumentManager::infoForModel(ModelId modelId) const { return findInMap(this->openModels, modelId); } QString errorStringFromMissingDependencies(const DocumentManager::MissingDependencies& missing) { QString missingString; forValueInMap(missing, [&missingString](const QString& path){ missingString = joined(missingString, QStringLiteral(", "), path); }); return QObject::tr("The following files could not be opened: %1") .arg(missingString); } /** * @brief Cleans up and erases models that are no longer required. */ void DocumentManager::prune() { for (auto it = this->openModels.begin(); it != this->openModels.end(); ++it) { // Find models that are not edited by the user and are not needed by any other model if (true and it->second.opentype == OpenType::AutomaticallyOpened and not this->isReferencedByAnything(it->first) ) { // Remove the model this->openModels.erase(it); // We need to start over now. It is possible that other models that // previously were referenced by the model we just erased have // become prunable. Moreover, our iterator is invalid now and we // cannot continue in this loop. this->prune(); break; } } } /** * @brief Finds out whether the specified model id is referenced by any other model * @param modelId * @returns bool */ bool DocumentManager::isReferencedByAnything(const ModelId modelId) const { for (auto& haystackModelPair : this->openModels) { if (haystackModelPair.first != modelId) { for (auto& dependencyPair : haystackModelPair.second.dependencies) { if (dependencyPair.second == modelId) { return true; } } } } return false; } void DocumentManager::makePolygonCacheForModel(const ModelId modelId) { Model* model = this->getModelById(modelId); if (model != nullptr) { const auto modelModified = [this, model]{ const std::optional<ModelId> modelId = this->findIdForModel(model); if (modelId.has_value()) { ModelInfo* info = findInMap(this->openModels, *modelId); if (info != nullptr) { info->polygonCache.needRecache = true; } } }; QObject::connect(model, &Model::dataChanged, modelModified); QObject::connect(model, &Model::rowsInserted, modelModified); QObject::connect(model, &Model::rowsRemoved, modelModified); } } static QString findFile( QString referenceName, const QString& modelPath, const LibraryManager& libraries) { // Try to find the file in the same place as the model itself referenceName.replace("\\", "/"); const QDir dir = QFileInfo{modelPath}.dir(); QString referencedFilePath = dir.filePath(referenceName); if (not QFileInfo{referencedFilePath}.exists()) { // Look for it in the libraries referencedFilePath = libraries.findFile(referenceName); } return referencedFilePath; } static std::set<QString> referenceNames(const Model* model) { std::set<QString> result; iterate<Colored<SubfileReference>>(*model, [&result](const SubfileReference& ref){ result.insert(ref.name); }); return result; } struct Dependency { QString name; QString path; bool operator<(const Dependency& other) const { if (this->name != other.name) { return this->name < other.name; } else { return this->path < other.path; } } }; static std::set<Dependency> resolveReferencePaths( const DocumentManager::ModelInfo* modelInfo, const LibraryManager* libraries) { std::set<Dependency> result; const std::set<QString> refNames = referenceNames(modelInfo->model.get()); if (modelInfo != nullptr) { for (const QString& name : refNames) { const QString path = findFile(name, modelInfo->path, *libraries); if (not path.isEmpty()) { result.insert(Dependency{.name = name, .path = path}); } } } return result; } static void loadDependenciesForModel( DocumentManager::ModelInfo* info, DocumentManager* documents, const LibraryManager* libraries, std::map<QString, QString>& missing) { bool repeat = true; info->dependencies.clear(); while (repeat) { repeat = false; const std::set<Dependency> dependencies = resolveReferencePaths(info, libraries); for (const Dependency& dep : dependencies) { if (not info->dependencies.contains(dep.name) and not missing.contains(dep.path)) { QString loadErrorString; QTextStream localErrorStream{&loadErrorString}; const std::optional<ModelId> modelIdOpt = documents->openModel( dep.path, localErrorStream, OpenType::AutomaticallyOpened); if (not modelIdOpt.has_value()) { const QString& errorMessage = QObject::tr("could not load '%1': %2") .arg(dep.path, loadErrorString); missing[dep.path] = errorMessage; } else { info->dependencies[dep.name] = modelIdOpt.value(); repeat = true; } } } } } std::map<QString, QString> DocumentManager::loadDependenciesForAllModels(const LibraryManager& libraries) { std::map<QString, QString> missing; for (auto& modelInfoPair : this->openModels) { loadDependenciesForModel(&modelInfoPair.second, this, &libraries, missing); } this->prune(); return missing; }