src/main.cpp

Tue, 07 Jun 2022 01:37:26 +0300

author
Teemu Piippo <teemu@hecknology.net>
date
Tue, 07 Jun 2022 01:37:26 +0300
changeset 201
5d201ee4a9c3
parent 200
ca23936b455b
child 202
b05af0bab735
permissions
-rw-r--r--

Continue giant refactor

#include <QApplication>
#include <QFileDialog>
#include <QMessageBox>
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "version.h"
#include "document.h"
#include "settingseditor/settingseditor.h"
#include "widgets/colorselectdialog.h"

static const QDir LOCALE_DIR {":/locale"};

static void doQtRegistrations()
{
	QCoreApplication::setApplicationName(::appName);
	QCoreApplication::setOrganizationName("hecknology.net");
	QCoreApplication::setOrganizationDomain("hecknology.net");
	qRegisterMetaTypeStreamOperators<Library>("Library");
	qRegisterMetaTypeStreamOperators<Libraries>("Libraries");
}

template<typename BaseType, typename MemberType, typename DataType>
struct MemberData
{
	std::size_t member;
	DataType payload;
	constexpr MemberType memberInstance(BaseType* instance) const
	{
		return *reinterpret_cast<MemberType*>(reinterpret_cast<char*>(instance) + this->member);
	}
};

static constexpr MemberData<Ui_MainWindow, QAction*, gl::RenderStyle> renderStyleButtons[] = {
	{ offsetof(Ui_MainWindow, actionRenderStyleNormal), gl::RenderStyle::Normal },
	{ offsetof(Ui_MainWindow, actionRenderStyleBfc), gl::RenderStyle::BfcRedGreen },
	{ offsetof(Ui_MainWindow, actionRenderStyleRandom), gl::RenderStyle::RandomColors },
	{ offsetof(Ui_MainWindow, actionRenderStylePickScene), gl::RenderStyle::PickScene },
};

static std::optional<ModelId> openModelFromPath(
	const QString& path,
	const LibraryManager* libraries,
	DocumentManager* documents,
	QWidget* parent)
{
	QString errorString;
	QTextStream errorStream{&errorString};
	const std::optional<ModelId> modelIdOpt = documents->openModel(
		path,
		errorStream,
		DocumentManager::OpenType::ManuallyOpened);
	if (modelIdOpt.has_value()) {
		documents->loadDependenciesForModel(modelIdOpt.value(), path, *libraries, errorStream);
		if (not errorString.isEmpty()) {
			QMessageBox::warning(
				parent,
				QObject::tr("Problem loading references"),
				errorString);
		}
	}
	else {
		QMessageBox::critical(
			parent,
			QObject::tr("Problem opening file"),
			format(QObject::tr("Could not open %1: %2"), path, errorString));
	}
	return modelIdOpt;
}

static QString getOpenModelPath(QWidget* parent)
{
	return QFileDialog::getOpenFileName(
		parent,
		QObject::tr("Open model"),
		"",
		QObject::tr("LDraw models (*.ldr *.dat)"));
}

static const QString localeCode(const QString& locale)
{
	if (locale == "system") {
		return QLocale::system().name();
	}
	else {
		return locale;
	}
}

/**
 * @brief Changes the application language to the specified language
 */
static void changeLanguage(const QString& locale, QTranslator* translator)
{
	if (not locale.isEmpty()) {
		const QString localeCode = ::localeCode(locale);
		QLocale::setDefault({localeCode});
		qApp->removeTranslator(translator);
		const QString path = LOCALE_DIR.filePath(localeCode + ".qm");
		const bool loadSuccessful = translator->load(path);
		if (loadSuccessful)
		{
			qApp->installTranslator(translator);
		}
	}
}

/*
void MainWindow::handleDocumentSplitterChange()
{
	EditorTabWidget* currentDocument = this->currentDocument();
	if (currentDocument != nullptr)
	{
		this->documentSplitterState = currentDocument->saveSplitterState();
		for (int i = 0; i < this->ui->tabs->count(); i += 1)
		{
			EditorTabWidget* document = qobject_cast<EditorTabWidget*>(this->ui->tabs->widget(i));
			if (document != nullptr and document != currentDocument)
			{
				document->restoreSplitterState(this->documentSplitterState);
			}
		}
		this->settings.setMainSplitterState(this->documentSplitterState);
	}
}
*/

static EditorTabWidget* currentTabWidget(Ui_MainWindow* ui)
{
	return qobject_cast<EditorTabWidget*>(ui->tabs->currentWidget());
};


static void closeDocument(DocumentManager* documents, EditorTabWidget *document)
{
	std::optional<ModelId> modelId = documents->findIdForModel(document->model);
	if (modelId.has_value()) {
		documents->closeDocument(modelId.value());
		delete document;
	}
}

static void handleTabCloseButton(Ui_MainWindow* ui, DocumentManager* documents, int tabIndex)
{
	if (tabIndex >= 0 and tabIndex < ui->tabs->count()) {
		EditorTabWidget* tab = qobject_cast<EditorTabWidget*>(ui->tabs->widget(tabIndex));
		if (tab != nullptr) {
			closeDocument(documents, tab);
		}
	}
}

static std::optional<ModelId> findCurrentModelId(Ui_MainWindow* ui, DocumentManager* documents)
{
	const EditorTabWidget* tab = currentTabWidget(ui);
	if (tab != nullptr) {
		return documents->findIdForModel(tab->model);
	}
	else {
		return {};
	}
}

/**
 * @brief Updates the title of the main window so to contain the app's name
 * and version as well as the open document name.
 */
static QString title()
{
	QString title = ::appName;
	title += " ";
	title += fullVersionString();
	return title;
}

static ldraw::ColorTable loadColors(const LibraryManager* libraries)
{
	QTextStream errors;
	return libraries->loadColorTable(errors);
}

static QString tabName(const QFileInfo& fileInfo)
{
	QString result = fileInfo.baseName();
	if (result.isEmpty()) {
		result = QObject::tr("<unnamed>");
	}
	return result;
}

void rebuildRecentFilesMenu(QMenu* menu, const QStringList& strings, QWidget* parent)
{
	menu->clear();
	for (const QString& path : strings) {
		QAction* action = new QAction{path, parent};
		action->setData(path);
		menu->addAction(action);
	}
}

static void updateRenderPreferences(
	Ui_MainWindow* ui,
	const gl::RenderPreferences* renderPreferences)
{
	for (int i = 0; i < ui->tabs->count(); i += 1) {
		EditorTabWidget* tab = qobject_cast<EditorTabWidget*>(ui->tabs->widget(i));
		if (tab != nullptr) {
			tab->canvas->setRenderPreferences(*renderPreferences);
		}
	}
	for (auto data : ::renderStyleButtons) {
		QAction* action = data.memberInstance(ui);
		action->setChecked(renderPreferences->style == data.payload);
	}
	ui->actionDrawAxes->setChecked(renderPreferences->drawAxes);
};

static gl::RenderPreferences loadRenderPreferences(Configuration* settings)
{
	return gl::RenderPreferences{
		.style = static_cast<gl::RenderStyle>(settings->renderStyle()),
		.mainColor = settings->mainColor(),
		.backgroundColor = settings->backgroundColor(),
		.selectedColor = settings->selectedColor(),
		.lineThickness = settings->lineThickness(),
		.lineAntiAliasing = settings->lineAntiAliasing(),
		.drawAxes = settings->drawAxes(),
	};
}

int main(int argc, char *argv[])
{
	doQtRegistrations();
	QApplication app{argc, argv};
	QMainWindow mainWindow;
	Ui_MainWindow ui;
	DocumentManager documents{&mainWindow};
	QString currentLanguage = "en";
	QTranslator translator{&mainWindow};
	Configuration settings;
	LibraryManager libraries{&mainWindow};
	QByteArray documentSplitterState;
	QStringList recentlyOpenedFiles;
	ldraw::ColorTable colorTable;
	gl::RenderPreferences renderPreferences;
	ui.setupUi(&mainWindow);
	const uiutilities::KeySequenceMap defaultKeyboardShortcuts =
		uiutilities::makeKeySequenceMap(uiutilities::collectActions(&mainWindow));
	const auto saveSettings = [&]{
		settings.setMainWindowGeometry(mainWindow.saveGeometry());
		settings.setRecentFiles(recentlyOpenedFiles);
		settings.setMainSplitterState(documentSplitterState);
		settings.setRenderStyle(static_cast<int>(renderPreferences.style));
		settings.setDrawAxes(renderPreferences.drawAxes);
		libraries.storeToSettings(&settings);
	};
	const auto updateRecentlyOpenedDocumentsMenu = [&]{
		rebuildRecentFilesMenu(ui.menuRecentFiles, recentlyOpenedFiles, &mainWindow);
		for (QAction* action : ui.menuRecentFiles->findChildren<QAction*>()) {
			QString path = action->data().toString();
			QObject::connect(
				action,
				&QAction::triggered,
				[path, &libraries, &documents, &mainWindow]() {
					openModelFromPath(path, &libraries, &documents, &mainWindow);
				}
			);
		}
	};
	const auto restoreSettings = [&]{
		recentlyOpenedFiles = settings.recentFiles();
		documentSplitterState = settings.mainSplitterState();
		renderPreferences = loadRenderPreferences(&settings);
		changeLanguage(settings.locale(), &translator);
		libraries.restoreFromSettings(&settings);
		updateRecentlyOpenedDocumentsMenu();
		colorTable = loadColors(&libraries);
		updateRenderPreferences(&ui, &renderPreferences);
		ui.retranslateUi(&mainWindow);
	};
	const auto addRecentlyOpenedFile = [&](const QString& path){
		constexpr int maxRecentlyOpenedFiles = 10;
		recentlyOpenedFiles.removeAll(path);
		recentlyOpenedFiles.insert(0, path);
		while (recentlyOpenedFiles.size() > maxRecentlyOpenedFiles)
		{
			recentlyOpenedFiles.removeLast();
		}
		saveSettings();
		updateRecentlyOpenedDocumentsMenu();
	};
	const auto openModelForEditing = [&](const ModelId modelId){
		EditorTabWidget* document = new EditorTabWidget{
			documents.getModelById(modelId),
			&documents,
			colorTable,
		};
		document->canvas->setRenderPreferences(renderPreferences);
		QObject::connect(
			document,
			&EditorTabWidget::newStatusText,
			[&](const QString& newStatusText) {
				mainWindow.statusBar()->showMessage(newStatusText);
			});
		const QFileInfo fileInfo{*documents.modelPath(modelId)};
		ui.tabs->addTab(document, tabName(fileInfo));
		ui.tabs->setCurrentWidget(document);
		document->restoreSplitterState(documentSplitterState);
	};
	const auto newModel = [&openModelForEditing](DocumentManager* documents){
		openModelForEditing(documents->newModel());
	};
	QObject::connect(ui.actionNew, &QAction::triggered, [&newModel, &documents]{
		newModel(&documents);
	});
	QObject::connect(ui.actionOpen, &QAction::triggered, [&]{
		const QString path = getOpenModelPath(&mainWindow);
		if (not path.isEmpty())
		{
			const std::optional<ModelId> id = openModelFromPath(path, &libraries, &documents, &mainWindow);
			if (id.has_value()) {
				openModelForEditing(id.value());
				addRecentlyOpenedFile(path);
			}
		}
	});
	QObject::connect(ui.actionSettingsEditor, &QAction::triggered, [&]{
		SettingsEditor settingsEditor{&settings, defaultKeyboardShortcuts, &mainWindow};
		const int result = settingsEditor.exec();
		if (result == QDialog::Accepted)
		{
			restoreSettings();
		}
	});
	QObject::connect(ui.actionQuit, &QAction::triggered, &mainWindow, &QMainWindow::close);
	QObject::connect(ui.actionAdjustGridToView, &QAction::triggered, [&ui]{
		EditorTabWidget* tab = currentTabWidget(&ui);
		if (tab != nullptr)
		{
			adjustGridToView(tab->canvas);
		}
	});
	QObject::connect(ui.actionClose, &QAction::triggered, [&ui, &documents]{
		EditorTabWidget* tab = currentTabWidget(&ui);
		if (tab != nullptr)
		{
			closeDocument(&documents, tab);
		}
	});
	const auto save = [&](ModelId modelId){
		QString error;
		QTextStream errorStream{&error};
		const bool succeeded = documents.saveModel(modelId, errorStream);
		if (not succeeded)
		{
			QMessageBox::critical(&mainWindow, QObject::tr("Save error"), error);
		}
		else
		{
			const QString* pathPtr = documents.modelPath(modelId);
			if (pathPtr != nullptr) {
				addRecentlyOpenedFile(*pathPtr);
			}
		}
	};
	const auto actionSaveAs = [&]{
		const std::optional<ModelId> modelId = findCurrentModelId(&ui, &documents);
		if (modelId.has_value())
		{
			const QString* pathPtr = documents.modelPath(*modelId);
			QString defaultPath = (pathPtr != nullptr) ? *pathPtr : "";
			const QString newPath = QFileDialog::getSaveFileName(
				&mainWindow,
				QObject::tr("Save as…"),
				QFileInfo{defaultPath}.absoluteDir().path(), 
				QObject::tr("LDraw files (*.ldr *dat);;All files (*)")
			);
			if (not newPath.isEmpty()) {
				QString error;
				QTextStream errorStream{&error};
				documents.setModelPath(*modelId, newPath, libraries, errorStream);
				ui.tabs->setTabText(ui.tabs->currentIndex(), QFileInfo{newPath}.fileName());
				save(*modelId);
			}
		}
	};
	QObject::connect(ui.actionSaveAs, &QAction::triggered, actionSaveAs);
	QObject::connect(ui.actionSave, &QAction::triggered, [&]{
		if (currentTabWidget(&ui) != nullptr) {
			const std::optional<ModelId> modelId = findCurrentModelId(&ui, &documents);
			if (modelId.has_value()) {
				const QString* path = documents.modelPath(*modelId);
				if (path == nullptr or path->isEmpty()) {
					actionSaveAs();
				}
				else {
					save(*modelId);
				}
			}
		}
	});
	QObject::connect(ui.tabs, &QTabWidget::tabCloseRequested, [&](int index){
		handleTabCloseButton(&ui, &documents, index);
	});
	QObject::connect(ui.actionDrawAxes, &QAction::triggered, [&](bool drawAxes){
		renderPreferences.drawAxes = drawAxes;
		saveSettings();
		updateRenderPreferences(&ui, &renderPreferences);
	});
	for (auto data : ::renderStyleButtons) {
		QAction* action = data.memberInstance(&ui);
		QObject::connect(action, &QAction::triggered, [&, data]{
			renderPreferences.style = data.payload;
			saveSettings();
			updateRenderPreferences(&ui, &renderPreferences);
		});
	}
	mainWindow.setWindowTitle(title());
	mainWindow.restoreGeometry(settings.mainWindowGeometry());
	restoreSettings();
	updateRenderPreferences(&ui, &renderPreferences);
	newModel(&documents);
	mainWindow.show();
	const int result = app.exec();
	saveSettings();
	return result;
}

mercurial