src/mainwindow.cpp

Wed, 09 Mar 2022 13:01:50 +0200

author
Teemu Piippo <teemu@hecknology.net>
date
Wed, 09 Mar 2022 13:01:50 +0200
changeset 173
8a3047468994
parent 170
9b655f6fe5a1
child 174
3016b494685c
permissions
-rw-r--r--

Fix performance issues in Model::find

/*
 *  LDForge: LDraw parts authoring CAD
 *  Copyright (C) 2013 - 2020 Teemu Piippo
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <QLabel>
#include <QVBoxLayout>
#include <QCloseEvent>
#include <QFileDialog>
#include <QMessageBox>
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "settingseditor/settingseditor.h"
#include "version.h"
#include "document.h"
#include "uiutilities.h"
#include "widgets/colorselectdialog.h"
#include "modeleditor.h"

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

class A : public QSettings
{
	using QSettings::QSettings;
};

MainWindow::MainWindow(QWidget *parent) :
	QMainWindow{parent},
	ui{std::make_unique<Ui_MainWindow>()},
	documents{this},
	settings{},
	libraries{this}
{
	this->ui->setupUi(this);
	defaultKeyboardShortcuts = uiutilities::makeKeySequenceMap(uiutilities::collectActions(this));
	connect(ui->actionNew, &QAction::triggered, this, &MainWindow::newModel);
	connect(ui->actionOpen, &QAction::triggered, this, &MainWindow::openModel);
	connect(ui->actionQuit, &QAction::triggered, this, &QMainWindow::close);
	connect(ui->actionSettingsEditor, &QAction::triggered, this, &MainWindow::runSettingsEditor);
	connect(ui->actionAdjustGridToView, &QAction::triggered, [&]()
	{
		if (this->currentDocument() != nullptr)
		{
			this->currentDocument()->adjustGridToView();
		}
	});
	connect(this->ui->actionSave, &QAction::triggered,
		this, &MainWindow::actionSave);
	connect(this->ui->actionSaveAs, &QAction::triggered,
		this, &MainWindow::actionSaveAs);
	connect(this->ui->actionClose, &QAction::triggered, this, &MainWindow::actionClose);
	connect(this->ui->actionDelete, &QAction::triggered, this, &MainWindow::actionDelete);
	connect(this->ui->tabs, &QTabWidget::tabCloseRequested, this, &MainWindow::handleTabCloseButton);
	for (auto data : ::renderStyleButtons)
	{
		QAction* action = data.memberInstance(this->ui.get());
		connect(action, &QAction::triggered, [this, data]()
		{
			this->setRenderStyle(data.payload);
		});
	}
	connect(this->ui->actionDrawAxes, &QAction::triggered, this, &MainWindow::setDrawAxes);
	this->updateTitle();
	this->restoreStartupSettings();
	this->restoreSettings();
	this->updateRenderPreferences();
	this->newModel();
}

// MainWindow needs a destructor even if it is empty because otherwise the destructor of the
// std::unique_ptr is resolved in the header file, where it will complain about Ui_MainWindow
// being incomplete.
MainWindow::~MainWindow()
{
}

void MainWindow::newModel()
{
	this->openModelForEditing(documents.newModel());
}

void MainWindow::openModel()
{
	const QString path = QFileDialog::getOpenFileName(
		this,
		tr("Open model"),
		"",
		tr("LDraw models (*.ldr *.dat)"));
	if (not path.isEmpty())
	{
		this->openModelFromPath(path);
	}
}

void MainWindow::openModelFromPath(const QString& path)
{
	QString errorString;
	QTextStream errorStream{&errorString};
	std::optional<ModelId> modelIdOpt = this->documents.openModel(
		path,
		errorStream,
		DocumentManager::OpenType::ManuallyOpened);
	if (modelIdOpt.has_value())
	{
		const ModelId modelId = modelIdOpt.value();
		this->documents.loadDependenciesForModel(modelId, path, this->libraries, errorStream);
		if (not errorString.isEmpty())
		{
			QMessageBox::warning(
				this,
				tr("Problem loading references"),
				errorString);
		}
		this->openModelForEditing(modelId);
		this->addRecentlyOpenedFile(path);
	}
	else
	{
		QMessageBox::critical(
			this,
			tr("Problem opening file"),
			utility::format(
				tr("Could not open %1: %2"),
				path,
				errorString));
	}
}

/**
 * @brief Changes the application language to the specified language
 * @param localeCode Code of the locale to translate to
 */
void MainWindow::changeLanguage(QString localeCode)
{
	if (not localeCode.isEmpty() and localeCode != this->currentLanguage)
	{
		this->currentLanguage = localeCode;
		if (localeCode == "system")
		{
			localeCode = QLocale::system().name();
		}
		QLocale::setDefault({localeCode});
		qApp->removeTranslator(&this->translator);
		const bool loadSuccessful = this->translator.load(pathToTranslation(localeCode));
		if (loadSuccessful)
		{
			qApp->installTranslator(&this->translator);
		}
	}
}

void MainWindow::addRecentlyOpenedFile(const QString& path)
{
	this->recentlyOpenedFiles.removeAll(path);
	this->recentlyOpenedFiles.insert(0, path);
	while (this->recentlyOpenedFiles.size() > maxRecentlyOpenedFiles)
	{
		this->recentlyOpenedFiles.removeLast();
	}
	this->saveSettings();
	this->updateRecentlyOpenedDocumentsMenu();
}

void MainWindow::openModelForEditing(const ModelId modelId)
{
	Document* document = new Document{this->documents.getModelById(modelId), &this->documents, this->colorTable};
	document->setRenderPreferences(this->renderPreferences);
	connect(document, &Document::newStatusText, [&](const QString& newStatusText)
	{
		this->statusBar()->showMessage(newStatusText);
	});
	const QFileInfo fileInfo{*this->documents.modelPath(modelId)};
	this->ui->tabs->addTab(document, fileInfo.baseName());
	this->ui->tabs->setCurrentWidget(document);
	document->restoreSplitterState(this->documentSplitterState);
}

void MainWindow::runSettingsEditor()
{
	SettingsEditor settingsEditor{&this->settings, this->defaultKeyboardShortcuts, this};
	const int result = settingsEditor.exec();
	if (result == QDialog::Accepted)
	{
		this->restoreSettings();
	}
}

Document* MainWindow::currentDocument()
{
	return qobject_cast<Document*>(this->ui->tabs->currentWidget());
}

void MainWindow::handleDocumentSplitterChange()
{
	Document* currentDocument = this->currentDocument();
	if (currentDocument != nullptr)
	{
		this->documentSplitterState = currentDocument->saveSplitterState();
		for (int i = 0; i < this->ui->tabs->count(); i += 1)
		{
			Document* document = qobject_cast<Document*>(this->ui->tabs->widget(i));
			if (document != nullptr and document != currentDocument)
			{
				document->restoreSplitterState(this->documentSplitterState);
			}
		}
		this->settings.setMainSplitterState(this->documentSplitterState);
	}
}

void MainWindow::updateRecentlyOpenedDocumentsMenu()
{
	this->ui->menuRecentFiles->clear();
	for (const QString& path : this->recentlyOpenedFiles)
	{
		QAction* action = new QAction{path, this};
		action->setData(path);
		this->ui->menuRecentFiles->addAction(action);
		connect(action, &QAction::triggered, this, &MainWindow::openRecentFile);
	}
}

void MainWindow::openRecentFile()
{
	QAction* action = qobject_cast<QAction*>(this->sender());
	if (action != nullptr)
	{
		const QString path = action->data().toString();
		this->openModelFromPath(path);
	}
}

void MainWindow::setRenderStyle(gl::RenderStyle renderStyle)
{
	this->renderPreferences.style = renderStyle;
	this->saveSettings();
	this->updateRenderPreferences();
}

void MainWindow::setDrawAxes(bool drawAxes)
{
	this->renderPreferences.drawAxes = drawAxes;
	this->saveSettings();
	this->updateRenderPreferences();
}

/**
 * @brief Handles the "Save" (Ctrl+S) action
 */
void MainWindow::actionSave()
{
	if (this->currentDocument() != nullptr)
	{
		const ModelId modelId = {0};
		const QString* path = this->documents.modelPath(modelId);
		if (path == nullptr or path->isEmpty())
		{
			this->actionSaveAs();
		}
		else
		{
			QString error;
			QTextStream errorStream{&error};
			const bool succeeded = this->documents.saveModel(modelId, errorStream);
			if (not succeeded)
			{
				QMessageBox::critical(this, tr("Save error"), error);
			}
			else
			{
				this->addRecentlyOpenedFile(*path);
			}
		}
	}
}

/**
 * @brief Handles the "Save as…" (Ctrl+Shift+S) action
 */
void MainWindow::actionSaveAs()
{
	if (this->currentDocument() != nullptr)
	{
		const ModelId modelId = {0};
		const QString* pathPtr = this->documents.modelPath(modelId);
		QString defaultPath = (pathPtr != nullptr) ? *pathPtr : "";
		const QString newPath = QFileDialog::getSaveFileName(
			this,
			tr("Save as…"),
			QFileInfo{defaultPath}.absoluteDir().path(), 
			tr("LDraw files (*.ldr *dat);;All files (*)")
		);
		if (not newPath.isEmpty())
		{
			QString error;
			QTextStream errorStream{&error};
			this->documents.setModelPath(modelId, newPath, this->libraries, errorStream);
			this->ui->tabs->setTabText(this->ui->tabs->currentIndex(), QFileInfo{newPath}.fileName());
			this->actionSave();
		}
	}
}

/**
 * @brief Handles the "Close" (Ctrl+W) action
 */
void MainWindow::actionClose()
{
	if (this->currentDocument() != nullptr)
	{
		this->closeDocument(this->currentDocument());
	}
}

/**
 * @brief Handles the "Delete" (Del) action
 */
void MainWindow::actionDelete()
{
	Document* document = this->currentDocument();
	if (document != nullptr)
	{
		std::unique_ptr<ModelEditor> modelEditor = document->editModel();
		QSet<ldraw::id_t> ids = document->selectedObjects(); // copy
		for (const ldraw::id_t id : ids)
		{
			const QModelIndex index = modelEditor->model().find(id);
			if (index.isValid())
			{
				modelEditor->remove(index.row());
			}
		}
	}
}

/**
 * @brief Removes the document at the specified tab index
 * @param index
 */
void MainWindow::handleTabCloseButton(int tabIndex)
{
	if (tabIndex >= 0 and tabIndex < this->ui->tabs->count())
	{
		Document* document = qobject_cast<Document*>(this->ui->tabs->widget(tabIndex));
		if (document != nullptr)
		{
			this->closeDocument(document);
		}
	}
}

/**
 * @brief Closes the specified document
 * @param document
 */
void MainWindow::closeDocument(Document *document)
{
	std::optional<ModelId> modelId = this->documents.findIdForModel(&document->getModel());
	if (modelId.has_value())
	{
		this->documents.closeDocument(modelId.value());
		delete document;
	}
}

void MainWindow::changeEvent(QEvent* event)
{
	if (event != nullptr)
	{
		switch (event->type())
		{
		case QEvent::LanguageChange:
			this->ui->retranslateUi(this);
			break;
		default:
			break;
		}
	}
	QMainWindow::changeEvent(event);
}

/**
 * @brief Handles closing the main window
 * @param event Event information
 */
void MainWindow::closeEvent(QCloseEvent* event)
{
	saveSettings();
	event->accept();
}

/**
 * @brief Updates the title of the main window so to contain the app's name
 * and version as well as the open document name.
 */
void MainWindow::updateTitle()
{
	QString title = ::appName;
	title += " ";
	title += fullVersionString();
	setWindowTitle(title);
}

void MainWindow::updateRenderPreferences()
{
	for (int i = 0; i < this->ui->tabs->count(); i += 1)
	{
		Document* document = qobject_cast<Document*>(this->ui->tabs->widget(i));
		if (document != nullptr)
		{
			document->setRenderPreferences(this->renderPreferences);
		}
	}
	for (auto data : ::renderStyleButtons)
	{
		QAction* action = data.memberInstance(this->ui.get());
		action->setChecked(this->renderPreferences.style == data.payload);
	}
	this->ui->actionDrawAxes->setChecked(this->renderPreferences.drawAxes);
}

/**
 * @brief Stores the settings of the main window, storing geometry, etc
 */
void MainWindow::saveSettings()
{
	this->settings.setMainWindowGeometry(this->saveGeometry());
	this->settings.setRecentFiles(this->recentlyOpenedFiles);
	this->settings.setMainSplitterState(this->documentSplitterState);
	this->settings.setRenderStyle(static_cast<int>(this->renderPreferences.style));
	this->settings.setDrawAxes(this->renderPreferences.drawAxes);
	this->libraries.storeToSettings(&this->settings);
}

void MainWindow::restoreStartupSettings()
{
	this->restoreGeometry(this->settings.mainWindowGeometry());
}

/**
 * @brief Restores saved settings relating to the main window
 */
void MainWindow::restoreSettings()
{
	this->recentlyOpenedFiles = this->settings.recentFiles();
	this->documentSplitterState = this->settings.mainSplitterState();
	this->renderPreferences.style = static_cast<gl::RenderStyle>(this->settings.renderStyle());
	this->renderPreferences.mainColor = this->settings.mainColor();
	this->renderPreferences.backgroundColor = this->settings.backgroundColor();
	this->renderPreferences.lineThickness = this->settings.lineThickness();
	this->renderPreferences.lineAntiAliasing = this->settings.lineAntiAliasing();
	this->renderPreferences.selectedColor = this->settings.selectedColor();
	this->renderPreferences.drawAxes = this->settings.drawAxes();
	const QString systemLocale = QLocale::system().name();
	const QVariant defaultLocale = this->settings.locale();
	this->changeLanguage(defaultLocale.toString());
	this->libraries.restoreFromSettings(&this->settings);
	this->updateRecentlyOpenedDocumentsMenu();
	this->loadColors();
	this->updateRenderPreferences();
}

QString MainWindow::pathToTranslation(const QString& localeCode)
{
	QDir dir {":/locale"};
	return dir.filePath(localeCode + ".qm");
}

void MainWindow::loadColors()
{
	QTextStream errors;
	this->colorTable = this->libraries.loadColorTable(errors);
}

void MainWindow::keyReleaseEvent(QKeyEvent* event)
{
	Document* document = this->currentDocument();
	if (document != nullptr)
	{
		document->handleKeyPress(event);
	}
}

mercurial