src/documentmanager.cpp

Wed, 19 Apr 2023 22:51:56 +0300

author
Teemu Piippo <teemu.s.piippo@gmail.com>
date
Wed, 19 Apr 2023 22:51:56 +0300
changeset 381
80bea7a6e84f
parent 380
16f6717a218b
permissions
-rw-r--r--

Made library_role_e an enum class

/*
 *  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 <QPlainTextDocumentLayout>
#include <deque>
#include "src/documentmanager.h"
#include "src/parser.h"

DocumentManager::DocumentManager(QObject *parent) :
	QObject{parent}
{
}

static std::unique_ptr<QTextDocument> newTextDocument()
{
	std::unique_ptr<QTextDocument> newModel = std::make_unique<QTextDocument>(nullptr);
	newModel->setDocumentLayout(new QPlainTextDocumentLayout{newModel.get()});
	return newModel;
}

/**
 * @brief Creates a new model.
 * @returns the ID of the new model
 */
ModelId DocumentManager::newModel()
{
	const ModelId modelId{++this->modelIdCounter};
	this->openModels.emplace(std::make_pair(modelId, ModelInfo{
		.model = newTextDocument(),
		.id = modelId,
		.opentype = OpenType::ManuallyOpened,
	}));
	this->makePolygonCacheForModel(modelId);
	Q_EMIT this->message(logInfo(tr("New model %1 created").arg(modelId.value)));
	return modelId;
}

QTextDocument* 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
 */
QTextDocument *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(QFileInfo{path});
	file.open(QFile::ReadOnly | QFile::Text);
	std::unique_ptr<QTextDocument> newModel = newTextDocument();
	newModel->setPlainText(file.readAll());
	std::optional<ModelId> result;
	if (file.error() == QFile::NoError)
	{
		const ModelId modelId{++this->modelIdCounter};
		this->openModels.emplace(std::make_pair(modelId, ModelInfo{
			.model = std::move(newModel),
			.id = modelId,
			.path = path,
			.opentype = openType,
			.polygonCache = {},
		}));
		this->makePolygonCacheForModel(modelId);
		result = modelId;
		Q_EMIT this->message(logInfo(tr("Opened %1 as model %2").arg(quoted(path)).arg(modelId.value)));
	}
	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 LibrariesModel &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)) {
			file.write(info->model->toPlainText().replace("\n", "\r\n").toUtf8());
			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 QTextDocument *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::find(ModelId modelId) const
{
	return findInMap(this->openModels, modelId);
}

void DocumentManager::setModelPayload(ModelId modelId, QObject *object)
{
	ModelInfo* info = findInMap(this->openModels, modelId);
	if (info != nullptr) {
		info->payload = object;
		object->setParent(this);
	}
}

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);
}

template<typename T, typename K>
void removeFromSet(std::set<T>& set, K&& valueToRemove)
{
	const auto it = std::lower_bound(set.begin(), set.end(), valueToRemove);
	if (it != set.end() and *it == valueToRemove) {
		set.erase(it);
	}
}

//! @brief Cleans up and erases models that are no longer required.
void DocumentManager::prune()
{
	Graph<ModelId> dependencyGraph;
	forValueInMap(this->openModels, [&dependencyGraph](const ModelInfo& info) {
		forValueInMap(info.dependencies, [&dependencyGraph, &info](ModelId dep){
			dependencyGraph.push_back({.from = info.id, .to = dep});
		});
	});
	std::set<ModelId> autoOpened;
	forValueInMap(this->openModels, [&autoOpened](const ModelInfo& info) {
		if (info.opentype == OpenType::AutomaticallyOpened) {
			autoOpened.insert(info.id);
		}
	});
	bool repeat = true;
	while (repeat) {
		repeat = false;
		std::set<ModelId> prunable = autoOpened;
		for (const auto& pair : dependencyGraph) {
			removeFromSet(prunable, pair.to);
		}
		for (ModelId idToPrune : prunable) {
			auto it = this->openModels.find(idToPrune);
			if (it != this->openModels.end()) {
				Q_EMIT this->message(logInfo(tr("Model %1 (%2) pruned").arg(idToPrune.value).arg(it->second.path)));
				this->openModels.erase(it);
			}
			removeFromSet(autoOpened, idToPrune);
			std::erase_if(dependencyGraph, [&idToPrune](const GraphEdge<ModelId>& edge) {
				return edge.from == idToPrune;
			});
			repeat = true;
		}
	}
}

void DocumentManager::makePolygonCacheForModel(const ModelId modelId)
{
	QTextDocument* 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, &QTextDocument::contentsChanged, modelModified);
	}
}

static QFileInfo findFile(
	QString referenceName,
	const QString& modelPath,
	const LibrariesModel& libraries)
{
	// Try to find the file in the same place as the model itself
	referenceName.replace("\\", "/");
	const QDir dir = QFileInfo{modelPath}.dir();
	QFileInfo referencedFilePath = {dir.filePath(referenceName)};
	if (not referencedFilePath.exists())
	{
		// Look for it in the libraries
		referencedFilePath = libraries.findFile(referenceName);
	}
	return referencedFilePath;
}

static std::set<QString> referenceNames(const QTextDocument* model)
{
	std::set<QString> result;
	for (const QString& line : model->toPlainText().split("\n")) {
		const opt<ParsedLine> parsed = parse(line);
		if (parsed.has_value() and std::holds_alternative<LineType1>(*parsed)) {
			result.insert(std::get<LineType1>(*parsed).value.element.name);
		}
	}
	return result;
}

struct Dependency
{
	QString name;
	QFileInfo path;
	bool operator<(const Dependency& other) const
	{
		if (this->name != other.name) {
			return this->name < other.name;
		}
		else {
			return this->path.absoluteFilePath() < other.path.absoluteFilePath();
		}
	}
};

static std::set<Dependency> resolveReferencePaths(
	const DocumentManager::ModelInfo* modelInfo,
	const LibrariesModel* libraries)
{
	std::set<Dependency> result;
	const std::set<QString> refNames = referenceNames(modelInfo->model.get());
	if (modelInfo != nullptr) {
		for (const QString& name : refNames) {
			const QFileInfo path = findFile(name, modelInfo->path, *libraries);
			if (path.exists())
			{
				result.insert(Dependency{.name = name, .path = path});
			}
		}
	}
	return result;
}

static void loadDependenciesForModel(
	DocumentManager::ModelInfo* info,
	DocumentManager* documents,
	const LibrariesModel* libraries,
	std::map<QString, QString>& missing)
{
	bool repeat = true;
	const auto olddeps = info->dependencies;
	info->dependencies.clear();
	while (repeat) {
		repeat = false;
		const std::set<Dependency> dependencies = resolveReferencePaths(info, libraries);
		for (const Dependency& dep : dependencies) {
			const ModelId* const idp = findInMap(olddeps, dep.name);
			if (idp != nullptr) {
				info->dependencies[dep.name] = *idp;
			}
			else if (not info->dependencies.contains(dep.name) and not missing.contains(dep.path.absoluteFilePath())) {
				QString loadErrorString;
				QTextStream localErrorStream{&loadErrorString};
				const std::optional<ModelId> modelIdOpt = documents->openModel(
					dep.path.absoluteFilePath(),
					localErrorStream,
					OpenType::AutomaticallyOpened);
				if (not modelIdOpt.has_value()) {
					const QString& errorMessage = QObject::tr("could not load '%1': %2")
					.arg(dep.path.absoluteFilePath(), loadErrorString);
					missing[dep.path.absoluteFilePath()] = errorMessage;
				}
				else {
					info->dependencies[dep.name] = modelIdOpt.value();
					repeat = true;
				}
			}
		}
	}
}

std::map<QString, QString> DocumentManager::loadDependenciesForAllModels(const LibrariesModel& libraries)
{
	std::map<QString, QString> missing;
	for (auto& modelInfoPair : this->openModels)
	{
		loadDependenciesForModel(&modelInfoPair.second, this, &libraries, missing);
	}
	this->prune();
	return missing;
}

mercurial