#include "MainWindow.h" #include "NodePalette.h" #include "PropertyPanel.h" #include "core/FlowSolver.h" #include "core/JsonIo.h" #include "scene/DiagramScene.h" #include "scene/GridView.h" #include #include #include #include #include #include #include #include #include namespace diag { MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { setWindowTitle(tr("PipeDiagram")); resize(1280, 800); TypeRegistry& registry = TypeRegistry::instance(); if (registry.nodeTypes().isEmpty()) { QString err; if (!registry.loadFromFile(QStringLiteral(":/config/nodetypes.json"), &err)) QMessageBox::warning(this, tr("Configuration"), tr("Failed to load node types: %1").arg(err)); } m_model = new NetworkModel(®istry, this); m_undoStack = new QUndoStack(this); m_scene = new DiagramScene(m_model, m_undoStack, this); m_view = new GridView(m_scene, this); setCentralWidget(m_view); m_palette = new NodePalette(®istry, this); auto* paletteDock = new QDockWidget(tr("Node palette"), this); paletteDock->setObjectName(QStringLiteral("paletteDock")); paletteDock->setWidget(m_palette); addDockWidget(Qt::LeftDockWidgetArea, paletteDock); m_properties = new PropertyPanel(this); m_properties->setScene(m_scene); auto* propsDock = new QDockWidget(tr("Properties"), this); propsDock->setObjectName(QStringLiteral("propsDock")); propsDock->setWidget(m_properties); addDockWidget(Qt::RightDockWidgetArea, propsDock); buildActions(); auto* cursorLabel = new QLabel; auto* zoomLabel = new QLabel(QStringLiteral("100%")); statusBar()->addPermanentWidget(cursorLabel); statusBar()->addPermanentWidget(zoomLabel); connect(m_view, &GridView::cursorMoved, this, [cursorLabel](QPointF p) { cursorLabel->setText(QStringLiteral("x: %1 y: %2").arg(int(p.x())).arg(int(p.y()))); }); connect(m_view, &GridView::zoomChanged, this, [zoomLabel](double z) { zoomLabel->setText(QStringLiteral("%1%").arg(int(z * 100))); }); connect(m_scene, &DiagramScene::connectionMessage, this, [this](const QString& text) { statusBar()->showMessage(text, 4000); }); m_settings.load(); applySettings(m_settings); } void MainWindow::buildActions() { auto* fileMenu = menuBar()->addMenu(tr("&File")); auto* toolbar = addToolBar(tr("Main")); toolbar->setObjectName(QStringLiteral("mainToolbar")); QAction* newAct = fileMenu->addAction(tr("&New"), QKeySequence::New, this, &MainWindow::newDocument); QAction* openAct = fileMenu->addAction(tr("&Open…"), QKeySequence::Open, this, &MainWindow::openDocument); QAction* saveAct = fileMenu->addAction(tr("&Save"), QKeySequence::Save, this, &MainWindow::saveDocument); fileMenu->addAction(tr("Save &As…"), QKeySequence::SaveAs, this, &MainWindow::saveDocumentAs); fileMenu->addAction(tr("Load Sa&mple"), this, &MainWindow::loadSample); fileMenu->addSeparator(); fileMenu->addAction(tr("&Quit"), QKeySequence::Quit, this, &QWidget::close); auto* editMenu = menuBar()->addMenu(tr("&Edit")); QAction* undoAct = m_undoStack->createUndoAction(this, tr("&Undo")); undoAct->setShortcut(QKeySequence::Undo); QAction* redoAct = m_undoStack->createRedoAction(this, tr("&Redo")); redoAct->setShortcut(QKeySequence::Redo); editMenu->addAction(undoAct); editMenu->addAction(redoAct); editMenu->addSeparator(); editMenu->addAction(tr("Cu&t"), QKeySequence::Cut, m_scene, &DiagramScene::cutSelection); editMenu->addAction(tr("&Copy"), QKeySequence::Copy, m_scene, &DiagramScene::copySelection); editMenu->addAction(tr("&Paste"), QKeySequence::Paste, m_scene, &DiagramScene::paste); QAction* deleteAct = editMenu->addAction(tr("&Delete"), QKeySequence::Delete, m_scene, &DiagramScene::deleteSelection); editMenu->addAction(tr("Select &All"), QKeySequence::SelectAll, m_scene, &DiagramScene::selectAll); editMenu->addSeparator(); editMenu->addAction(tr("Se&ttings…"), QKeySequence::Preferences, this, &MainWindow::showSettings); auto* viewMenu = menuBar()->addMenu(tr("&View")); viewMenu->addAction(tr("Zoom &In"), QKeySequence::ZoomIn, m_view, &GridView::zoomIn); viewMenu->addAction(tr("Zoom &Out"), QKeySequence::ZoomOut, m_view, &GridView::zoomOut); QAction* fitAct = viewMenu->addAction(tr("Zoom to &Fit"), QKeySequence(Qt::Key_F), m_view, &GridView::zoomToFit); viewMenu->addAction(tr("&Reset Zoom"), QKeySequence(Qt::CTRL | Qt::Key_0), m_view, &GridView::zoomReset); auto* simMenu = menuBar()->addMenu(tr("&Simulation")); m_simulateAction = simMenu->addAction(tr("&Run Flow Simulation")); m_simulateAction->setCheckable(true); m_simulateAction->setShortcut(QKeySequence(Qt::Key_F5)); connect(m_simulateAction, &QAction::toggled, this, &MainWindow::runSimulation); toolbar->addAction(newAct); toolbar->addAction(openAct); toolbar->addAction(saveAct); toolbar->addSeparator(); toolbar->addAction(undoAct); toolbar->addAction(redoAct); toolbar->addAction(deleteAct); toolbar->addSeparator(); toolbar->addAction(fitAct); toolbar->addAction(m_simulateAction); } void MainWindow::applySettings(const EditorSettings& settings) { m_settings = settings; m_scene->setRouteConfig(settings.route); m_scene->setTheme(Theme::make(settings.scheme)); settings.save(); } void MainWindow::showSettings() { SettingsDialog dialog(m_settings, this); if (dialog.exec() == QDialog::Accepted) applySettings(dialog.settings()); } bool MainWindow::maybeSave() { if (m_undoStack->isClean()) return true; const auto answer = QMessageBox::question( this, tr("Unsaved changes"), tr("The diagram has unsaved changes. Save them?"), QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); if (answer == QMessageBox::Save) { saveDocument(); return m_undoStack->isClean(); } return answer == QMessageBox::Discard; } void MainWindow::newDocument() { if (!maybeSave()) return; m_model->clear(); m_undoStack->clear(); m_currentFile.clear(); m_simulateAction->setChecked(false); } void MainWindow::openDocument() { if (!maybeSave()) return; const QString path = QFileDialog::getOpenFileName(this, tr("Open diagram"), {}, tr("Diagrams (*.json)")); if (path.isEmpty()) return; QString err; if (!JsonIo::load(m_model, path, &err)) { QMessageBox::warning(this, tr("Open failed"), err); return; } m_undoStack->clear(); m_currentFile = path; m_simulateAction->setChecked(false); m_view->zoomToFit(); } void MainWindow::saveDocument() { if (m_currentFile.isEmpty()) { saveDocumentAs(); return; } QString err; if (!JsonIo::save(*m_model, m_currentFile, &err)) QMessageBox::warning(this, tr("Save failed"), err); else m_undoStack->setClean(); } void MainWindow::saveDocumentAs() { const QString path = QFileDialog::getSaveFileName(this, tr("Save diagram"), {}, tr("Diagrams (*.json)")); if (path.isEmpty()) return; m_currentFile = path; saveDocument(); } void MainWindow::loadSample() { if (!maybeSave()) return; QString err; if (!JsonIo::load(m_model, QStringLiteral(":/samples/waterworks.json"), &err)) { QMessageBox::warning(this, tr("Sample"), err); return; } m_undoStack->clear(); m_currentFile.clear(); m_view->zoomToFit(); } void MainWindow::setSimulationRunning(bool running) { m_simulateAction->setChecked(running); } void MainWindow::runSimulation(bool enabled) { if (enabled) { // Mock calculation: distribute flow in every relation network, then // publish rates to the read-only flowRate property and animate. for (const Relation& rel : m_model->registry()->relations()) { const FlowResult result = FlowSolver::solve(*m_model, rel.id); if (!result.ok && !result.message.isEmpty()) statusBar()->showMessage(tr("%1: %2").arg(rel.name, result.message), 5000); FlowSolver::apply(*m_model, result); } statusBar()->showMessage(tr("Flow simulation running"), 3000); } else { statusBar()->showMessage(tr("Flow simulation stopped"), 3000); } m_scene->setFlowAnimationEnabled(enabled); } void MainWindow::closeEvent(QCloseEvent* event) { if (maybeSave()) event->accept(); else event->ignore(); } } // namespace diag