src/documentmanager.cpp

changeset 212
27259810da6d
parent 206
654661eab7f3
child 213
ee5758ddb6d2
--- a/src/documentmanager.cpp	Thu Jun 09 19:11:27 2022 +0300
+++ b/src/documentmanager.cpp	Sat Jun 11 14:30:30 2022 +0300
@@ -23,12 +23,7 @@
 #include "documentmanager.h"
 #include "parser.h"
 
-/**
- * @brief Constructs a new document manager
- * @param parent Parent object
- */
-DocumentManager::DocumentManager(QObject* parent) :
-	QObject{parent}
+DocumentManager::DocumentManager()
 {
 }
 
@@ -39,40 +34,27 @@
 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->openModels[modelId].id = modelId;
+	this->openModels[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))
-	{
+	if (modelsIterator != std::end(this->openModels)) {
 		const auto& dependencies = modelsIterator->second.dependencies;
 		const auto dependenciesIterator = dependencies.find(name);
-		if (dependenciesIterator != dependencies.end())
-		{
+		if (dependenciesIterator != dependencies.end()) {
 			ModelInfo& modelInfo = this->openModels[dependenciesIterator->second];
 			return modelInfo.model.get();
 		}
-		else
-		{
+		else {
 			return nullptr;
 		}
 	}
-	else
-	{
+	else {
 		return nullptr;
 	}
 }
@@ -131,7 +113,7 @@
 	QFile file{path};
 	const QString name = pathToName(path);
 	file.open(QFile::ReadOnly | QFile::Text);
-	std::unique_ptr<Model> newModel = std::make_unique<Model>(this);
+	std::unique_ptr<Model> newModel = std::make_unique<Model>(nullptr);
 	QTextStream textStream{&file};
 	Parser parser{file};
 	parser.parseBody(*newModel);
@@ -139,7 +121,13 @@
 	if (file.error() == QFile::NoError)
 	{
 		const ModelId modelId{++this->modelIdCounter};
-		this->openModels[modelId] = {std::move(newModel), modelId, path, openType};
+		this->openModels[modelId] = {
+			.model = std::move(newModel),
+			.id = modelId,
+			.path = path,
+			.opentype = openType,
+			.polygonCache = {},
+		};
 		this->makePolygonCacheForModel(modelId);
 		result = modelId;
 	}
@@ -150,49 +138,6 @@
 	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 << tr("The following files could not be opened: %1")
-			.arg(bag.missing.join(", "));
-	}
-}
-
 void DocumentManager::closeDocument(const ModelId modelId)
 {
 	ModelInfo* modelInfo = findInMap(this->openModels, modelId);
@@ -216,63 +161,51 @@
 	}
 }
 
-/**
- * @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
- */
+//! \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)
 {
-	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);
+	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)
 {
-	const QString* const path = this->modelPath(modelId);
-	if (path != nullptr)
+	ModelInfo* info = findInMap(this->openModels, modelId);
+	if (info != nullptr)
 	{
-		QSaveFile file{*path};
+		QSaveFile file{info->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);
+		if (file.open(QSaveFile::WriteOnly)) {
+			::save(info->model.get(), &file);
 			const bool commitSucceeded = file.commit();
-			if (not commitSucceeded)
-			{
-				errors << tr("Could not save: %1").arg(file.errorString());
+			if (not commitSucceeded) {
+				errors << QObject::tr("Could not save: %1").arg(file.errorString());
 				return false;
 			}
-			else
-			{
+			else {
 				return true;
 			}
 		}
-		else
-		{
-			errors << tr("Could not open %1 for writing: %2")
+		else {
+			errors << QObject::tr("Could not open %1 for writing: %2")
 				.arg(file.fileName(), file.errorString());
 			return false;
 		}
 	}
-	else
-	{
-		errors << tr("Bad model ID %1").arg(modelId.value);
+	else {
+		errors << QObject::tr("Bad model ID %1").arg(modelId.value);
 		return false;
 	}
 }
@@ -298,17 +231,30 @@
 
 PolygonCache *DocumentManager::getPolygonCacheForModel(ModelId modelId)
 {
-	auto it = this->polygonCaches.find(modelId);
-	if (it != this->polygonCaches.end())
-	{
-		return &it->second;
+	ModelInfo* info = findInMap(this->openModels, modelId);
+	if (info != nullptr) {
+		return &info->polygonCache;
 	}
-	else
-	{
+	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.
  */
@@ -321,17 +267,12 @@
 			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.
+			// 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;
 		}
@@ -366,27 +307,29 @@
 	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);
+		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);
 	}
 }
 
-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)
+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{path}.dir();
+	const QDir dir = QFileInfo{modelPath}.dir();
 	QString referencedFilePath = dir.filePath(referenceName);
 	if (not QFileInfo{referencedFilePath}.exists())
 	{
@@ -396,72 +339,87 @@
 	return referencedFilePath;
 }
 
-template<typename T>
-void iterate(const Model& model, std::function<void(const T&)> fn)
+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)
 {
-	for (int i = 0; i < model.size(); ++i) {
-		if (std::holds_alternative<T>(model[i])) {
-			fn(std::get<T>(model[i]));
+	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;
+				}
+			}
 		}
 	}
 }
 
-void DocumentManager::loadDependenciesForModel(
-	const ModelId modelId,
-	const QString &path,
-	LoadDepedenciesBag& bag)
+std::map<QString, QString> DocumentManager::loadDependenciesForAllModels(const LibraryManager& libraries)
 {
-	QSet<QString> failedToOpen;
-	struct LoadingError
+	std::map<QString, QString> missing;
+	for (auto& modelInfoPair : this->openModels)
 	{
-		QString message;
-	};
-	bag.processed.insert(modelId);
-	if (not this->openModels.contains(modelId))
-	{
-		bag.errorStream << tr("bad model ID %1").arg(modelId.value);
-		return;
+		loadDependenciesForModel(&modelInfoPair.second, this, &libraries, missing);
 	}
-	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);
-			}
-		}
-	});
+	this->prune();
+	return missing;
 }

mercurial