src/main.cpp

Fri, 01 Jul 2022 16:46:43 +0300

author
Teemu Piippo <teemu.s.piippo@gmail.com>
date
Fri, 01 Jul 2022 16:46:43 +0300
changeset 312
2637134bc37c
parent 311
fab454611f9b
child 325
64ddcd77639f
permissions
-rw-r--r--

Fix right click to delete not really working properly
Instead of removing the point that had been added, it would remove
the point that is being drawn, which would cause it to overwrite the
previous point using the new point, causing a bit of a delay

#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<ElementId> resolveIdsFromSelection(const ModelData* data)
{
	const auto selection = data->itemSelectionModel->selection();
	QSet<ElementId> 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<ElementId>& indices, bool retain) {
					ModelData* data = findModelData(&documents, modelId);
					if (data != nullptr) {
						if (not retain) {
							data->itemSelectionModel->clear();
						}
						for (const ElementId 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);
			QObject::connect(
				data->tools.get(),
				&EditTools::suggestCursor,
				data->canvas.get(),
				&QWidget::setCursor);
			data->tools->setEditMode(SelectMode);
			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