src/main.cpp

Thu, 15 Jun 2023 16:18:03 +0300

author
Teemu Piippo <teemu.s.piippo@gmail.com>
date
Thu, 15 Jun 2023 16:18:03 +0300
changeset 383
530d23cd4e97
parent 376
3cef3b016330
permissions
-rw-r--r--

Refactor, make selecting elements from the model select the corresponding line from the editor as well

#include <QApplication>
#include <QClipboard>
#include <QCloseEvent>
#include <QFileDialog>
#include <QMdiSubWindow>
#include <QMessageBox>
#include <QScrollBar>
#include <QStackedWidget>
#include <QTranslator>
#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/mainwindow.h"
#include "src/messagelog.h"
#include "src/modelsubwindow.h"
#include "src/settings.h"
#include "src/settingseditor/settingseditor.h"
#include "src/ui/circletooloptionswidget.h"
#include "src/version.h"
#include "src/widgets/colorselectdialog.h"
#include "src/parser.h"
#include "src/ldrawsyntaxhighlighter.h"
#include <GL/glew.h>
#include "src/openedmodel.h"

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

#include <main.moc>

using ModelData = EditableModel;

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>();
	qRegisterMetaTypeStreamOperators<Qt::ToolButtonStyle>();
#endif
}

struct ToolWidgets
{
	CircleToolOptionsWidget* circleToolOptions;
};

struct MainState
{
	MainWindow mainWindow;
	DocumentManager documents;
	QString currentLanguage = "en";
	QTranslator translator{&mainWindow};
	LibrariesModel libraries{&mainWindow};
	QStringList recentlyOpenedFiles;
	ColorTable colorTable;
	gl::RenderPreferences renderPreferences;
	gl::build_preferences user_gl_build_preferences;
	MessageLog messageLog;
	ToolWidgets toolWidgets{
		.circleToolOptions = new CircleToolOptionsWidget{&mainWindow},
	};
};

static std::optional<ModelId> openModelFromPath(MainState* state, const QString& path)
{
	QString errorString;
	QTextStream errorStream{&errorString};
	const std::optional<ModelId> modelIdOpt = state->documents.openModel(
		path,
		errorStream,
		OpenType::ManuallyOpened);
	if (modelIdOpt.has_value()) {
		const DocumentManager::MissingDependencies missing = state->documents.loadDependenciesForAllModels(state->libraries);
		if (not missing.empty()) {
			QMessageBox::warning(
				&state->mainWindow,
				QObject::tr("Problem loading references"),
				errorStringFromMissingDependencies(missing));
		}
	}
	else {
		QMessageBox::critical(
			&state->mainWindow,
			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(MainWindow* ui)
{
	auto* w = ui->mdiArea->activeSubWindow();
	return qobject_cast<ModelSubWindow*>(w);
}

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

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

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

template<typename Fn>
static void forEachModel(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 rebuild_polygons_for_all_models(MainState* state)
{
	forEachModel(&state->documents, [](const void*, const ModelData* data){
		if (data->canvas != nullptr) {
			data->canvas->build();
			data->canvas->update();
		}
	});
}

static void updateRenderPreferences(MainState* state)
{
	forEachModel(&state->documents, [state](const void*, const ModelData* data){
		if (data->canvas != nullptr) {
			data->canvas->setLayerEnabled(data->axesLayer.get(), state->renderPreferences.drawAxes);
			data->canvas->update();
		}
	});
	state->mainWindow.setRenderStyle(state->renderPreferences.style);
	state->mainWindow.actionDrawAxes->setChecked(state->renderPreferences.drawAxes);
	state->mainWindow.actionWireframe->setChecked(state->renderPreferences.wireframe);
}

static gl::build_preferences load_gl_build_preferences_from_settings()
{
	return gl::build_preferences{
		.mainColor = setting<Setting::MainColor>(),
		.backgroundColor = setting<Setting::BackgroundColor>(),
	};
}

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

static void initializeTools(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 = nullptr,
		},
		{
			.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(QVariant::fromValue(static_cast<editing_mode_e>(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);
		});
	}
}

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 void executeAction(QTextDocument* model, QPlainTextEdit* editor, const ModelAction& action)
{
	std::visit(overloaded{
		[model](const AppendToModel& action){
			QTextCursor cursor{model};
			cursor.movePosition(QTextCursor::End);
			const QString newText = modelElementToString(action.newElement);
			// Make sure we have an empty line
			if (not model->lastBlock().text().isEmpty()) {
				cursor.insertBlock();
			}
			cursor.insertText(newText);
		},
		[](const DeleteFromModel&){},
		[model](const ModifyModel& action){
			QTextBlock block = model->findBlockByLineNumber((int) action.position);
			if (block.isValid()) {
				QTextCursor cursor{block};
				cursor.select(QTextCursor::LineUnderCursor);
				cursor.insertText(modelElementToString(action.newElement));
			}
			//model->assignAt(action.position, action.newElement);
		},
		[editor](const SelectInModel& action){
			QTextCursor cursor = editor->textCursor();
			cursor.movePosition(QTextCursor::Start);
			cursor.movePosition(QTextCursor::NextBlock, QTextCursor::MoveAnchor, static_cast<signed>(action.position));
			cursor.select(QTextCursor::LineUnderCursor);
			editor->setTextCursor(cursor);
			editor->update();
		},
	}, action);
}

QFont codeEditorFontFromSettings()
{
	QFont font{};
	if (setting<Setting::CodeEditorUseSystemFont>())
	{
		font.setStyleHint(QFont::Monospace);
	}
	else
	{
		font.setFamily(setting<Setting::CodeEditorFontFamily>());
		font.setPointSize(setting<Setting::CodeEditorFontSize>());
	}
	return font;
}

static void openModelForEditing(MainState* state, const ModelId modelId)
{
	QTextDocument* model = state->documents.getModelById(modelId);
	if (model != nullptr) {
		ModelData* data = new ModelData(model, &state->documents, &state->colorTable);
		state->documents.setModelPayload(modelId, data);
		QObject::connect(
			data,
			&EditableModel::modelAction,
			std::bind(executeAction, model, state->mainWindow.modelEdit, std::placeholders::_1));
		data->canvas->render_preferences = &state->renderPreferences;
		data->canvas->build_preferences = &state->user_gl_build_preferences;
		QObject::connect(
			data,
			&EditableModel::newStatusText,
			[&state](const QString& newStatusText) {
				state->mainWindow.statusBar()->showMessage(newStatusText);
			});
#if 0
		QObject::connect(this, &Main::settingsChanged, [modelId, this]{
			ModelData* data = findModelData(&state.documents, modelId);
			if (data != nullptr) {
				data->gridLayer->settingsChanged();
			}
		});
#endif
		QObject::connect(data->canvas.get(), &PartRenderer::message, &state->messageLog, &MessageLog::addMessage);
		QObject::connect(
			data->tools.get(),
			&EditTools::suggestCursor,
			data->canvas.get(),
			&QWidget::setCursor);
		data->tools->setEditMode(editing_mode_e::select);
		const QFileInfo fileInfo{*state->documents.modelPath(modelId)};
		auto* const subWindow = createSubWindow<ModelSubWindow>(state->mainWindow.mdiArea, modelId);
		subWindow->setMinimumSize({96, 96});
		subWindow->resize({320, 200});
		subWindow->setWidget(data->canvas.get());
		subWindow->setWindowTitle(tabName(fileInfo));
		subWindow->show();
	}
}

static void updateRecentlyOpenedDocumentsMenu(MainState* state)
{
	state->mainWindow.rebuildRecentFilesMenu(state->recentlyOpenedFiles);
}

static void restoreSettings(MainState* state)
{
	state->recentlyOpenedFiles = setting<Setting::RecentFiles>();
	state->renderPreferences = loadRenderPreferences();
	state->user_gl_build_preferences = load_gl_build_preferences_from_settings();
	state->libraries.restoreFromSettings();
	updateRecentlyOpenedDocumentsMenu(state);
	state->colorTable = loadColors(&state->libraries);
	updateRenderPreferences(state);
	rebuild_polygons_for_all_models(state);
	state->mainWindow.mdiArea->setViewMode(setting<Setting::ViewMode>());
	state->mainWindow.retranslateUi(&state->mainWindow);
	state->mainWindow.setToolButtonStyle(setting<Setting::ToolButtonStyle>());
}

static void saveSettings(MainState* state)
{
	setSetting<Setting::MainWindowGeometry>(state->mainWindow.saveGeometry());
	setSetting<Setting::MainWindowState>(state->mainWindow.saveState());
	setSetting<Setting::RecentFiles>(state->recentlyOpenedFiles);
	setSetting<Setting::RenderStyle>(state->renderPreferences.style);
	setSetting<Setting::DrawAxes>(state->renderPreferences.drawAxes);
	setSetting<Setting::Wireframe>(state->renderPreferences.wireframe);
	state->libraries.storeToSettings();
}

static void addRecentlyOpenedFile(MainState* state, const QString& path)
{
	constexpr int maxRecentlyOpenedFiles = 10;
	state->recentlyOpenedFiles.removeAll(path);
	state->recentlyOpenedFiles.insert(0, path);
	while (state->recentlyOpenedFiles.size() > maxRecentlyOpenedFiles)
	{
		state->recentlyOpenedFiles.removeLast();
	}
	saveSettings(state);
	updateRecentlyOpenedDocumentsMenu(state);
}

static void saveCurrentModel(MainState* state, ModelId modelId)
{
	QString error;
	QTextStream errorStream{&error};
	const bool succeeded = state->documents.saveModel(modelId, errorStream);
	if (not succeeded)
	{
		QMessageBox::critical(&state->mainWindow, QObject::tr("Save error"), error);
	}
	else
	{
		const QString* pathPtr = state->documents.modelPath(modelId);
		if (pathPtr != nullptr) {
			addRecentlyOpenedFile(state, *pathPtr);
		}
	}
}

static void saveCurrentModelAs(MainState* state)
{
	const std::optional<ModelId> modelId = findCurrentModelId(&state->mainWindow);
	if (modelId.has_value())
	{
		const QString* pathPtr = state->documents.modelPath(*modelId);
		QString defaultPath = (pathPtr != nullptr) ? *pathPtr : "";
		const QString newPath = QFileDialog::getSaveFileName(
			&state->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};
			state->documents.setModelPath(*modelId, newPath, state->libraries, errorStream);
			QMdiSubWindow* const subWindow = state->mainWindow.mdiArea->currentSubWindow();
			if (subWindow != nullptr) {
				subWindow->setWindowTitle(tabName(QFileInfo{newPath}));
			}
			saveCurrentModel(state, *modelId);
		}
	}
}

static void checkEditingModeAction(MainState* state, const editing_mode_e mode)
{
	const bool hasDocument = currentModelData(&state->mainWindow, &state->documents) != nullptr;
	for (QAction* action : state->mainWindow.editingModesToolBar->actions())
	{
		action->setEnabled(hasDocument);
		action->setChecked(hasDocument and action->data().value<editing_mode_e>() == mode);
	}
}

static void update_model_grid_matrix(MainState* state)
{
	const glm::mat4 new_grid_matrix = state->mainWindow.gridMatrix->value();
	forEachModel(&state->documents, [&](const void*, const ModelData* data)
	{
		if (data->gridLayer != nullptr and data->tools != nullptr and data->canvas != nullptr)
		{
			data->gridLayer->setGridMatrix(new_grid_matrix);
			data->tools->setGridMatrix(new_grid_matrix);
			data->canvas->setModelViewOrigin(new_grid_matrix[3]);
			data->canvas->update();
		}
	});
}

static void set_grid_scale(MainState* state, const float factor)
{
	const glm::mat4 original = state->mainWindow.gridMatrix->value();
	const glm::mat4 unscaled = unscale_matrix(original).unscaled;
	const glm::mat4 rescaled = factor * unscaled;
	state->mainWindow.gridMatrix->setValue(rescaled);
	update_model_grid_matrix(state);
}

static void replace_color_in_selected_code(QTextCursor* cursor, const ColorIndex color)
{
	const auto pattern = R"(^(\s*(?:1|2|3|4|5)\s+)\d+)";
	static const QRegularExpression regular_expression{pattern, QRegularExpression::MultilineOption};
	QString text = cursor->selectedText();
	// Qt has decided to be "smart" and uses strange unicode characters instead of newlines
	text.replace("\u2029", "\n");
	text.replace(regular_expression, QStringLiteral(R"(\1%1)").arg(color.index));
	cursor->removeSelectedText();
	cursor->insertText(text);
}

int main(int argc, char *argv[])
{
	doQtRegistrations();
	QApplication app{argc, argv};
	QApplication::setWindowIcon(QIcon{":/icons/appicon.png"});
	MainState state;
	QObject::connect(
		&state.mainWindow,
		&MainWindow::recentFileSelected,
		[&state](const QString& path) {
			const auto id = openModelFromPath(&state, path);
			if (id.has_value())
			{
				openModelForEditing(&state, id.value());
				addRecentlyOpenedFile(&state, path);
			}
		}
	);
	QObject::connect(state.mainWindow.actionNew, &QAction::triggered,
		[&state]{
			openModelForEditing(&state, state.documents.newModel());
		}
	);
	QObject::connect(state.mainWindow.actionOpen, &QAction::triggered,
		[&state]
		{
			const QString path = getOpenModelPath(&state.mainWindow);
			if (not path.isEmpty())
			{
				const std::optional<ModelId> id = openModelFromPath(&state, path);
				if (id.has_value()) {
					openModelForEditing(&state, id.value());
					addRecentlyOpenedFile(&state, path);
				}
			}
		}
	);
	QObject::connect(state.mainWindow.actionSettingsEditor, &QAction::triggered, [
		&state,
		defaultKeyboardShortcuts = uiutilities::makeKeySequenceMap(uiutilities::collectActions(&state.mainWindow))]
	{
		if (state.mainWindow.mdiArea->findChildren<SettingsEditor*>().isEmpty())
		{
			auto* const settingsEditor = createSubWindow<SettingsEditor>(state.mainWindow.mdiArea, defaultKeyboardShortcuts);
			QObject::connect(settingsEditor, &SettingsEditor::settingsChanged, [&]{
				restoreSettings(&state);
			});
			settingsEditor->setAttribute(Qt::WA_DeleteOnClose);
			settingsEditor->show();
		}
	});
	QObject::connect(state.mainWindow.actionQuit, &QAction::triggered, &state.mainWindow, &QMainWindow::close);
#if 0
	QObject::connect(ui.actionAdjustGridToView, &QAction::triggered, [&]{
		if (ModelData* data = currentModelData(&ui, &documents)) {
			adjustGridToView(data->canvas.get());
		}
	});
#endif
	QObject::connect(state.mainWindow.actionClose, &QAction::triggered, [&state]{
		if (ModelData* data = currentModelData(&state.mainWindow, &state.documents)) {
			// TODO
		}
	});
	QObject::connect(state.mainWindow.actionSaveAs, &QAction::triggered, [&state]{
		saveCurrentModelAs(&state);
	});
	QObject::connect(state.mainWindow.actionSave, &QAction::triggered, [&state]
	{
		const std::optional<ModelId> modelId = findCurrentModelId(&state.mainWindow);
		if (modelId.has_value()) {
			const QString* path = state.documents.modelPath(*modelId);
			if (path == nullptr or path->isEmpty()) {
				saveCurrentModelAs(&state);
			}
			else {
				saveCurrentModel(&state, *modelId);
			}
		}
	});
	QObject::connect(state.mainWindow.actionDrawAxes, &QAction::triggered, [&state]
		(bool drawAxes)
	{
		state.renderPreferences.drawAxes = drawAxes;
		saveSettings(&state);
		updateRenderPreferences(&state);
	});
	QObject::connect(state.mainWindow.actionWireframe, &QAction::triggered, [&state]
		(bool enabled)
	{
		state.renderPreferences.wireframe = enabled;
		saveSettings(&state);
		updateRenderPreferences(&state);
	});
	QObject::connect(&state.mainWindow, &MainWindow::renderStyleSelected, [&state]
		(gl::RenderStyle newStyle)
	{
		state.renderPreferences.style = newStyle;
		saveSettings(&state);
		updateRenderPreferences(&state);
	});
	initializeTools(&state.mainWindow, &state.toolWidgets, &state.mainWindow);
	for (QAction* action : state.mainWindow.editingModesToolBar->actions()) {
		QObject::connect(action, &QAction::triggered, [action, &state]
		{
			if (ModelData* data = currentModelData(&state.mainWindow, &state.documents))
			{
				const editing_mode_e mode = action->data().value<editing_mode_e>();
				data->tools->setEditMode(mode);
				checkEditingModeAction(&state, mode);
			}
		});
	}
	QObject::connect(state.mainWindow.mdiArea, &QMdiArea::subWindowActivated,
		[&state](QMdiSubWindow* subWindow)
	{
		ModelSubWindow* modelSubWindow = qobject_cast<ModelSubWindow*>(subWindow);
		if (modelSubWindow != nullptr)
		{
			if (ModelData* data = state.documents.findPayload<ModelData>(modelSubWindow->modelId))
			{
				checkEditingModeAction(&state, data->tools->currentEditingMode());
				state.mainWindow.modelEdit->setDocument(data->model);
				state.mainWindow.modelEdit->setTextCursor(*data->textcursor);
				state.mainWindow.modelEdit->setFont(codeEditorFontFromSettings());
			}
		}
		else
		{
			checkEditingModeAction(&state, editing_mode_e::select);
		}
		state.mainWindow.modelEdit->setEnabled(modelSubWindow != nullptr);
	});
	state.mainWindow.messageLog->setModel(&state.messageLog);
	QObject::connect(&state.documents, &DocumentManager::message, &state.messageLog, &MessageLog::addMessage);
	QObject::connect(&state.messageLog, &MessageLog::rowsAboutToBeInserted, [&state]{
		const auto bar = state.mainWindow.messageLog->verticalScrollBar();
		state.mainWindow.messageLog->setProperty("shouldAutoScroll", bar->value() == bar->maximum());
	});
	QObject::connect(&state.messageLog, &MessageLog::rowsInserted, [&state]{
		state.mainWindow.messageLog->resizeRowsToContents();
		if (state.mainWindow.messageLog->property("shouldAutoScroll").toBool()) {
			state.mainWindow.messageLog->scrollToBottom();
		}
	});
	QObject::connect(
		state.toolWidgets.circleToolOptions,
		&CircleToolOptionsWidget::optionsChanged,
		[&state](const CircleToolOptions& options) {
			if (ModelData* data = currentModelData(&state.mainWindow, &state.documents)) {
				data->tools->setCircleToolOptions(options);
			}
		});
	QObject::connect(
		state.mainWindow.actionMakeUnofficial,
		&QAction::triggered,
		[&state]{
			if (ModelData* data = currentModelData(&state.mainWindow, &state.documents)) {
				QTextDocument* const model = data->model;
				for (const ModelAction& action : ldraw::makeUnofficial(model)) {
					executeAction(model, state.mainWindow.modelEdit, action);
				}
			}
		});
	QObject::connect(state.mainWindow.actionAboutQt, &QAction::triggered, &QApplication::aboutQt);
	QObject::connect(
		state.mainWindow.modelEdit,
		&QPlainTextEdit::textChanged,
		[&state]{
			if (ModelData* data = currentModelData(&state.mainWindow, &state.documents)) {
				state.documents.loadDependenciesForAllModels(state.libraries);
				data->canvas->update();
			}
		});
	QObject::connect(
		state.mainWindow.gridMatrix,
		&MatrixEditor::valueChanged,
		[&state]
		{
			update_model_grid_matrix(&state);
		}
	);
	QObject::connect(
		state.mainWindow.actionDelete,
		&QAction::triggered,
		[&state]{
			QTextCursor cursor = state.mainWindow.modelEdit->textCursor();
			cursor.removeSelectedText();
		}
	);
	QObject::connect(
		state.mainWindow.actionGridCoarse,
		&QAction::triggered,
		[&state]{
			set_grid_scale(&state, 5.0f);
		}
	);
	QObject::connect(
		state.mainWindow.actionGridMedium,
		&QAction::triggered,
		[&state]{
			set_grid_scale(&state, 1.0f);
		}
	);
	QObject::connect(
		state.mainWindow.actionGridFine,
		&QAction::triggered,
		[&state]{
			set_grid_scale(&state, 0.1f);
		}
	);
	QObject::connect(
		state.mainWindow.action_make_stuff_red,
		&QAction::triggered,
		[&state]{
			QTextCursor cursor = state.mainWindow.modelEdit->textCursor();
			replace_color_in_selected_code(&cursor, ColorIndex{4});
		}
	);
	restoreSettings(&state);
	const int result = app.exec();
	saveSettings(&state);
	return result;
}

mercurial