Mon, 06 Jun 2022 22:01:22 +0300
Giant refactor
/* * 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" /** * @brief Constructs a new document manager * @param parent Parent object */ DocumentManager::DocumentManager(QObject* parent) : QObject{parent} { } /** * @brief Creates a new model. * @returns the ID of the new model */ ModelId DocumentManager::newModel() { const ModelId modelId{++this->modelIdCounter}; const QString name = makeNewModelName(); this->openModels[modelId] = ModelInfo{ .model = std::make_unique<Model>(this), .id = modelId, .opentype = OpenType::ManuallyOpened, }; this->makePolygonCacheForModel(modelId); return modelId; } /** * @brief Looks for a model by name * @param name Name of the model * @returns model or null */ 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>(this); 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] = {std::move(newModel), modelId, path, openType}; this->makePolygonCacheForModel(modelId); result = modelId; } else { errorStream << file.errorString(); } return result; } QString DocumentManager::makeNewModelName() { untitledNameCounter += 1; return "untitled-" + QString::number(untitledNameCounter); } void DocumentManager::loadDependenciesForAllModels(const LibraryManager& libraries, QTextStream& errorStream) { for (const auto& modelInfoPair : this->openModels) { this->loadDependenciesForModel(modelInfoPair.first, modelInfoPair.second.path, libraries, errorStream); } } struct DocumentManager::LoadDepedenciesBag { const LibraryManager& libraries; QStringList missing; QSet<ModelId> processed; QTextStream& errorStream; }; void DocumentManager::loadDependenciesForModel( const ModelId modelId, const QString& path, const LibraryManager& libraries, QTextStream& errorStream) { LoadDepedenciesBag bag { .libraries = libraries, .missing = {}, .processed = {}, .errorStream = errorStream, }; this->loadDependenciesForModel(modelId, path, bag); if (not bag.missing.empty()) { bag.missing.sort(Qt::CaseInsensitive); errorStream << utility::format( "The following files could not be opened: %1", bag.missing.join(", ")); } } 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. Since the name of the file may change, * changing the path can cause dependencies to be resolved differently. As such, dependencies * need to be resolved for all files after this operation. * @param modelId Model to change the path of * @param newPath New path * @param libraries Library manager for the purpose of dependency resolving * @param errorStream Where to write any errors regarding dependency resolving */ void DocumentManager::setModelPath( const ModelId modelId, const QString &newPath, const LibraryManager &libraries, QTextStream &errorStream) { auto modelInfoPair = this->openModels.find(modelId); if (true and modelInfoPair != this->openModels.end() and modelInfoPair->second.opentype == OpenType::ManuallyOpened ) { modelInfoPair->second.path = newPath; this->loadDependenciesForAllModels(libraries, errorStream); } } bool DocumentManager::saveModel(const ModelId modelId, QTextStream &errors) { const QString* const path = this->modelPath(modelId); if (path != nullptr) { QSaveFile file{*path}; file.setDirectWriteFallback(true); if (file.open(QSaveFile::WriteOnly)) { // if path is not nullptr, getModelById will always return a value as well ::save(*this->getModelById(modelId), &file); const bool commitSucceeded = file.commit(); if (not commitSucceeded) { errors << tr("Could not save: %1").arg(file.errorString()); return false; } else { return true; } } else { errors << tr("Could not open %1 for writing: %2") .arg(file.fileName()) .arg(file.errorString()); return false; } } else { errors << 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) { auto it = this->polygonCaches.find(modelId); if (it != this->polygonCaches.end()) { return &it->second; } else { return nullptr; } } /** * @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 its polygon cache const auto polygonCache = this->polygonCaches.find(it->first); if (polygonCache != this->polygonCaches.end()) { this->polygonCaches.erase(polygonCache); } // 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 for 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) { this->polygonCaches[modelId] = {}; connect(model, &Model::dataChanged, this, &DocumentManager::modelModified); connect(model, &Model::rowsInserted, this, &DocumentManager::modelModified); connect(model, &Model::rowsRemoved, this, &DocumentManager::modelModified); } } void DocumentManager::modelModified() { Model* const model = qobject_cast<Model*>(this->sender()); const std::optional<ModelId> modelId = this->findIdForModel(model); if (modelId.has_value()) { this->polygonCaches[*modelId].needRecache = true; } } static QString findFile(QString referenceName, const QString& path, const LibraryManager& libraries) { // Try to find the file in the same place as the model itself referenceName.replace("\\", "/"); const QDir dir = QFileInfo{path}.dir(); QString referencedFilePath = dir.filePath(referenceName); if (not QFileInfo{referencedFilePath}.exists()) { // Look for it in the libraries referencedFilePath = libraries.findFile(referenceName); } return referencedFilePath; } template<typename T> void iterate(const Model& model, std::function<void(const T&)> fn) { for (int i = 0; i < model.size(); ++i) { if (std::holds_alternative<T>(model[i])) { fn(std::get<T>(model[i])); } } } void DocumentManager::loadDependenciesForModel( const ModelId modelId, const QString &path, LoadDepedenciesBag& bag) { QSet<QString> failedToOpen; struct LoadingError { QString message; }; bag.processed.insert(modelId); if (not this->openModels.contains(modelId)) { bag.errorStream << tr("bad model ID %1").arg(modelId.value); return; } ModelInfo& modelInfo = this->openModels[modelId]; modelInfo.dependencies.clear(); iterate<Colored<SubfileReference>>(*modelInfo.model, [&](const SubfileReference& ref) { const QString referenceName = ref.name; if (not referenceName.isEmpty() and modelInfo.dependencies.count(referenceName) == 0 and not failedToOpen.contains(referenceName)) { try { const QString referencedFilePath = ::findFile(referenceName, path, bag.libraries); if (referencedFilePath.isEmpty()) { throw LoadingError{tr("could not find '%1'").arg(referenceName)}; } QString loadErrorString; QTextStream localErrorStream{&loadErrorString}; const std::optional<ModelId> modelIdOpt = this->openModel( referencedFilePath, localErrorStream, OpenType::AutomaticallyOpened); if (not modelIdOpt.has_value()) { const QString& errorMessage = tr("could not load '%1': %2") .arg(referencedFilePath) .arg(loadErrorString); throw LoadingError{errorMessage}; } modelInfo.dependencies[referenceName] = modelIdOpt.value(); if (not bag.processed.contains(modelIdOpt.value())) { this->loadDependenciesForModel(modelIdOpt.value(), referencedFilePath, bag); } } catch(const LoadingError& error) { bag.errorStream << error.message << "\n"; failedToOpen.insert(referenceName); bag.missing.append(referenceName); } } }); }