Thu, 15 Jun 2023 16:18:03 +0300
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; }