diff -r 37f936073cac -r e1ced2523cad src/documentmanager.cpp --- a/src/documentmanager.cpp Sun Oct 24 11:33:32 2021 +0300 +++ b/src/documentmanager.cpp Tue Nov 02 15:43:57 2021 +0200 @@ -19,6 +19,7 @@ #include #include #include +#include #include "documentmanager.h" #include "modeleditcontext.h" #include "linetypes/comment.h" @@ -36,31 +37,62 @@ /** * @brief Creates a new model. - * @returns the name to the new model + * @returns the ID of the new model */ -QString DocumentManager::newModel() +ModelId DocumentManager::newModel() { + const ModelId modelId{++this->modelIdCounter}; const QString name = makeNewModelName(); - this->openModels.emplace(name, new Model); - return name; + this->openModels[modelId] = ModelInfo{ + .model = std::make_unique(), + .opentype = OpenType::ManuallyOpened, + }; + return modelId; } /** * @brief Looks for a model by name * @param name Name of the model * @returns model or null - * ' */ -Model* DocumentManager::findModelByName(const QString& name) +Model* DocumentManager::findDependencyByName(const ModelId modelId, const QString& name) { - const auto iterator = this->openModels.find(name); - if (iterator == std::end(this->openModels)) + 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 iterator->second.model.get(); + return nullptr; } } @@ -90,23 +122,27 @@ * @param path Path to the model to open * @param errorStream Where to write any errors * @param openType rationale behind opening this file - * @returns file name or "" on error + * @returns model id, or no value on error */ -QString DocumentManager::openModel(const QString& path, QTextStream& errorStream, const OpenType openType) -{ +std::optional 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 newModel = std::make_unique(path); + std::unique_ptr newModel = std::make_unique(this); QTextStream textStream{&file}; Model::EditContext editor = newModel->edit(); Parser parser{file}; parser.parseBody(editor); - QString result; + std::optional result; if (file.error() == QFile::NoError) { - openModels[name] = {std::move(newModel), openType}; - result = name; + const ModelId modelId{++this->modelIdCounter}; + this->openModels[modelId] = {std::move(newModel), path, openType}; + result = modelId; } else { @@ -121,59 +157,192 @@ 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 processed; + QTextStream& errorStream; +}; + void DocumentManager::loadDependenciesForModel( - const QString& modelName, + const ModelId modelId, const QString& path, const LibraryManager& libraries, QTextStream& errorStream) { - QStringList missing; - QStringList processed; - loadDependenciesForModel(modelName, path, libraries, missing, processed, errorStream); - if (not missing.empty()) + LoadDepedenciesBag bag { + .libraries = libraries, + .missing = {}, + .processed = {}, + .errorStream = errorStream, + }; + this->loadDependenciesForModel(modelId, path, bag); + if (not bag.missing.empty()) { - missing.sort(Qt::CaseInsensitive); + bag.missing.sort(Qt::CaseInsensitive); errorStream << utility::format( "The following files could not be opened: %1", - missing.join(", ")); + 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); } } -void DocumentManager::closeDocument(const QString &name) +bool DocumentManager::saveModel(const ModelId modelId, QTextStream &errors) { - const auto& it = this->openModels.find(name); - if (it != this->openModels.end()) + const QString* const path = this->modelPath(modelId); + if (path != nullptr) { - this->openModels.erase(it); - } - QSet referenced; - for (const auto& it : this->openModels) - { - if (it.second.opentype == OpenType::ManuallyOpened) + QSaveFile file{*path}; + file.setDirectWriteFallback(true); + if (file.open(QSaveFile::WriteOnly)) { - this->collectReferences(referenced, it.first, it.second.model.get()); + // if path is not nullptr, getModelById will always return a value as well + this->getModelById(modelId)->save(&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; + } } -void DocumentManager::collectReferences(QSet& referenced, const QString &name, const Model *model) +/** + * @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 DocumentManager::findIdForModel(const Model *model) const { - if (not referenced.contains(name)) + std::optional result; + for (auto it = this->openModels.begin(); it != this->openModels.end(); ++it) { - referenced.insert(name); - model->apply([&](const ldraw::SubfileReference* referenceObject) + if (it->second.model.get() == model) { - const ldraw::id_t id = referenceObject->id; - const QString& referenceName = model->getObjectProperty(id); - auto it = this->openModels.find(referenceName); - if (it != this->openModels.end()) + result = it->first; + break; + } + } + return result; +} + +/** + * @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 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) { - const Model* const model = it->second.model.get(); - this->collectReferences(referenced, referenceName, model); + if (dependencyPair.second == modelId) + { + return true; + } } - }); + } } + return false; } static QString findFile(QString referenceName, const QString& path, const LibraryManager& libraries) @@ -191,56 +360,61 @@ } void DocumentManager::loadDependenciesForModel( - const QString& modelName, + const ModelId modelId, const QString &path, - const LibraryManager& libraries, - QStringList& missing, - QStringList& processed, - QTextStream& errorStream) + LoadDepedenciesBag& bag) { + QSet failedToOpen; struct LoadingError { QString message; }; - processed.append(modelName); - Model* model = this->findModelByName(modelName); - for (int i = 0; i < model->size(); i += 1) + bag.processed.insert(modelId); + if (not this->openModels.contains(modelId)) { - const QString referenceName = model->getObjectProperty(i, ldraw::Property::ReferenceName).toString(); + bag.errorStream << tr("bad model ID %1").arg(modelId.value); + return; + } + ModelInfo& modelInfo = this->openModels[modelId]; + modelInfo.dependencies.clear(); + for (int i = 0; i < modelInfo.model->size(); i += 1) + { + const QString referenceName = modelInfo.model->getObjectProperty(i, ldraw::Property::ReferenceName).toString(); if (not referenceName.isEmpty() - and openModels.find(referenceName) == std::end(openModels) - and not missing.contains(referenceName)) + and modelInfo.dependencies.count(referenceName) == 0 + and not failedToOpen.contains(referenceName)) { try { - const QString referencedFilePath = findFile(referenceName, path, libraries); + const QString referencedFilePath = ::findFile(referenceName, path, bag.libraries); if (referencedFilePath.isEmpty()) { - throw LoadingError{utility::format("'%1' was not found.", referenceName)}; + throw LoadingError{tr("could not find '%1'").arg(referenceName)}; } - QString errorString; - QTextStream localErrorStream{&errorString}; - QString resultName = this->openModel( + QString loadErrorString; + QTextStream localErrorStream{&loadErrorString}; + const std::optional modelIdOpt = this->openModel( referencedFilePath, localErrorStream, OpenType::AutomaticallyOpened); - if (resultName.isEmpty()) + if (not modelIdOpt.has_value()) { - throw LoadingError{utility::format( - "could not load '%1': %2", - referencedFilePath, - errorString)}; + const QString& errorMessage = tr("could not load '%1': %2") + .arg(referencedFilePath) + .arg(loadErrorString); + throw LoadingError{errorMessage}; } - if (not processed.contains(referenceName)) + modelInfo.dependencies[referenceName] = modelIdOpt.value(); + if (not bag.processed.contains(modelIdOpt.value())) { - loadDependenciesForModel(referenceName, path, libraries, missing, processed, errorStream); + this->loadDependenciesForModel(modelIdOpt.value(), referencedFilePath, bag); } } catch(const LoadingError& error) { - errorStream << error.message << "\n"; - missing.append(referenceName); - processed.append(referenceName); + bag.errorStream << error.message << "\n"; + failedToOpen.insert(referenceName); + bag.missing.append(referenceName); } } }