|    326 		font.setPointSize(setting<Setting::CodeEditorFontSize>()); | 
   307 		font.setPointSize(setting<Setting::CodeEditorFontSize>()); | 
|    327 	} | 
   308 	} | 
|    328 	return font; | 
   309 	return font; | 
|    329 } | 
   310 } | 
|    330  | 
   311  | 
|    331 constexpr glm::mat4 DEFAULT_GRID_MATRIX = {{1, 0, 0, 0}, {0, 0, 1, 0}, {0, 1, 0, 0}, {0, 0, 0, 1}}; | 
   312 struct MainState | 
|    332  | 
   313 { | 
|    333 int main(int argc, char *argv[]) | 
        | 
|    334 { | 
        | 
|    335 	doQtRegistrations(); | 
        | 
|    336 	QApplication app{argc, argv}; | 
        | 
|    337 	QApplication::setWindowIcon(QIcon{":/icons/appicon.png"}); | 
        | 
|    338 	MainWindow mainWindow; | 
   314 	MainWindow mainWindow; | 
|    339 	DocumentManager documents; | 
   315 	DocumentManager documents; | 
|    340 	QString currentLanguage = "en"; | 
   316 	QString currentLanguage = "en"; | 
|    341 	QTranslator translator{&mainWindow}; | 
   317 	QTranslator translator{&mainWindow}; | 
|    342 	LibrariesModel libraries{&mainWindow}; | 
   318 	LibrariesModel libraries{&mainWindow}; | 
|    343 	QStringList recentlyOpenedFiles; | 
   319 	QStringList recentlyOpenedFiles; | 
|    344 	ColorTable colorTable; | 
   320 	ColorTable colorTable; | 
|    345 	gl::RenderPreferences renderPreferences; | 
   321 	gl::RenderPreferences renderPreferences; | 
|    346 	MessageLog messageLog; | 
   322 	MessageLog messageLog; | 
|    347 	Signal settingsChanged; | 
        | 
|    348 	ToolWidgets toolWidgets{ | 
   323 	ToolWidgets toolWidgets{ | 
|    349 		.circleToolOptions = new CircleToolOptionsWidget{&mainWindow}, | 
   324 		.circleToolOptions = new CircleToolOptionsWidget{&mainWindow}, | 
|    350 	}; | 
   325 	}; | 
|    351 	const auto updateTitle = [&mainWindow]{ | 
   326 }; | 
|    352 		mainWindow.setWindowTitle(title(&mainWindow)); | 
   327  | 
|    353 	}; | 
   328 static void openModelForEditing(MainState* state, const ModelId modelId) | 
|    354 	const uiutilities::KeySequenceMap defaultKeyboardShortcuts = | 
   329 { | 
|    355 		uiutilities::makeKeySequenceMap(uiutilities::collectActions(&mainWindow)); | 
   330 	QTextDocument* model = state->documents.getModelById(modelId); | 
|    356 	const auto saveSettings = [ | 
   331 	if (model != nullptr) { | 
|    357 		&libraries, | 
   332 		ModelData* data = new ModelData(&state->documents); | 
|    358 		&mainWindow, | 
   333 		data->tools = std::make_unique<EditTools>(); | 
|    359 		&recentlyOpenedFiles, | 
   334 		data->canvas = std::make_unique<PartRenderer>(model, &state->documents, state->colorTable); | 
|    360 		&renderPreferences, | 
   335 		data->axesLayer = std::make_unique<AxesLayer>(); | 
|    361 		&settingsChanged] | 
   336 		data->gridLayer = std::make_unique<GridLayer>(); | 
|    362 	{ | 
   337 		data->gridLayer->setGridMatrix(DEFAULT_GRID_MATRIX); | 
|    363 		setSetting<Setting::MainWindowGeometry>(mainWindow.saveGeometry()); | 
   338 		data->tools->setGridMatrix(DEFAULT_GRID_MATRIX); | 
|    364 		setSetting<Setting::MainWindowState>(mainWindow.saveState()); | 
   339 		data->model = model; | 
|    365 		setSetting<Setting::RecentFiles>(recentlyOpenedFiles); | 
   340 		data->canvas->addRenderLayer(data->axesLayer.get()); | 
|    366 		setSetting<Setting::RenderStyle>(renderPreferences.style); | 
   341 		data->canvas->setLayerEnabled(data->axesLayer.get(), setting<Setting::DrawAxes>()); | 
|    367 		setSetting<Setting::DrawAxes>(renderPreferences.drawAxes); | 
   342 		data->canvas->addRenderLayer(data->gridLayer.get()); | 
|    368 		setSetting<Setting::Wireframe>(renderPreferences.wireframe); | 
   343 		data->canvas->addRenderLayer(data->tools.get()); | 
|    369 		libraries.storeToSettings(); | 
   344 		new LDrawSyntaxHighlighter{model}; | 
|    370 		settingsChanged.emit(); | 
   345 		data->textcursor = std::make_unique<QTextCursor>(model); | 
|    371 	}; | 
   346 		state->documents.setModelPayload(modelId, data); | 
|    372 	const auto openModelForEditing = [ | 
   347 		QObject::connect( | 
|    373 		&colorTable, | 
   348 			data->tools.get(), | 
|    374 		&documents, | 
   349 			&EditTools::modelAction, | 
|    375 		&mainWindow, | 
   350 			std::bind(executeAction, model, std::placeholders::_1)); | 
|    376 		&messageLog, | 
   351 		data->canvas->setRenderPreferences(state->renderPreferences); | 
|    377 		&renderPreferences, | 
   352 		QObject::connect( | 
|    378 		&settingsChanged] | 
   353 			data->tools.get(), | 
|    379 		(const ModelId modelId) | 
   354 			&EditTools::newStatusText, | 
|    380 	{ | 
   355 			[&state](const QString& newStatusText) { | 
|    381 		QTextDocument* model = documents.getModelById(modelId); | 
   356 				state->mainWindow.statusBar()->showMessage(newStatusText); | 
|    382 		if (model != nullptr) { | 
   357 			}); | 
|    383 			ModelData* data = new ModelData(&documents); | 
        | 
|    384 			data->tools = std::make_unique<EditTools>(); | 
        | 
|    385 			data->canvas = std::make_unique<PartRenderer>(model, &documents, colorTable); | 
        | 
|    386 			data->axesLayer = std::make_unique<AxesLayer>(); | 
        | 
|    387 			data->gridLayer = std::make_unique<GridLayer>(); | 
        | 
|    388 			data->gridLayer->setGridMatrix(DEFAULT_GRID_MATRIX); | 
        | 
|    389 			data->tools->setGridMatrix(DEFAULT_GRID_MATRIX); | 
        | 
|    390 			data->model = model; | 
        | 
|    391 			data->canvas->addRenderLayer(data->axesLayer.get()); | 
        | 
|    392 			data->canvas->setLayerEnabled(data->axesLayer.get(), setting<Setting::DrawAxes>()); | 
        | 
|    393 			data->canvas->addRenderLayer(data->gridLayer.get()); | 
        | 
|    394 			data->canvas->addRenderLayer(data->tools.get()); | 
        | 
|    395 			new LDrawSyntaxHighlighter{model}; | 
        | 
|    396 			data->textcursor = std::make_unique<QTextCursor>(model); | 
        | 
|    397 			documents.setModelPayload(modelId, data); | 
        | 
|    398 			QObject::connect( | 
        | 
|    399 				data->tools.get(), | 
        | 
|    400 				&EditTools::modelAction, | 
        | 
|    401 				std::bind(executeAction, model, std::placeholders::_1)); | 
        | 
|    402 			data->canvas->setRenderPreferences(renderPreferences); | 
        | 
|    403 			QObject::connect( | 
        | 
|    404 				data->tools.get(), | 
        | 
|    405 				&EditTools::newStatusText, | 
        | 
|    406 				[&mainWindow](const QString& newStatusText) { | 
        | 
|    407 					mainWindow.statusBar()->showMessage(newStatusText); | 
        | 
|    408 				}); | 
        | 
|    409 #if 0 | 
   358 #if 0 | 
|    410 			QObject::connect( | 
   359 		QObject::connect( | 
|    411 				data->tools.get(), | 
   360 			data->tools.get(), | 
|    412 				&EditTools::select, | 
   361 			&EditTools::select, | 
|    413 				[modelId, &documents](const QSet<ElementId>& indices, bool retain) { | 
   362 			[modelId, &documents](const QSet<ElementId>& indices, bool retain) { | 
|    414 					ModelData* data = findModelData(&documents, modelId); | 
   363 				ModelData* data = findModelData(&documents, modelId); | 
|    415 					if (data != nullptr) { | 
   364 				if (data != nullptr) { | 
|    416 						if (not retain) { | 
   365 					if (not retain) { | 
|    417 							data->textcursor->clearSelection(); | 
   366 						data->textcursor->clearSelection(); | 
|    418 						} | 
   367 					} | 
|    419 						for (const ElementId id : indices) { | 
   368 					for (const ElementId id : indices) { | 
|    420 							opt<int> index = data->model->find(id); | 
   369 						opt<int> index = data->model->find(id); | 
|    421 							if (index.has_value()) { | 
   370 						if (index.has_value()) { | 
|    422 								const QModelIndex qindex = data->model->index(*index); | 
   371 							const QModelIndex qindex = data->model->index(*index); | 
|    423 								data->itemSelectionModel->select(qindex, QItemSelectionModel::Select); | 
   372 							data->itemSelectionModel->select(qindex, QItemSelectionModel::Select); | 
|    424 							} | 
        | 
|    425 						} | 
   373 						} | 
|    426 					} | 
   374 					} | 
|    427 				}); | 
        | 
|    428 #endif | 
        | 
|    429 			QObject::connect(&settingsChanged, &Signal::triggered, [modelId, &documents]{ | 
        | 
|    430 				ModelData* data = findModelData(&documents, modelId); | 
        | 
|    431 				if (data != nullptr) { | 
        | 
|    432 					data->gridLayer->settingsChanged(); | 
        | 
|    433 				} | 
   375 				} | 
|    434 			}); | 
   376 			}); | 
|    435 			QObject::connect(data->canvas.get(), &PartRenderer::message, &messageLog, &MessageLog::addMessage); | 
   377 #endif | 
|    436 			QObject::connect( | 
   378 #if 0 | 
|    437 				data->tools.get(), | 
   379 		QObject::connect(this, &Main::settingsChanged, [modelId, this]{ | 
|    438 				&EditTools::suggestCursor, | 
   380 			ModelData* data = findModelData(&state.documents, modelId); | 
|    439 				data->canvas.get(), | 
   381 			if (data != nullptr) { | 
|    440 				&QWidget::setCursor); | 
   382 				data->gridLayer->settingsChanged(); | 
|    441 			data->tools->setEditMode(SelectMode); | 
   383 			} | 
|    442 			const QFileInfo fileInfo{*documents.modelPath(modelId)}; | 
   384 		}); | 
|    443 			auto* const subWindow = createSubWindow<ModelSubWindow>(mainWindow.mdiArea, modelId); | 
   385 #endif | 
|    444 			subWindow->setMinimumSize({96, 96}); | 
   386 		QObject::connect(data->canvas.get(), &PartRenderer::message, &state->messageLog, &MessageLog::addMessage); | 
|    445 			subWindow->resize({320, 200}); | 
   387 		QObject::connect( | 
|    446 			subWindow->setWidget(data->canvas.get()); | 
   388 			data->tools.get(), | 
|    447 			subWindow->setWindowTitle(tabName(fileInfo)); | 
   389 			&EditTools::suggestCursor, | 
|    448 			subWindow->show(); | 
   390 			data->canvas.get(), | 
|    449 		} | 
   391 			&QWidget::setCursor); | 
|    450 	}; | 
   392 		data->tools->setEditMode(SelectMode); | 
|    451 	const auto updateRecentlyOpenedDocumentsMenu = [ | 
   393 		const QFileInfo fileInfo{*state->documents.modelPath(modelId)}; | 
|    452 		&mainWindow, | 
   394 		auto* const subWindow = createSubWindow<ModelSubWindow>(state->mainWindow.mdiArea, modelId); | 
|    453 		&recentlyOpenedFiles] | 
   395 		subWindow->setMinimumSize({96, 96}); | 
|    454 	{ | 
   396 		subWindow->resize({320, 200}); | 
|    455 		mainWindow.rebuildRecentFilesMenu(recentlyOpenedFiles); | 
   397 		subWindow->setWidget(data->canvas.get()); | 
|    456 	}; | 
   398 		subWindow->setWindowTitle(tabName(fileInfo)); | 
|    457 	const auto restoreSettings = [ | 
   399 		subWindow->show(); | 
|    458 		&colorTable, | 
   400 	} | 
|    459 		&documents, | 
   401 } | 
|    460 		&libraries, | 
   402  | 
|    461 		&mainWindow, | 
   403 static void updateRecentlyOpenedDocumentsMenu(MainState* state) | 
|    462 		&recentlyOpenedFiles, | 
   404 { | 
|    463 		&renderPreferences, | 
   405 	state->mainWindow.rebuildRecentFilesMenu(state->recentlyOpenedFiles); | 
|    464 		&settingsChanged, | 
   406 } | 
|    465 		&updateRecentlyOpenedDocumentsMenu] | 
   407  | 
|    466 	{ | 
   408 static void restoreSettings(MainState* state) | 
|    467 		recentlyOpenedFiles = setting<Setting::RecentFiles>(); | 
   409 { | 
|    468 		renderPreferences = loadRenderPreferences(); | 
   410 	state->recentlyOpenedFiles = setting<Setting::RecentFiles>(); | 
|    469 		libraries.restoreFromSettings(); | 
   411 	state->renderPreferences = loadRenderPreferences(); | 
|    470 		updateRecentlyOpenedDocumentsMenu(); | 
   412 	state->libraries.restoreFromSettings(); | 
|    471 		colorTable = loadColors(&libraries); | 
   413 	updateRecentlyOpenedDocumentsMenu(state); | 
|    472 		updateRenderPreferences(&mainWindow, &renderPreferences, &documents); | 
   414 	state->colorTable = loadColors(&state->libraries); | 
|    473 		mainWindow.mdiArea->setViewMode(setting<Setting::ViewMode>()); | 
   415 	updateRenderPreferences(&state->mainWindow, &state->renderPreferences, &state->documents); | 
|    474 		mainWindow.retranslateUi(&mainWindow); | 
   416 	state->mainWindow.mdiArea->setViewMode(setting<Setting::ViewMode>()); | 
|    475 		mainWindow.setToolButtonStyle(setting<Setting::ToolButtonStyle>()); | 
   417 	state->mainWindow.retranslateUi(&state->mainWindow); | 
|    476 		settingsChanged.emit(); | 
   418 	state->mainWindow.setToolButtonStyle(setting<Setting::ToolButtonStyle>()); | 
|    477 	}; | 
   419 } | 
|    478 	const auto addRecentlyOpenedFile = [ | 
   420  | 
|    479 		&recentlyOpenedFiles, | 
   421 static void saveSettings(MainState* state) | 
|    480 		&saveSettings, | 
   422 { | 
|    481 		&updateRecentlyOpenedDocumentsMenu] | 
   423 	setSetting<Setting::MainWindowGeometry>(state->mainWindow.saveGeometry()); | 
|    482 		(const QString& path) | 
   424 	setSetting<Setting::MainWindowState>(state->mainWindow.saveState()); | 
|    483 	{ | 
   425 	setSetting<Setting::RecentFiles>(state->recentlyOpenedFiles); | 
|    484 		constexpr int maxRecentlyOpenedFiles = 10; | 
   426 	setSetting<Setting::RenderStyle>(state->renderPreferences.style); | 
|    485 		recentlyOpenedFiles.removeAll(path); | 
   427 	setSetting<Setting::DrawAxes>(state->renderPreferences.drawAxes); | 
|    486 		recentlyOpenedFiles.insert(0, path); | 
   428 	setSetting<Setting::Wireframe>(state->renderPreferences.wireframe); | 
|    487 		while (recentlyOpenedFiles.size() > maxRecentlyOpenedFiles) | 
   429 	state->libraries.storeToSettings(); | 
|    488 		{ | 
   430 } | 
|    489 			recentlyOpenedFiles.removeLast(); | 
   431  | 
|    490 		} | 
   432 static void addRecentlyOpenedFile(MainState* state, const QString& path) | 
|    491 		saveSettings(); | 
   433 { | 
|    492 		updateRecentlyOpenedDocumentsMenu(); | 
   434 	constexpr int maxRecentlyOpenedFiles = 10; | 
|    493 	}; | 
   435 	state->recentlyOpenedFiles.removeAll(path); | 
|         | 
   436 	state->recentlyOpenedFiles.insert(0, path); | 
|         | 
   437 	while (state->recentlyOpenedFiles.size() > maxRecentlyOpenedFiles) | 
|         | 
   438 	{ | 
|         | 
   439 		state->recentlyOpenedFiles.removeLast(); | 
|         | 
   440 	} | 
|         | 
   441 	saveSettings(state); | 
|         | 
   442 	updateRecentlyOpenedDocumentsMenu(state); | 
|         | 
   443 } | 
|         | 
   444  | 
|         | 
   445 static void saveCurrentModel(MainState* state, ModelId modelId) | 
|         | 
   446 { | 
|         | 
   447 	QString error; | 
|         | 
   448 	QTextStream errorStream{&error}; | 
|         | 
   449 	const bool succeeded = state->documents.saveModel(modelId, errorStream); | 
|         | 
   450 	if (not succeeded) | 
|         | 
   451 	{ | 
|         | 
   452 		QMessageBox::critical(&state->mainWindow, QObject::tr("Save error"), error); | 
|         | 
   453 	} | 
|         | 
   454 	else | 
|         | 
   455 	{ | 
|         | 
   456 		const QString* pathPtr = state->documents.modelPath(modelId); | 
|         | 
   457 		if (pathPtr != nullptr) { | 
|         | 
   458 			addRecentlyOpenedFile(state, *pathPtr); | 
|         | 
   459 		} | 
|         | 
   460 	} | 
|         | 
   461 } | 
|         | 
   462  | 
|         | 
   463 static void saveCurrentModelAs(MainState* state) | 
|         | 
   464 { | 
|         | 
   465 	const std::optional<ModelId> modelId = findCurrentModelId(&state->mainWindow); | 
|         | 
   466 	if (modelId.has_value()) | 
|         | 
   467 	{ | 
|         | 
   468 		const QString* pathPtr = state->documents.modelPath(*modelId); | 
|         | 
   469 		QString defaultPath = (pathPtr != nullptr) ? *pathPtr : ""; | 
|         | 
   470 		const QString newPath = QFileDialog::getSaveFileName( | 
|         | 
   471 			&state->mainWindow, | 
|         | 
   472 			QObject::tr("Save as…"), | 
|         | 
   473 			QFileInfo{defaultPath}.absoluteDir().path(),  | 
|         | 
   474 			QObject::tr("LDraw files (*.ldr *dat);;All files (*)") | 
|         | 
   475 		); | 
|         | 
   476 		if (not newPath.isEmpty()) { | 
|         | 
   477 			QString error; | 
|         | 
   478 			QTextStream errorStream{&error}; | 
|         | 
   479 			state->documents.setModelPath(*modelId, newPath, state->libraries, errorStream); | 
|         | 
   480 			QMdiSubWindow* const subWindow = state->mainWindow.mdiArea->currentSubWindow(); | 
|         | 
   481 			if (subWindow != nullptr) { | 
|         | 
   482 				subWindow->setWindowTitle(tabName(QFileInfo{newPath})); | 
|         | 
   483 			} | 
|         | 
   484 			saveCurrentModel(state, *modelId); | 
|         | 
   485 		} | 
|         | 
   486 	} | 
|         | 
   487 } | 
|         | 
   488  | 
|         | 
   489 static void checkEditingModeAction(MainState* state, const EditingMode mode) | 
|         | 
   490 { | 
|         | 
   491 	const bool hasDocument = currentModelData(&state->mainWindow, &state->documents) != nullptr; | 
|         | 
   492 	for (QAction* action : state->mainWindow.editingModesToolBar->actions()) | 
|         | 
   493 	{ | 
|         | 
   494 		action->setEnabled(hasDocument); | 
|         | 
   495 		action->setChecked(hasDocument and action->data().value<EditingMode>() == mode); | 
|         | 
   496 	} | 
|         | 
   497 } | 
|         | 
   498  | 
|         | 
   499 int main(int argc, char *argv[]) | 
|         | 
   500 { | 
|         | 
   501 	doQtRegistrations(); | 
|         | 
   502 	QApplication app{argc, argv}; | 
|         | 
   503 	QApplication::setWindowIcon(QIcon{":/icons/appicon.png"}); | 
|         | 
   504 	MainState state; | 
|    494 	QObject::connect( | 
   505 	QObject::connect( | 
|    495 		&mainWindow, | 
   506 		&state.mainWindow, | 
|    496 		&MainWindow::recentFileSelected, | 
   507 		&MainWindow::recentFileSelected, | 
|    497 		[&libraries, &documents, &mainWindow, &openModelForEditing, &addRecentlyOpenedFile](const QString& path) { | 
   508 		[&state](const QString& path) { | 
|    498 			const auto id = openModelFromPath(path, &libraries, &documents, &mainWindow); | 
   509 			const auto id = openModelFromPath(path, &state.libraries, &state.documents, &state.mainWindow); | 
|    499 			if (id.has_value()) | 
   510 			if (id.has_value()) | 
|    500 			{ | 
   511 			{ | 
|    501 				openModelForEditing(id.value()); | 
   512 				openModelForEditing(&state, id.value()); | 
|    502 				addRecentlyOpenedFile(path); | 
   513 				addRecentlyOpenedFile(&state, path); | 
|    503 			} | 
   514 			} | 
|    504 		} | 
   515 		} | 
|    505 	); | 
   516 	); | 
|    506 	QObject::connect(mainWindow.actionNew, &QAction::triggered, | 
   517 	QObject::connect(state.mainWindow.actionNew, &QAction::triggered, | 
|    507 		[&documents, &openModelForEditing]{ | 
   518 		[&state]{ | 
|    508 			openModelForEditing(documents.newModel()); | 
   519 			openModelForEditing(&state, state.documents.newModel()); | 
|    509 		} | 
   520 		} | 
|    510 	); | 
   521 	); | 
|    511 	QObject::connect(mainWindow.actionOpen, &QAction::triggered, | 
   522 	QObject::connect(state.mainWindow.actionOpen, &QAction::triggered, | 
|    512 		[&addRecentlyOpenedFile, &documents, &libraries, &mainWindow, &openModelForEditing] | 
   523 		[&state] | 
|    513 		{ | 
   524 		{ | 
|    514 			const QString path = getOpenModelPath(&mainWindow); | 
   525 			const QString path = getOpenModelPath(&state.mainWindow); | 
|    515 			if (not path.isEmpty()) | 
   526 			if (not path.isEmpty()) | 
|    516 			{ | 
   527 			{ | 
|    517 				const std::optional<ModelId> id = openModelFromPath(path, &libraries, &documents, &mainWindow); | 
   528 				const std::optional<ModelId> id = openModelFromPath(path, &state.libraries, &state.documents, &state.mainWindow); | 
|    518 				if (id.has_value()) { | 
   529 				if (id.has_value()) { | 
|    519 					openModelForEditing(id.value()); | 
   530 					openModelForEditing(&state, id.value()); | 
|    520 					addRecentlyOpenedFile(path); | 
   531 					addRecentlyOpenedFile(&state, path); | 
|    521 				} | 
   532 				} | 
|    522 			} | 
   533 			} | 
|    523 		} | 
   534 		} | 
|    524 	); | 
   535 	); | 
|    525 	QObject::connect(mainWindow.actionSettingsEditor, &QAction::triggered, [&defaultKeyboardShortcuts, &restoreSettings, &settingsChanged, &mainWindow]{ | 
   536 	QObject::connect(state.mainWindow.actionSettingsEditor, &QAction::triggered, [ | 
|    526 		if (mainWindow.mdiArea->findChildren<SettingsEditor*>().isEmpty()) { | 
   537 		&state, | 
|    527 			auto* const settingsEditor = createSubWindow<SettingsEditor>(mainWindow.mdiArea, defaultKeyboardShortcuts); | 
   538 		defaultKeyboardShortcuts = uiutilities::makeKeySequenceMap(uiutilities::collectActions(&state.mainWindow))] | 
|    528 			QObject::connect(&settingsChanged, &Signal::triggered, settingsEditor, &SettingsEditor::loadSettings); | 
   539 	{ | 
|    529 			QObject::connect(settingsEditor, &SettingsEditor::settingsChanged, restoreSettings); | 
   540 		if (state.mainWindow.mdiArea->findChildren<SettingsEditor*>().isEmpty()) | 
|         | 
   541 		{ | 
|         | 
   542 			auto* const settingsEditor = createSubWindow<SettingsEditor>(state.mainWindow.mdiArea, defaultKeyboardShortcuts); | 
|         | 
   543 			QObject::connect(settingsEditor, &SettingsEditor::settingsChanged, [&]{ | 
|         | 
   544 				restoreSettings(&state); | 
|         | 
   545 			}); | 
|    530 			settingsEditor->setAttribute(Qt::WA_DeleteOnClose); | 
   546 			settingsEditor->setAttribute(Qt::WA_DeleteOnClose); | 
|    531 			settingsEditor->show(); | 
   547 			settingsEditor->show(); | 
|    532 		} | 
   548 		} | 
|    533 	}); | 
   549 	}); | 
|    534 	QObject::connect(mainWindow.actionQuit, &QAction::triggered, &mainWindow, &QMainWindow::close); | 
   550 	QObject::connect(state.mainWindow.actionQuit, &QAction::triggered, &state.mainWindow, &QMainWindow::close); | 
|    535 #if 0 | 
   551 #if 0 | 
|    536 	QObject::connect(ui.actionAdjustGridToView, &QAction::triggered, [&]{ | 
   552 	QObject::connect(ui.actionAdjustGridToView, &QAction::triggered, [&]{ | 
|    537 		if (ModelData* data = currentModelData(&ui, &documents)) { | 
   553 		if (ModelData* data = currentModelData(&ui, &documents)) { | 
|    538 			adjustGridToView(data->canvas.get()); | 
   554 			adjustGridToView(data->canvas.get()); | 
|    539 		} | 
   555 		} | 
|    540 	}); | 
   556 	}); | 
|    541 #endif | 
   557 #endif | 
|    542 	QObject::connect(mainWindow.actionClose, &QAction::triggered, [&mainWindow, &documents]{ | 
   558 	QObject::connect(state.mainWindow.actionClose, &QAction::triggered, [&state]{ | 
|    543 		if (ModelData* data = currentModelData(&mainWindow, &documents)) { | 
   559 		if (ModelData* data = currentModelData(&state.mainWindow, &state.documents)) { | 
|    544 			// TODO | 
   560 			// TODO | 
|    545 		} | 
   561 		} | 
|    546 	}); | 
   562 	}); | 
|    547 	const auto save = [&addRecentlyOpenedFile, &mainWindow](DocumentManager* documents, ModelId modelId){ | 
   563 	QObject::connect(state.mainWindow.actionSaveAs, &QAction::triggered, [&state]{ | 
|    548 		QString error; | 
   564 		saveCurrentModelAs(&state); | 
|    549 		QTextStream errorStream{&error}; | 
   565 	}); | 
|    550 		const bool succeeded = documents->saveModel(modelId, errorStream); | 
   566 	QObject::connect(state.mainWindow.actionSave, &QAction::triggered, [&state] | 
|    551 		if (not succeeded) | 
   567 	{ | 
|    552 		{ | 
   568 		const std::optional<ModelId> modelId = findCurrentModelId(&state.mainWindow); | 
|    553 			QMessageBox::critical(&mainWindow, QObject::tr("Save error"), error); | 
        | 
|    554 		} | 
        | 
|    555 		else | 
        | 
|    556 		{ | 
        | 
|    557 			const QString* pathPtr = documents->modelPath(modelId); | 
        | 
|    558 			if (pathPtr != nullptr) { | 
        | 
|    559 				addRecentlyOpenedFile(*pathPtr); | 
        | 
|    560 			} | 
        | 
|    561 		} | 
        | 
|    562 	}; | 
        | 
|    563 	const auto actionSaveAs = [&documents, &libraries, &mainWindow, &save]{ | 
        | 
|    564 		const std::optional<ModelId> modelId = findCurrentModelId(&mainWindow); | 
        | 
|    565 		if (modelId.has_value()) | 
        | 
|    566 		{ | 
        | 
|    567 			const QString* pathPtr = documents.modelPath(*modelId); | 
        | 
|    568 			QString defaultPath = (pathPtr != nullptr) ? *pathPtr : ""; | 
        | 
|    569 			const QString newPath = QFileDialog::getSaveFileName( | 
        | 
|    570 				&mainWindow, | 
        | 
|    571 				QObject::tr("Save as…"), | 
        | 
|    572 				QFileInfo{defaultPath}.absoluteDir().path(),  | 
        | 
|    573 				QObject::tr("LDraw files (*.ldr *dat);;All files (*)") | 
        | 
|    574 			); | 
        | 
|    575 			if (not newPath.isEmpty()) { | 
        | 
|    576 				QString error; | 
        | 
|    577 				QTextStream errorStream{&error}; | 
        | 
|    578 				documents.setModelPath(*modelId, newPath, libraries, errorStream); | 
        | 
|    579 				QMdiSubWindow* const subWindow = mainWindow.mdiArea->currentSubWindow(); | 
        | 
|    580 				if (subWindow != nullptr) { | 
        | 
|    581 					subWindow->setWindowTitle(tabName(QFileInfo{newPath})); | 
        | 
|    582 				} | 
        | 
|    583 				save(&documents, *modelId); | 
        | 
|    584 			} | 
        | 
|    585 		} | 
        | 
|    586 	}; | 
        | 
|    587 	QObject::connect(mainWindow.actionSaveAs, &QAction::triggered, actionSaveAs); | 
        | 
|    588 	QObject::connect(mainWindow.actionSave, &QAction::triggered, [ | 
        | 
|    589 		&actionSaveAs, | 
        | 
|    590 		&documents, | 
        | 
|    591 		&save, | 
        | 
|    592 		&mainWindow] | 
        | 
|    593 	{ | 
        | 
|    594 		const std::optional<ModelId> modelId = findCurrentModelId(&mainWindow); | 
        | 
|    595 		if (modelId.has_value()) { | 
   569 		if (modelId.has_value()) { | 
|    596 			const QString* path = documents.modelPath(*modelId); | 
   570 			const QString* path = state.documents.modelPath(*modelId); | 
|    597 			if (path == nullptr or path->isEmpty()) { | 
   571 			if (path == nullptr or path->isEmpty()) { | 
|    598 				actionSaveAs(); | 
   572 				saveCurrentModelAs(&state); | 
|    599 			} | 
   573 			} | 
|    600 			else { | 
   574 			else { | 
|    601 				save(&documents, *modelId); | 
   575 				saveCurrentModel(&state, *modelId); | 
|    602 			} | 
   576 			} | 
|    603 		} | 
   577 		} | 
|    604 	}); | 
   578 	}); | 
|    605 	QObject::connect(mainWindow.actionDrawAxes, &QAction::triggered, [ | 
   579 	QObject::connect(state.mainWindow.actionDrawAxes, &QAction::triggered, [&state] | 
|    606 		&documents, | 
        | 
|    607 		&renderPreferences, | 
        | 
|    608 		&saveSettings, | 
        | 
|    609 		&mainWindow] | 
        | 
|    610 		(bool drawAxes) | 
   580 		(bool drawAxes) | 
|    611 	{ | 
   581 	{ | 
|    612 		renderPreferences.drawAxes = drawAxes; | 
   582 		state.renderPreferences.drawAxes = drawAxes; | 
|    613 		saveSettings(); | 
   583 		saveSettings(&state); | 
|    614 		updateRenderPreferences(&mainWindow, &renderPreferences, &documents); | 
   584 		updateRenderPreferences(&state.mainWindow, &state.renderPreferences, &state.documents); | 
|    615 	}); | 
   585 	}); | 
|    616 	QObject::connect(mainWindow.actionWireframe, &QAction::triggered, [ | 
   586 	QObject::connect(state.mainWindow.actionWireframe, &QAction::triggered, [&state] | 
|    617 		&documents, | 
        | 
|    618 		&renderPreferences, | 
        | 
|    619 		&saveSettings, | 
        | 
|    620 		&mainWindow] | 
        | 
|    621 		(bool enabled) | 
   587 		(bool enabled) | 
|    622 	{ | 
   588 	{ | 
|    623 		renderPreferences.wireframe = enabled; | 
   589 		state.renderPreferences.wireframe = enabled; | 
|    624 		saveSettings(); | 
   590 		saveSettings(&state); | 
|    625 		updateRenderPreferences(&mainWindow, &renderPreferences, &documents); | 
   591 		updateRenderPreferences(&state.mainWindow, &state.renderPreferences, &state.documents); | 
|    626 	}); | 
   592 	}); | 
|    627 	QObject::connect(&mainWindow, &MainWindow::renderStyleSelected, [ | 
   593 	QObject::connect(&state.mainWindow, &MainWindow::renderStyleSelected, [&state] | 
|    628 		&documents, | 
        | 
|    629 		&mainWindow, | 
        | 
|    630 		&renderPreferences, | 
        | 
|    631 		&saveSettings] | 
        | 
|    632 		(gl::RenderStyle newStyle) | 
   594 		(gl::RenderStyle newStyle) | 
|    633 	{ | 
   595 	{ | 
|    634 		renderPreferences.style = newStyle; | 
   596 		state.renderPreferences.style = newStyle; | 
|    635 		saveSettings(); | 
   597 		saveSettings(&state); | 
|    636 		updateRenderPreferences(&mainWindow, &renderPreferences, &documents); | 
   598 		updateRenderPreferences(&state.mainWindow, &state.renderPreferences, &state.documents); | 
|    637 	}); | 
   599 	}); | 
|    638 	const auto checkEditingModeAction = [&mainWindow, &documents](EditingMode mode) { | 
   600 	initializeTools(&state.mainWindow, &state.toolWidgets, &state.mainWindow); | 
|    639 		const bool hasDocument = currentModelData(&mainWindow, &documents) != nullptr; | 
   601 	for (QAction* action : state.mainWindow.editingModesToolBar->actions()) { | 
|    640 		for (QAction* action : mainWindow.editingModesToolBar->actions()) { | 
   602 		QObject::connect(action, &QAction::triggered, [action, &state] | 
|    641 			action->setEnabled(hasDocument); | 
   603 		{ | 
|    642 			action->setChecked(hasDocument and action->data().value<EditingMode>() == mode); | 
   604 			if (ModelData* data = currentModelData(&state.mainWindow, &state.documents)) | 
|    643 		} | 
   605 			{ | 
|    644 	}; | 
        | 
|    645 	initializeTools(&mainWindow, &toolWidgets, &mainWindow); | 
        | 
|    646 	for (QAction* action : mainWindow.editingModesToolBar->actions()) { | 
        | 
|    647 		QObject::connect(action, &QAction::triggered, [ | 
        | 
|    648 			action, | 
        | 
|    649 			&checkEditingModeAction, | 
        | 
|    650 			&documents, | 
        | 
|    651 			&mainWindow] | 
        | 
|    652 		{ | 
        | 
|    653 			if (ModelData* data = currentModelData(&mainWindow, &documents)) { | 
        | 
|    654 				const EditingMode mode = action->data().value<EditingMode>(); | 
   606 				const EditingMode mode = action->data().value<EditingMode>(); | 
|    655 				data->tools->setEditMode(mode); | 
   607 				data->tools->setEditMode(mode); | 
|    656 				checkEditingModeAction(mode); | 
   608 				checkEditingModeAction(&state, mode); | 
|    657 			} | 
   609 			} | 
|    658 		}); | 
   610 		}); | 
|    659 	} | 
   611 	} | 
|    660 	QObject::connect(mainWindow.mdiArea, &QMdiArea::subWindowActivated, [ | 
   612 	QObject::connect(state.mainWindow.mdiArea, &QMdiArea::subWindowActivated, | 
|    661 		&checkEditingModeAction, | 
   613 		[&state](QMdiSubWindow* subWindow) | 
|    662 		&documents, | 
        | 
|    663 		&mainWindow, | 
        | 
|    664 		&updateTitle] | 
        | 
|    665 		(QMdiSubWindow* subWindow) | 
        | 
|    666 	{ | 
   614 	{ | 
|    667 		ModelSubWindow* modelSubWindow = qobject_cast<ModelSubWindow*>(subWindow); | 
   615 		ModelSubWindow* modelSubWindow = qobject_cast<ModelSubWindow*>(subWindow); | 
|    668 		if (modelSubWindow != nullptr) { | 
   616 		if (modelSubWindow != nullptr) | 
|    669 			if (ModelData* data = documents.findPayload<ModelData>(modelSubWindow->modelId)) { | 
   617 		{ | 
|    670 				checkEditingModeAction(data->tools->currentEditingMode()); | 
   618 			if (ModelData* data = state.documents.findPayload<ModelData>(modelSubWindow->modelId)) | 
|    671 				mainWindow.modelEdit->setDocument(data->model); | 
   619 			{ | 
|    672 				mainWindow.modelEdit->setTextCursor(*data->textcursor); | 
   620 				checkEditingModeAction(&state, data->tools->currentEditingMode()); | 
|    673 				mainWindow.modelEdit->setFont(codeEditorFontFromSettings()); | 
   621 				state.mainWindow.modelEdit->setDocument(data->model); | 
|    674 			} | 
   622 				state.mainWindow.modelEdit->setTextCursor(*data->textcursor); | 
|    675 		} | 
   623 				state.mainWindow.modelEdit->setFont(codeEditorFontFromSettings()); | 
|    676 		else { | 
   624 			} | 
|    677 			checkEditingModeAction(EditingMode::SelectMode); | 
   625 		} | 
|    678 		} | 
   626 		else | 
|    679 		mainWindow.modelEdit->setEnabled(modelSubWindow != nullptr); | 
   627 		{ | 
|    680 		updateTitle(); | 
   628 			checkEditingModeAction(&state, EditingMode::SelectMode); | 
|    681 	}); | 
   629 		} | 
|    682 	mainWindow.messageLog->setModel(&messageLog); | 
   630 		state.mainWindow.modelEdit->setEnabled(modelSubWindow != nullptr); | 
|    683 	QObject::connect(&documents, &DocumentManager::message, &messageLog, &MessageLog::addMessage); | 
   631 	}); | 
|    684 	QObject::connect(&messageLog, &MessageLog::rowsAboutToBeInserted, [&mainWindow]{ | 
   632 	state.mainWindow.messageLog->setModel(&state.messageLog); | 
|    685 		const auto bar = mainWindow.messageLog->verticalScrollBar(); | 
   633 	QObject::connect(&state.documents, &DocumentManager::message, &state.messageLog, &MessageLog::addMessage); | 
|    686 		mainWindow.messageLog->setProperty("shouldAutoScroll", bar->value() == bar->maximum()); | 
   634 	QObject::connect(&state.messageLog, &MessageLog::rowsAboutToBeInserted, [&state]{ | 
|    687 	}); | 
   635 		const auto bar = state.mainWindow.messageLog->verticalScrollBar(); | 
|    688 	QObject::connect(&messageLog, &MessageLog::rowsInserted, [&mainWindow]{ | 
   636 		state.mainWindow.messageLog->setProperty("shouldAutoScroll", bar->value() == bar->maximum()); | 
|    689 		mainWindow.messageLog->resizeRowsToContents(); | 
   637 	}); | 
|    690 		if (mainWindow.messageLog->property("shouldAutoScroll").toBool()) { | 
   638 	QObject::connect(&state.messageLog, &MessageLog::rowsInserted, [&state]{ | 
|    691 			mainWindow.messageLog->scrollToBottom(); | 
   639 		state.mainWindow.messageLog->resizeRowsToContents(); | 
|         | 
   640 		if (state.mainWindow.messageLog->property("shouldAutoScroll").toBool()) { | 
|         | 
   641 			state.mainWindow.messageLog->scrollToBottom(); | 
|    692 		} | 
   642 		} | 
|    693 	}); | 
   643 	}); | 
|    694 	QObject::connect( | 
   644 	QObject::connect( | 
|    695 		toolWidgets.circleToolOptions, | 
   645 		state.toolWidgets.circleToolOptions, | 
|    696 		&CircleToolOptionsWidget::optionsChanged, | 
   646 		&CircleToolOptionsWidget::optionsChanged, | 
|    697 		[&mainWindow, &documents](const CircleToolOptions& options) { | 
   647 		[&state](const CircleToolOptions& options) { | 
|    698 			if (ModelData* data = currentModelData(&mainWindow, &documents)) { | 
   648 			if (ModelData* data = currentModelData(&state.mainWindow, &state.documents)) { | 
|    699 				data->tools->setCircleToolOptions(options); | 
   649 				data->tools->setCircleToolOptions(options); | 
|    700 			} | 
   650 			} | 
|    701 		}); | 
   651 		}); | 
|    702 	QObject::connect( | 
   652 	QObject::connect( | 
|    703 		mainWindow.actionMakeUnofficial, | 
   653 		state.mainWindow.actionMakeUnofficial, | 
|    704 		&QAction::triggered, | 
   654 		&QAction::triggered, | 
|    705 		[&documents, &mainWindow]{ | 
   655 		[&state]{ | 
|    706 			if (ModelData* data = currentModelData(&mainWindow, &documents)) { | 
   656 			if (ModelData* data = currentModelData(&state.mainWindow, &state.documents)) { | 
|    707 				QTextDocument* const model = data->model; | 
   657 				QTextDocument* const model = data->model; | 
|    708 				for (const ModelAction& action : ldraw::makeUnofficial(model)) { | 
   658 				for (const ModelAction& action : ldraw::makeUnofficial(model)) { | 
|    709 					executeAction(model, action); | 
   659 					executeAction(model, action); | 
|    710 				} | 
   660 				} | 
|    711 			} | 
   661 			} | 
|    712 		}); | 
   662 		}); | 
|    713 	QObject::connect(mainWindow.actionAboutQt, &QAction::triggered, &app, &QApplication::aboutQt); | 
   663 	QObject::connect(state.mainWindow.actionAboutQt, &QAction::triggered, &QApplication::aboutQt); | 
|    714 	QObject::connect( | 
   664 	QObject::connect( | 
|    715 		mainWindow.modelEdit, | 
   665 		state.mainWindow.modelEdit, | 
|    716 		&QPlainTextEdit::textChanged, | 
   666 		&QPlainTextEdit::textChanged, | 
|    717 		[&documents, &libraries, &mainWindow]{ | 
   667 		[&state]{ | 
|    718 			if (ModelData* data = currentModelData(&mainWindow, &documents)) { | 
   668 			if (ModelData* data = currentModelData(&state.mainWindow, &state.documents)) { | 
|    719 				documents.loadDependenciesForAllModels(libraries); | 
   669 				state.documents.loadDependenciesForAllModels(state.libraries); | 
|    720 				data->canvas->update(); | 
   670 				data->canvas->update(); | 
|    721 			} | 
   671 			} | 
|    722 		}); | 
   672 		}); | 
|    723 	QObject::connect( | 
   673 	QObject::connect( | 
|    724 		mainWindow.gridMatrix, | 
   674 		state.mainWindow.gridMatrix, | 
|    725 		&MatrixEditor::valueChanged, | 
   675 		&MatrixEditor::valueChanged, | 
|    726 		[&](const glm::mat4& newGridMatrix) | 
   676 		[&](const glm::mat4& newGridMatrix) | 
|    727 		{ | 
   677 		{ | 
|    728 			forEachModel(&documents, [&](const void*, const ModelData* data) | 
   678 			forEachModel(&state.documents, [&](const void*, const ModelData* data) | 
|    729 			{ | 
   679 			{ | 
|    730 				if (data->gridLayer != nullptr and data->tools != nullptr and data->canvas != nullptr) | 
   680 				if (data->gridLayer != nullptr and data->tools != nullptr and data->canvas != nullptr) | 
|    731 				{ | 
   681 				{ | 
|    732 					data->gridLayer->setGridMatrix(newGridMatrix); | 
   682 					data->gridLayer->setGridMatrix(newGridMatrix); | 
|    733 					data->tools->setGridMatrix(newGridMatrix); | 
   683 					data->tools->setGridMatrix(newGridMatrix); |