Wed, 09 Mar 2022 12:42:45 +0200
Render vertices as spheres
/* * 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); } }