diagrams-fable-qt/src/app/MainWindow.cpp
Ilya Ashikhmin 730d0fa7a7 feat(app): add --simulate flag for headless demo captures
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:38:07 +02:00

264 lines
9.0 KiB
C++

#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 <QApplication>
#include <QCloseEvent>
#include <QDockWidget>
#include <QFileDialog>
#include <QLabel>
#include <QMenuBar>
#include <QMessageBox>
#include <QStatusBar>
#include <QToolBar>
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(&registry, 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(&registry, 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