src/main.cpp

Wed, 29 Jun 2022 14:43:42 +0300

author
Teemu Piippo <teemu.s.piippo@gmail.com>
date
Wed, 29 Jun 2022 14:43:42 +0300
changeset 306
6ad27b7d2697
parent 299
cf9a854b56a9
child 309
d862721d19a3
permissions
-rw-r--r--

Enable drawing clockwise shapes despite mapbox::earcut rewinding them

#include <QApplication>
#include <QCloseEvent>
#include <QFileDialog>
#include <QMdiSubWindow>
#include <QMessageBox>
#include <QScrollBar>
#include <QStackedWidget>
#include <QTranslator>
#include <ui_about.h>
#include <ui_mainwindow.h>
#include "src/gl/partrenderer.h"
#include "src/layers/axeslayer.h"
#include "src/layers/edittools.h"
#include "src/layers/gridlayer.h"
#include "src/ldrawalgorithm.h"
#include "src/messagelog.h"
#include "src/settings.h"
#include "src/settingseditor/settingseditor.h"
#include "src/ui/circletooloptionswidget.h"
#include "src/ui/objecteditor.h"
#include "src/version.h"
#include "src/widgets/colorselectdialog.h"
#include <GL/glew.h>

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

class ModelSubWindow : public QMdiSubWindow
{
	Q_OBJECT
public:
	const ModelId modelId;
	explicit ModelSubWindow(ModelId modelId, QWidget* widget = nullptr) :
		QMdiSubWindow{widget},
		modelId{modelId}
	{
	}
protected:
	void closeEvent(QCloseEvent* event) override
	{
		event->ignore();
	}
};

class ModelData : public QObject
{
	Q_OBJECT
public:
	ModelData(QObject* parent) : QObject {parent} {}
	std::unique_ptr<PartRenderer> canvas;
	std::unique_ptr<QItemSelectionModel> itemSelectionModel;
	std::unique_ptr<EditTools> tools;
	std::unique_ptr<AxesLayer> axesLayer;
	std::unique_ptr<GridLayer> gridLayer;
	Model* model;
};

class Signal final : public QObject
{
	Q_OBJECT
public:
	Signal() : QObject{}{}
	virtual ~Signal(){}
	void emit()
	{
		Q_EMIT this->triggered();
	}
Q_SIGNALS:
	void triggered();
};
#include <main.moc>

static void doQtRegistrations()
{
	QCoreApplication::setApplicationName(QStringLiteral(CMAKE_PROJECT_NAME));
	QCoreApplication::setOrganizationName("hecknology.net");
	QCoreApplication::setOrganizationDomain("hecknology.net");
	qRegisterMetaType<Message>();
	qRegisterMetaType<Library>();
	qRegisterMetaType<QList<Library>>();
	qRegisterMetaType<QMdiArea::ViewMode>();
	qRegisterMetaType<gl::RenderStyle>();
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
	qRegisterMetaTypeStreamOperators<Library>("Library");
	qRegisterMetaTypeStreamOperators<Libraries>("Libraries");
	qRegisterMetaTypeStreamOperators<gl::RenderStyle>();
	qRegisterMetaTypeStreamOperators<QMdiArea::ViewMode>();
#endif
}

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 LibrariesModel* libraries,
	DocumentManager* documents,
	QWidget* parent)
{
	QString errorString;
	QTextStream errorStream{&errorString};
	const std::optional<ModelId> modelIdOpt = documents->openModel(
		path,
		errorStream,
		OpenType::ManuallyOpened);
	if (modelIdOpt.has_value()) {
		const DocumentManager::MissingDependencies missing = documents->loadDependenciesForAllModels(*libraries);
		if (not missing.empty()) {
			QMessageBox::warning(
				parent,
				QObject::tr("Problem loading references"),
				errorStringFromMissingDependencies(missing));
		}
	}
	else {
		QMessageBox::critical(
			parent,
			QObject::tr("Problem opening file"),
			QObject::tr("Could not open %1: %2").arg(quoted(path), errorString)
		);
	}
	return modelIdOpt;
}

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

static ModelData* findModelData(const DocumentManager* documents, ModelId modelId)
{
	return documents->findPayload<ModelData>(modelId);
}

static ModelSubWindow* currentModelSubWindow(Ui_MainWindow* ui)
{
	auto* w = ui->mdiArea->activeSubWindow();
	return qobject_cast<ModelSubWindow*>(w);
}

static ModelData* currentModelData(Ui_MainWindow* ui, const DocumentManager* documents)
{
	if (auto* const activeSubWindow = currentModelSubWindow(ui)) {
		return findModelData(documents, activeSubWindow->modelId);
	}
	else {
		return nullptr;
	}
}

static Model* currentModelBody(Ui_MainWindow* ui, DocumentManager* documents)
{
	if (auto* const activeSubWindow = currentModelSubWindow(ui)) {
		return documents->getModelById(activeSubWindow->modelId);
	}
	else {
		return nullptr;
	}
}

static std::optional<ModelId> findCurrentModelId(Ui_MainWindow* ui)
{
	ModelSubWindow* activeSubWindow = qobject_cast<ModelSubWindow*>(ui->mdiArea->activeSubWindow());
	if (activeSubWindow != nullptr) {
		return activeSubWindow->modelId;
	}
	else {
		return {};
	}
}

static QString title(Ui_MainWindow* ui)
{
	QMdiSubWindow* subWindow = ui->mdiArea->activeSubWindow();
	QString titlestring;
	const QString versionString = fullVersionString(QLocale::ShortFormat);
	if (subWindow != nullptr) {
		titlestring = QObject::tr("%1 - %2").arg(subWindow->windowTitle(), versionString);
	}
	else {
		titlestring = versionString;
	}
	if (/* DISABLES CODE */ (true)
		and std::strcmp(CMAKE_BUILD_TYPE, "Release") != 0
		and std::strcmp(CMAKE_BUILD_TYPE, "MinSizeRel") != 0
	) {
		titlestring += QObject::tr(" [%1]").arg(CMAKE_BUILD_TYPE);
	}
	return titlestring;
}

static ColorTable loadColors(const LibrariesModel* 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);
	}
}

template<typename Fn>
static void forModel(const DocumentManager* documents, Fn&& fn)
{
	forValueInMap(*documents, [&fn](const DocumentManager::ModelInfo& info)
	{
		ModelData* modelSpecificData = qobject_cast<ModelData*>(info.payload);
		if (modelSpecificData != nullptr) {
			fn(&info, modelSpecificData);
		}
	});
}

static void updateRenderPreferences(
	Ui_MainWindow* ui,
	const gl::RenderPreferences* renderPreferences,
	const DocumentManager* documents)
{
	forModel(documents, [&renderPreferences](const void*, const ModelData* data){
		if (data->canvas != nullptr) {
			data->canvas->setRenderPreferences(*renderPreferences);
			data->canvas->setLayerEnabled(data->axesLayer.get(), renderPreferences->drawAxes);
		}
	});
	for (auto data : ::renderStyleButtons) {
		QAction* action = data.memberInstance(ui);
		action->setChecked(renderPreferences->style == data.payload);
	}
	ui->actionDrawAxes->setChecked(renderPreferences->drawAxes);
	ui->actionWireframe->setChecked(renderPreferences->wireframe);
}

static gl::RenderPreferences loadRenderPreferences()
{
	return gl::RenderPreferences{
		.style = setting<Setting::RenderStyle>(),
		.mainColor = setting<Setting::MainColor>(),
		.backgroundColor = setting<Setting::BackgroundColor>(),
		.selectedColor = setting<Setting::SelectedColor>(),
		.lineThickness = setting<Setting::LineThickness>(),
		.lineAntiAliasing = setting<Setting::LineAntiAliasing>(),
		.drawAxes = setting<Setting::DrawAxes>(),
		.wireframe = setting<Setting::Wireframe>(),
	};
}

struct ToolWidgets
{
	CircleToolOptionsWidget* circleToolOptions;
	ObjectEditor* objectEditor;
};

void initializeTools(Ui_MainWindow* ui, ToolWidgets* toolWidgets, QWidget* parent)
{
	const struct
	{
		QString name, tooltip;
		QPixmap icon;
		QWidget* widget;
	} editingModesInfo[] = {
		{
			.name = QObject::tr("Select"),
			.tooltip = QObject::tr("Select elements from the model."),
			.icon = {":/icons/navigate-outline.png"},
			.widget = toolWidgets->objectEditor,
		},
		{
			.name = QObject::tr("Draw"),
			.tooltip = QObject::tr("Draw new elements into the model."),
			.icon = {":/icons/pencil-outline.png"},
			.widget = nullptr,
		},
		{
			.name = QObject::tr("Circle"),
			.tooltip = QObject::tr("Draw circular primitives."),
			.icon = {":/icons/linetype-circularprimitive.png"},
			.widget = toolWidgets->circleToolOptions,
		},
	};
	for (int i = 0; i < countof(editingModesInfo); ++i) {
		const auto& editingModeInfo = editingModesInfo[i];
		QAction* action = new QAction{editingModeInfo.name, parent};
		action->setCheckable(true);
		action->setEnabled(false);
		action->setData(static_cast<EditingMode>(i));
		action->setToolTip(editingModeInfo.tooltip);
		action->setIcon(QPixmap{editingModeInfo.icon});
		ui->editingModesToolBar->addAction(action);
		QWidget* widget = editingModeInfo.widget;
		if (widget == nullptr) {
			widget = new QWidget{parent};
		}
		ui->toolWidgetStack->addWidget(widget);
		QObject::connect(action, &QAction::triggered, [ui, i]{
			ui->toolWidgetStack->setCurrentIndex(i);
		});
	}
}

constexpr bool sortModelIndexesByRow(const QModelIndex& a, const QModelIndex& b)
{
	return a.row() < b.row();
}

std::vector<int> rows(const QModelIndexList& indexList)
{
	std::vector<int> result;
	result.reserve(unsigned_cast(indexList.size()));
	for (const QModelIndex& index : indexList)
	{
		result.push_back(index.row());
	}
	return result;
}

static void about(QWidget* parent)
{
	QDialog dialog{parent};
	Ui_About ui;
	ui.setupUi(&dialog);
	const char* glVersion = reinterpret_cast<const char*>(glGetString(GL_VERSION));
	const QString extensions = []{
		GLint numExtensions;
		glGetIntegerv(GL_NUM_EXTENSIONS, &numExtensions);
		QStringList extensionsList;
		for (GLint i = 0; i < numExtensions; i++) {
			const GLubyte* ext = glGetStringi(GL_EXTENSIONS, i);
			extensionsList.push_back(reinterpret_cast<const char*>(ext));
		}
		return extensionsList.join(" ");
	}();
	for (QTextBrowser* browser : dialog.findChildren<QTextBrowser*>()) {
		browser->setHtml(
			browser->toHtml()
			.replace("%APPNAME%", CMAKE_PROJECT_NAME)
			.replace("%COPYRIGHT%", COPYRIGHT)
			.replace("%QTVERSION%", qVersion())
			.replace("%VERSION%", detailedVersionString(QLocale::LongFormat))
			.replace("%REVDATE%", revisionDateString(QLocale::LongFormat))
			.replace("%BUILDTYPE%", CMAKE_BUILD_TYPE)
			.replace("%COMPILER_ID%", CMAKE_CXX_COMPILER_ID)
			.replace("%COMPILER_VERSION%", CMAKE_CXX_COMPILER_VERSION)
			.replace("%COMPILER_FLAGS%", CMAKE_CXX_FLAGS)
			.replace("%COMPILER_CPU%", CMAKE_SYSTEM_PROCESSOR)
			.replace("%COMPILER_SYSTEM%", CMAKE_SYSTEM)
			.replace("%GLMVERSIONSTRING%", GLM_VERSION_MESSAGE)
			.replace("%GL_VERSION%", glVersion)
			.replace("%GL_EXTENSIONS%", extensions)
		);
	}
	dialog.setWindowTitle(QObject::tr("About %1").arg(CMAKE_PROJECT_NAME));
	dialog.exec();
}

template<class SubWindow, class... Args>
SubWindow* createSubWindow(QMdiArea* mdiArea, Args&&... args)
{
	// Qt seems to have a bug where the first created sub window does not render
	// properly until it is minimized and maximized again. This only happens
	// if we give the mdi area as a parent argument. As a work-around, we create
	// the sub window with parent=nullptr, and add it manually.
	// c.f. https://bugreports.qt.io/browse/QTBUG-69495
	SubWindow* subWindow = new SubWindow{args..., nullptr};
	mdiArea->addSubWindow(subWindow);
	return subWindow;
}

static QSet<ModelId> resolveIdsFromSelection(const ModelData* data)
{
	const auto selection = data->itemSelectionModel->selection();
	QSet<ModelId> selectedIndexes;
	for (const QModelIndex& qindex : selection.indexes()) {
		const std::size_t row = unsigned_cast(qindex.row());
		selectedIndexes.insert(data->model->idAt(row));
	}
	return selectedIndexes;
}

int main(int argc, char *argv[])
{
	doQtRegistrations();
	QApplication app{argc, argv};
	QApplication::setWindowIcon(QIcon{":/icons/appicon.png"});
	QMainWindow mainWindow;
	Ui_MainWindow ui;
	DocumentManager documents;
	QString currentLanguage = "en";
	QTranslator translator{&mainWindow};
	LibrariesModel libraries{&mainWindow};
	QStringList recentlyOpenedFiles;
	ColorTable colorTable;
	gl::RenderPreferences renderPreferences;
	MessageLog messageLog;
	Signal settingsChanged;
	ui.setupUi(&mainWindow);
	ToolWidgets toolWidgets{
		.circleToolOptions = new CircleToolOptionsWidget{&mainWindow},
		.objectEditor = new ObjectEditor{&mainWindow},
	};
	const auto updateTitle = [&ui, &mainWindow]{
		mainWindow.setWindowTitle(title(&ui));
	};
	const uiutilities::KeySequenceMap defaultKeyboardShortcuts =
		uiutilities::makeKeySequenceMap(uiutilities::collectActions(&mainWindow));
	const auto saveSettings = [&]{
		setSetting<Setting::MainWindowGeometry>(mainWindow.saveGeometry());
		setSetting<Setting::MainWindowState>(mainWindow.saveState());
		setSetting<Setting::RecentFiles>(recentlyOpenedFiles);
		setSetting<Setting::RenderStyle>(renderPreferences.style);
		setSetting<Setting::DrawAxes>(renderPreferences.drawAxes);
		setSetting<Setting::Wireframe>(renderPreferences.wireframe);
		libraries.storeToSettings();
		settingsChanged.emit();
	};
	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);
				}
			);
		}
	};
	static constexpr auto executeAction = [&](
		Model* model, const ModelAction& action
	) {
		std::visit(overloaded{
			[model](const AppendToModel& action){
				model->append(action.newElement);
			},
			[model](const DeleteFromModel& action){
					model->remove(action.position);
			},
			[model](const ModifyModel& action){
				model->assignAt(action.position, action.newElement);
			},
		}, action);
	};
	const auto restoreSettings = [&]{
		recentlyOpenedFiles = setting<Setting::RecentFiles>();
		renderPreferences = loadRenderPreferences();
		libraries.restoreFromSettings();
		updateRecentlyOpenedDocumentsMenu();
		colorTable = loadColors(&libraries);
		updateRenderPreferences(&ui, &renderPreferences, &documents);
		ui.mdiArea->setViewMode(setting<Setting::ViewMode>());
		ui.retranslateUi(&mainWindow);
		settingsChanged.emit();
	};
	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){
		Model* model = documents.getModelById(modelId);
		if (model != nullptr) {
			ModelData* data = new ModelData(&documents);
			data->tools = std::make_unique<EditTools>();
			data->canvas = std::make_unique<PartRenderer>(model, &documents, colorTable);
			data->itemSelectionModel = std::make_unique<QItemSelectionModel>();
			data->itemSelectionModel->setModel(model);
			data->axesLayer = std::make_unique<AxesLayer>();
			constexpr glm::mat4 XZ = {{1, 0, 0, 0}, {0, 0, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 1}};
			data->gridLayer = std::make_unique<GridLayer>();
			data->gridLayer->setGridMatrix(XZ);
			data->tools->setGridMatrix(XZ);
			data->model = model;
			data->canvas->addRenderLayer(data->axesLayer.get());
			data->canvas->setLayerEnabled(data->axesLayer.get(), setting<Setting::DrawAxes>());
			data->canvas->addRenderLayer(data->gridLayer.get());
			data->canvas->addRenderLayer(data->tools.get());
			documents.setModelPayload(modelId, data);
			QObject::connect(
				data->tools.get(),
				&EditTools::modelAction,
				std::bind(executeAction, model, std::placeholders::_1));
			QObject::connect(
				data->itemSelectionModel.get(),
				&QItemSelectionModel::selectionChanged,
				[modelId, &documents, &toolWidgets]{
				ModelData* data = findModelData(&documents, modelId);
				if (data != nullptr) {
					data->canvas->setSelection(resolveIdsFromSelection(data));
					/*
					if (indices.size() == 1) {
						opt<std::size_t> index = data->model->find(*indices.begin());
						if (index.has_value()) {
							toolWidgets.objectEditor->setObject((*data->model)[*index]);
						}
					}
					else {
						toolWidgets.objectEditor->reset();
					}
					*/
				}
			});
			data->canvas->setRenderPreferences(renderPreferences);
			QObject::connect(
				data->tools.get(),
				&EditTools::newStatusText,
				[&](const QString& newStatusText) {
					mainWindow.statusBar()->showMessage(newStatusText);
				});
			QObject::connect(
				data->tools.get(),
				&EditTools::select,
				[modelId, &documents](const QSet<ModelId>& indices, bool retain) {
					ModelData* data = findModelData(&documents, modelId);
					if (data != nullptr) {
						if (not retain) {
							data->itemSelectionModel->clear();
						}
						for (const ModelId id : indices) {
							opt<int> index = data->model->find(id);
							if (index.has_value()) {
								const QModelIndex qindex = data->model->index(*index);
								data->itemSelectionModel->select(qindex, QItemSelectionModel::Select);
							}
						}
					}
				});
			QObject::connect(&settingsChanged, &Signal::triggered, [modelId, &documents]{
				ModelData* data = findModelData(&documents, modelId);
				if (data != nullptr) {
					data->gridLayer->settingsChanged();
				}
			});
			QObject::connect(data->canvas.get(), &PartRenderer::message, &messageLog, &MessageLog::addMessage);
			const QFileInfo fileInfo{*documents.modelPath(modelId)};
			auto* const subWindow = createSubWindow<ModelSubWindow>(ui.mdiArea, modelId);
			subWindow->setMinimumSize({96, 96});
			subWindow->resize({320, 200});
			subWindow->setWidget(data->canvas.get());
			subWindow->setWindowTitle(tabName(fileInfo));
			subWindow->show();
		}
	};
	QObject::connect(ui.actionNew, &QAction::triggered, [&]{
		openModelForEditing(documents.newModel());
	});
	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, [&]{
		if (ui.mdiArea->findChildren<SettingsEditor*>().isEmpty()) {
			auto* const settingsEditor = createSubWindow<SettingsEditor>(ui.mdiArea, defaultKeyboardShortcuts);
			QObject::connect(&settingsChanged, &Signal::triggered, settingsEditor, &SettingsEditor::loadSettings);
			QObject::connect(settingsEditor, &SettingsEditor::settingsChanged, restoreSettings);
			settingsEditor->setAttribute(Qt::WA_DeleteOnClose);
			settingsEditor->show();
		}
	});
	QObject::connect(ui.actionQuit, &QAction::triggered, &mainWindow, &QMainWindow::close);
#if 0
	QObject::connect(ui.actionAdjustGridToView, &QAction::triggered, [&]{
		if (ModelData* data = currentModelData(&ui, &documents)) {
			adjustGridToView(data->canvas.get());
		}
	});
#endif
	QObject::connect(ui.actionClose, &QAction::triggered, [&ui, &documents]{
		if (ModelData* data = currentModelData(&ui, &documents)) {
			// TODO
		}
	});
	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);
		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);
				QMdiSubWindow* const subWindow = ui.mdiArea->currentSubWindow();
				if (subWindow != nullptr) {
					subWindow->setWindowTitle(tabName(QFileInfo{newPath}));
				}
				save(*modelId);
			}
		}
	};
	QObject::connect(ui.actionSaveAs, &QAction::triggered, actionSaveAs);
	QObject::connect(ui.actionSave, &QAction::triggered, [&]{
		const std::optional<ModelId> modelId = findCurrentModelId(&ui);
		if (modelId.has_value()) {
			const QString* path = documents.modelPath(*modelId);
			if (path == nullptr or path->isEmpty()) {
				actionSaveAs();
			}
			else {
				save(*modelId);
			}
		}
	});
	QObject::connect(ui.actionDelete, &QAction::triggered, [&]{
		if (Model* model = currentModelBody(&ui, &documents)) {
			std::vector<int> selectedRows = rows(ui.modelListView->selectionModel()->selectedRows());
			std::sort(selectedRows.begin(), selectedRows.end(), std::greater<int>{});
			for (int row : selectedRows) {
				executeAction(model, DeleteFromModel{.position = unsigned_cast(row)});
			}
		}
	});
	QObject::connect(ui.actionDrawAxes, &QAction::triggered, [&](bool drawAxes){
		renderPreferences.drawAxes = drawAxes;
		saveSettings();
		updateRenderPreferences(&ui, &renderPreferences, &documents);
	});
	QObject::connect(ui.actionWireframe, &QAction::triggered, [&](bool enabled){
		renderPreferences.wireframe = enabled;
		saveSettings();
		updateRenderPreferences(&ui, &renderPreferences, &documents);
	});
	for (auto data : ::renderStyleButtons) {
		QAction* action = data.memberInstance(&ui);
		QObject::connect(action, &QAction::triggered, [&, data]{
			renderPreferences.style = data.payload;
			saveSettings();
			updateRenderPreferences(&ui, &renderPreferences, &documents);
		});
	}
	const auto checkEditingModeAction = [&ui, &documents](EditingMode mode) {
		const bool hasDocument = currentModelData(&ui, &documents) != nullptr;
		for (QAction* action : ui.editingModesToolBar->actions()) {
			action->setEnabled(hasDocument);
			action->setChecked(hasDocument and action->data().value<EditingMode>() == mode);
		}
	};
	initializeTools(&ui, &toolWidgets, &mainWindow);
	for (QAction* action : ui.editingModesToolBar->actions()) {
		QObject::connect(action, &QAction::triggered, [&, action]{
			if (ModelData* data = currentModelData(&ui, &documents)) {
				const EditingMode mode = action->data().value<EditingMode>();
				data->tools->setEditMode(mode);
				checkEditingModeAction(mode);
			}
		});
	}
	QObject::connect(ui.mdiArea, &QMdiArea::subWindowActivated,
	[&](QMdiSubWindow* subWindow) {
		ModelSubWindow* modelSubWindow = qobject_cast<ModelSubWindow*>(subWindow);
		if (modelSubWindow != nullptr) {
			if (ModelData* data = documents.findPayload<ModelData>(modelSubWindow->modelId)) {
				checkEditingModeAction(data->tools->currentEditingMode());
				if (data->itemSelectionModel != nullptr) {
					ui.modelListView->setModel(data->model);
					ui.modelListView->setSelectionModel(data->itemSelectionModel.get());
				}
			}
		}
		else {
			checkEditingModeAction(EditingMode::SelectMode);
		}
		updateTitle();
	});
	ui.messageLog->setModel(&messageLog);
	QObject::connect(ui.actionAboutQt, &QAction::triggered, &app, &QApplication::aboutQt);
	QObject::connect(&documents, &DocumentManager::message, &messageLog, &MessageLog::addMessage);
	QObject::connect(&messageLog, &MessageLog::rowsAboutToBeInserted, [&]{
		const auto bar = ui.messageLog->verticalScrollBar();
		ui.messageLog->setProperty("shouldAutoScroll", bar->value() == bar->maximum());
	});
	QObject::connect(&messageLog, &MessageLog::rowsInserted, [&]{
		ui.messageLog->resizeRowsToContents();
		if (ui.messageLog->property("shouldAutoScroll").toBool()) {
			ui.messageLog->scrollToBottom();
		}
	});
	QObject::connect(
		toolWidgets.circleToolOptions,
		&CircleToolOptionsWidget::optionsChanged,
		[&ui, &documents](const CircleToolOptions& options) {
			if (ModelData* data = currentModelData(&ui, &documents)) {
				data->tools->setCircleToolOptions(options);
			}
		});
	QObject::connect(
		ui.actionMakeUnofficial,
		&QAction::triggered,
		[&]{
			if (ModelData* data = currentModelData(&ui, &documents)) {
				Model* const model = data->model;
				for (const ModelAction& action : ldraw::makeUnofficial(model)) {
					executeAction(model, action);
				}
			}
		});
	QObject::connect(
		ui.actionAbout,
		&QAction::triggered,
		[&mainWindow, &ui]{
			// Make sure that there's an OpenGL context active, otherwise
			// we cannot obtain OpenGL information
			if (ui.mdiArea->findChildren<ModelSubWindow*>().empty()) {
				ui.actionNew->trigger();
			}
			about(&mainWindow);
		}
	);
	mainWindow.tabifyDockWidget(ui.messageLogDock, ui.toolOptionsDock);
	mainWindow.restoreGeometry(setting<Setting::MainWindowGeometry>());
	mainWindow.restoreState(setting<Setting::MainWindowState>());
	// If a dock is made floating and the app is closed, the dock becomes invisible
	// after the restoreState call. So we make them visible again here.
	for (QDockWidget* dock : mainWindow.findChildren<QDockWidget*>()) {
		dock->setVisible(true);
	}
	restoreSettings();
	updateRenderPreferences(&ui, &renderPreferences, &documents);
	ui.actionAbout->setText(ui.actionAbout->text().arg(CMAKE_PROJECT_NAME));
	updateTitle();
	mainWindow.show();
	const int result = app.exec();
	saveSettings();
	return result;
}

mercurial