--- a/src/main.cpp Mon Jun 06 22:01:22 2022 +0300 +++ b/src/main.cpp Tue Jun 07 01:37:26 2022 +0300 @@ -1,24 +1,14 @@ -/* - * 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 <QApplication> +#include <QFileDialog> +#include <QMessageBox> +#include "mainwindow.h" +#include "ui_mainwindow.h" +#include "version.h" +#include "document.h" +#include "settingseditor/settingseditor.h" +#include "widgets/colorselectdialog.h" -#include <QApplication> -#include "mainwindow.h" -#include "version.h" +static const QDir LOCALE_DIR {":/locale"}; static void doQtRegistrations() { @@ -29,11 +19,408 @@ qRegisterMetaTypeStreamOperators<Libraries>("Libraries"); } +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 LibraryManager* libraries, + DocumentManager* documents, + QWidget* parent) +{ + QString errorString; + QTextStream errorStream{&errorString}; + const std::optional<ModelId> modelIdOpt = documents->openModel( + path, + errorStream, + DocumentManager::OpenType::ManuallyOpened); + if (modelIdOpt.has_value()) { + documents->loadDependenciesForModel(modelIdOpt.value(), path, *libraries, errorStream); + if (not errorString.isEmpty()) { + QMessageBox::warning( + parent, + QObject::tr("Problem loading references"), + errorString); + } + } + else { + QMessageBox::critical( + parent, + QObject::tr("Problem opening file"), + format(QObject::tr("Could not open %1: %2"), path, errorString)); + } + return modelIdOpt; +} + +static QString getOpenModelPath(QWidget* parent) +{ + return QFileDialog::getOpenFileName( + parent, + QObject::tr("Open model"), + "", + QObject::tr("LDraw models (*.ldr *.dat)")); +} + +static const QString localeCode(const QString& locale) +{ + if (locale == "system") { + return QLocale::system().name(); + } + else { + return locale; + } +} + +/** + * @brief Changes the application language to the specified language + */ +static void changeLanguage(const QString& locale, QTranslator* translator) +{ + if (not locale.isEmpty()) { + const QString localeCode = ::localeCode(locale); + QLocale::setDefault({localeCode}); + qApp->removeTranslator(translator); + const QString path = LOCALE_DIR.filePath(localeCode + ".qm"); + const bool loadSuccessful = translator->load(path); + if (loadSuccessful) + { + qApp->installTranslator(translator); + } + } +} + +/* +void MainWindow::handleDocumentSplitterChange() +{ + EditorTabWidget* currentDocument = this->currentDocument(); + if (currentDocument != nullptr) + { + this->documentSplitterState = currentDocument->saveSplitterState(); + for (int i = 0; i < this->ui->tabs->count(); i += 1) + { + EditorTabWidget* document = qobject_cast<EditorTabWidget*>(this->ui->tabs->widget(i)); + if (document != nullptr and document != currentDocument) + { + document->restoreSplitterState(this->documentSplitterState); + } + } + this->settings.setMainSplitterState(this->documentSplitterState); + } +} +*/ + +static EditorTabWidget* currentTabWidget(Ui_MainWindow* ui) +{ + return qobject_cast<EditorTabWidget*>(ui->tabs->currentWidget()); +}; + + +static void closeDocument(DocumentManager* documents, EditorTabWidget *document) +{ + std::optional<ModelId> modelId = documents->findIdForModel(document->model); + if (modelId.has_value()) { + documents->closeDocument(modelId.value()); + delete document; + } +} + +static void handleTabCloseButton(Ui_MainWindow* ui, DocumentManager* documents, int tabIndex) +{ + if (tabIndex >= 0 and tabIndex < ui->tabs->count()) { + EditorTabWidget* tab = qobject_cast<EditorTabWidget*>(ui->tabs->widget(tabIndex)); + if (tab != nullptr) { + closeDocument(documents, tab); + } + } +} + +static std::optional<ModelId> findCurrentModelId(Ui_MainWindow* ui, DocumentManager* documents) +{ + const EditorTabWidget* tab = currentTabWidget(ui); + if (tab != nullptr) { + return documents->findIdForModel(tab->model); + } + else { + return {}; + } +} + +/** + * @brief Updates the title of the main window so to contain the app's name + * and version as well as the open document name. + */ +static QString title() +{ + QString title = ::appName; + title += " "; + title += fullVersionString(); + return title; +} + +static ldraw::ColorTable loadColors(const LibraryManager* 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); + } +} + +static void updateRenderPreferences( + Ui_MainWindow* ui, + const gl::RenderPreferences* renderPreferences) +{ + for (int i = 0; i < ui->tabs->count(); i += 1) { + EditorTabWidget* tab = qobject_cast<EditorTabWidget*>(ui->tabs->widget(i)); + if (tab != nullptr) { + tab->canvas->setRenderPreferences(*renderPreferences); + } + } + for (auto data : ::renderStyleButtons) { + QAction* action = data.memberInstance(ui); + action->setChecked(renderPreferences->style == data.payload); + } + ui->actionDrawAxes->setChecked(renderPreferences->drawAxes); +}; + +static gl::RenderPreferences loadRenderPreferences(Configuration* settings) +{ + return gl::RenderPreferences{ + .style = static_cast<gl::RenderStyle>(settings->renderStyle()), + .mainColor = settings->mainColor(), + .backgroundColor = settings->backgroundColor(), + .selectedColor = settings->selectedColor(), + .lineThickness = settings->lineThickness(), + .lineAntiAliasing = settings->lineAntiAliasing(), + .drawAxes = settings->drawAxes(), + }; +} + int main(int argc, char *argv[]) { doQtRegistrations(); QApplication app{argc, argv}; - MainWindow mainwindow; - mainwindow.show(); - return app.exec(); + QMainWindow mainWindow; + Ui_MainWindow ui; + DocumentManager documents{&mainWindow}; + QString currentLanguage = "en"; + QTranslator translator{&mainWindow}; + Configuration settings; + LibraryManager libraries{&mainWindow}; + QByteArray documentSplitterState; + QStringList recentlyOpenedFiles; + ldraw::ColorTable colorTable; + gl::RenderPreferences renderPreferences; + ui.setupUi(&mainWindow); + const uiutilities::KeySequenceMap defaultKeyboardShortcuts = + uiutilities::makeKeySequenceMap(uiutilities::collectActions(&mainWindow)); + const auto saveSettings = [&]{ + settings.setMainWindowGeometry(mainWindow.saveGeometry()); + settings.setRecentFiles(recentlyOpenedFiles); + settings.setMainSplitterState(documentSplitterState); + settings.setRenderStyle(static_cast<int>(renderPreferences.style)); + settings.setDrawAxes(renderPreferences.drawAxes); + libraries.storeToSettings(&settings); + }; + 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); + } + ); + } + }; + const auto restoreSettings = [&]{ + recentlyOpenedFiles = settings.recentFiles(); + documentSplitterState = settings.mainSplitterState(); + renderPreferences = loadRenderPreferences(&settings); + changeLanguage(settings.locale(), &translator); + libraries.restoreFromSettings(&settings); + updateRecentlyOpenedDocumentsMenu(); + colorTable = loadColors(&libraries); + updateRenderPreferences(&ui, &renderPreferences); + ui.retranslateUi(&mainWindow); + }; + 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){ + EditorTabWidget* document = new EditorTabWidget{ + documents.getModelById(modelId), + &documents, + colorTable, + }; + document->canvas->setRenderPreferences(renderPreferences); + QObject::connect( + document, + &EditorTabWidget::newStatusText, + [&](const QString& newStatusText) { + mainWindow.statusBar()->showMessage(newStatusText); + }); + const QFileInfo fileInfo{*documents.modelPath(modelId)}; + ui.tabs->addTab(document, tabName(fileInfo)); + ui.tabs->setCurrentWidget(document); + document->restoreSplitterState(documentSplitterState); + }; + const auto newModel = [&openModelForEditing](DocumentManager* documents){ + openModelForEditing(documents->newModel()); + }; + QObject::connect(ui.actionNew, &QAction::triggered, [&newModel, &documents]{ + newModel(&documents); + }); + 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, [&]{ + SettingsEditor settingsEditor{&settings, defaultKeyboardShortcuts, &mainWindow}; + const int result = settingsEditor.exec(); + if (result == QDialog::Accepted) + { + restoreSettings(); + } + }); + QObject::connect(ui.actionQuit, &QAction::triggered, &mainWindow, &QMainWindow::close); + QObject::connect(ui.actionAdjustGridToView, &QAction::triggered, [&ui]{ + EditorTabWidget* tab = currentTabWidget(&ui); + if (tab != nullptr) + { + adjustGridToView(tab->canvas); + } + }); + QObject::connect(ui.actionClose, &QAction::triggered, [&ui, &documents]{ + EditorTabWidget* tab = currentTabWidget(&ui); + if (tab != nullptr) + { + closeDocument(&documents, tab); + } + }); + 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, &documents); + 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); + ui.tabs->setTabText(ui.tabs->currentIndex(), QFileInfo{newPath}.fileName()); + save(*modelId); + } + } + }; + QObject::connect(ui.actionSaveAs, &QAction::triggered, actionSaveAs); + QObject::connect(ui.actionSave, &QAction::triggered, [&]{ + if (currentTabWidget(&ui) != nullptr) { + const std::optional<ModelId> modelId = findCurrentModelId(&ui, &documents); + if (modelId.has_value()) { + const QString* path = documents.modelPath(*modelId); + if (path == nullptr or path->isEmpty()) { + actionSaveAs(); + } + else { + save(*modelId); + } + } + } + }); + QObject::connect(ui.tabs, &QTabWidget::tabCloseRequested, [&](int index){ + handleTabCloseButton(&ui, &documents, index); + }); + QObject::connect(ui.actionDrawAxes, &QAction::triggered, [&](bool drawAxes){ + renderPreferences.drawAxes = drawAxes; + saveSettings(); + updateRenderPreferences(&ui, &renderPreferences); + }); + for (auto data : ::renderStyleButtons) { + QAction* action = data.memberInstance(&ui); + QObject::connect(action, &QAction::triggered, [&, data]{ + renderPreferences.style = data.payload; + saveSettings(); + updateRenderPreferences(&ui, &renderPreferences); + }); + } + mainWindow.setWindowTitle(title()); + mainWindow.restoreGeometry(settings.mainWindowGeometry()); + restoreSettings(); + updateRenderPreferences(&ui, &renderPreferences); + newModel(&documents); + mainWindow.show(); + const int result = app.exec(); + saveSettings(); + return result; }