src/main.cpp

changeset 201
5d201ee4a9c3
parent 200
ca23936b455b
child 202
b05af0bab735
--- a/src/main.cpp	Mon Jun 06 22:01:22 2022 +0300
+++ b/src/main.cpp	Tue Jun 07 01:37:26 2022 +0300
@@ -1,24 +1,14 @@
-/*
- *  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 <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"
 
-#include <QApplication>
-#include "mainwindow.h"
-#include "version.h"
+static const QDir LOCALE_DIR {":/locale"};
 
 static void doQtRegistrations()
 {
@@ -29,11 +19,408 @@
 	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};
-	MainWindow mainwindow;
-	mainwindow.show();
-	return app.exec();
+	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