/** * @file MainWindow.cpp */ #include "MainWindow.h" #include "PlotGrid.h" #include "SourceSidebar.h" #include "TriggerBar.h" #include "HistoryBar.h" #include "StatsDialog.h" #include "Theme.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace shq { MainWindow::MainWindow(const QString& host, uint16_t port, QWidget* parent) : QMainWindow(parent) { setWindowTitle("StreamHub Qt Client"); resize(1280, 800); /* ── Central plot grid ── */ grid_ = new PlotGrid(&hub_, &gv_, this); setCentralWidget(grid_); /* ── Source sidebar dock ── */ sidebar_ = new SourceSidebar(&hub_, this); sideDock_ = new QDockWidget("Sources", this); sideDock_->setWidget(sidebar_); sideDock_->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetClosable); addDockWidget(Qt::LeftDockWidgetArea, sideDock_); connect(sidebar_, &SourceSidebar::addSourceRequested, this, &MainWindow::openAddSourceDialog); /* ── Trigger bar (hidden until toggled) ── */ trigBar_ = new TriggerBar(&hub_, this); auto* trigTb = new QToolBar("Trigger", this); trigTb->setMovable(false); trigTb->addWidget(trigBar_); addToolBarBreak(); addToolBar(Qt::TopToolBarArea, trigTb); trigTb->setVisible(false); /* ── History bar (hidden until toggled) ── */ histBar_ = new HistoryBar(&hub_, grid_, this); auto* histTb = new QToolBar("History", this); histTb->setMovable(false); histTb->addWidget(histBar_); addToolBarBreak(); addToolBar(Qt::TopToolBarArea, histTb); histTb->setVisible(false); connect(histBar_, &HistoryBar::liveRequested, this, [this, histTb]() { histTb->setVisible(false); histBtn_->setChecked(false); }); /* Keep the QToolBar pointers reachable from slots. */ trigBtn_ = nullptr; /* set in buildToolbar */ buildToolbar(); /* Wire trigger/history toggle buttons to their bars. */ connect(trigBtn_, &QToolButton::toggled, trigTb, &QToolBar::setVisible); connect(histBtn_, &QToolButton::toggled, this, [this, histTb](bool on) { if (on) { const auto& hi = hub_.historyInfo(); if (hi.enabled && !hi.signals.empty()) { histTb->setVisible(true); histBar_->showAll(); } else { histBtn_->setChecked(false); } } else { histTb->setVisible(false); grid_->goLive(); } }); /* ── Hub signal wiring ── */ connect(&hub_, &Hub::connectedChanged, this, &MainWindow::onConnectedChanged); connect(&hub_, &Hub::sourcesChanged, this, &MainWindow::onSourcesChanged); connect(&hub_, &Hub::configChanged, this, &MainWindow::onConfigChanged); connect(&hub_, &Hub::statsChanged, this, &MainWindow::onStatsChanged); connect(&hub_, &Hub::triggerStateChanged, this, &MainWindow::onTriggerStateChanged); connect(&hub_, &Hub::historyInfoChanged, this, &MainWindow::onHistoryInfoChanged); /* ── 60 Hz repaint / refresh ── */ timer_ = new QTimer(this); timer_->setInterval(16); connect(timer_, &QTimer::timeout, this, &MainWindow::onTick); timer_->start(); hostEdit_->setText(host); portEdit_->setText(QString::number(port)); hub_.start(host, port); onConnectedChanged(false); } /* ── Toolbar construction ────────────────────────────────────────────────── */ void MainWindow::buildToolbar() { auto* tb = new QToolBar("Main", this); tb->setMovable(false); addToolBar(Qt::TopToolBarArea, tb); /* Sidebar toggle. */ auto* sideBtn = new QToolButton(this); sideBtn->setText("\u2630"); /* ≡ */ sideBtn->setCheckable(true); sideBtn->setChecked(true); sideBtn->setToolTip("Toggle sidebar"); connect(sideBtn, &QToolButton::toggled, sideDock_, &QDockWidget::setVisible); connect(sideDock_, &QDockWidget::visibilityChanged, sideBtn, &QToolButton::setChecked); tb->addWidget(sideBtn); /* Layout picker. */ auto* layoutBtn = new QToolButton(this); layoutBtn->setText("Layout"); layoutBtn->setPopupMode(QToolButton::InstantPopup); auto* layMenu = new QMenu(layoutBtn); const char* const* names = layoutNames(); for (int i = 0; i < static_cast(PlotLayout::kCount); i++) { QAction* a = layMenu->addAction(QString::fromUtf8(names[i])); PlotLayout l = static_cast(i); connect(a, &QAction::triggered, this, [this, l]() { grid_->setLayout(l); }); } layoutBtn->setMenu(layMenu); tb->addWidget(layoutBtn); /* Pause. */ pauseBtn_ = new QToolButton(this); pauseBtn_->setText("Pause"); pauseBtn_->setCheckable(true); connect(pauseBtn_, &QToolButton::clicked, this, &MainWindow::togglePause); tb->addWidget(pauseBtn_); /* Cursors. */ cursorBtn_ = new QToolButton(this); cursorBtn_->setText("Cursors"); cursorBtn_->setCheckable(true); cursorBtn_->setToolTip("Cursors A/B"); connect(cursorBtn_, &QToolButton::toggled, this, [this](bool on) { gv_.cursorsOn = on; }); tb->addWidget(cursorBtn_); /* Trigger toggle. */ trigBtn_ = new QToolButton(this); trigBtn_->setText("Trigger"); trigBtn_->setCheckable(true); connect(trigBtn_, &QToolButton::toggled, this, [this](bool on) { gv_.trigView = on; }); tb->addWidget(trigBtn_); /* History toggle. */ histBtn_ = new QToolButton(this); histBtn_->setText("History"); histBtn_->setCheckable(true); histBtn_->setEnabled(false); tb->addWidget(histBtn_); /* Window presets. */ tb->addWidget(new QLabel(" Win ", this)); winCombo_ = new QComboBox(this); winCombo_->setEditable(true); static const double kWin[] = {1, 5, 10, 30, 60}; static const char* kWinL[] = {"1 s", "5 s", "10 s", "30 s", "60 s"}; for (int i = 0; i < 5; i++) { winCombo_->addItem(kWinL[i], kWin[i]); } winCombo_->setCurrentIndex(2); /* 10 s */ connect(winCombo_, QOverload::of(&QComboBox::currentIndexChanged), this, [this](int i) { if (i >= 0) { gv_.windowSec = winCombo_->itemData(i).toDouble(); } }); connect(winCombo_->lineEdit(), &QLineEdit::editingFinished, this, [this]() { double v = winCombo_->currentText().split(' ').first().toDouble(); if (v >= 1e-4 && v <= 3600.0) { gv_.windowSec = v; } }); tb->addWidget(winCombo_); /* Spacer pushes the rest to the right. */ auto* spacer = new QWidget(this); spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); tb->addWidget(spacer); /* Stats. */ auto* statsBtn = new QToolButton(this); statsBtn->setText("Stats"); connect(statsBtn, &QToolButton::clicked, this, [this]() { if (!stats_) { stats_ = new StatsDialog(&hub_, this); } hub_.sendGetStats(); stats_->refresh(); stats_->show(); stats_->raise(); }); tb->addWidget(statsBtn); /* Connection. */ tb->addWidget(new QLabel(" Host ", this)); hostEdit_ = new QLineEdit(this); hostEdit_->setMaximumWidth(120); tb->addWidget(hostEdit_); portEdit_ = new QLineEdit(this); portEdit_->setMaximumWidth(60); portEdit_->setValidator(new QIntValidator(1, 65535, this)); tb->addWidget(portEdit_); auto* connBtn = new QToolButton(this); connBtn->setText("Connect"); connect(connBtn, &QToolButton::clicked, this, &MainWindow::doConnect); tb->addWidget(connBtn); ledLbl_ = new QLabel(" \u25cf Disconnected ", this); tb->addWidget(ledLbl_); } /* ── Slots ───────────────────────────────────────────────────────────────── */ void MainWindow::togglePause() { gv_.paused = pauseBtn_->isChecked(); pauseBtn_->setText(gv_.paused ? "Resume" : "Pause"); grid_->onPauseChanged(); } void MainWindow::toggleHistory() { /* handled inline via histBtn_ toggle */ } void MainWindow::doConnect() { QString host = hostEdit_->text().trimmed(); uint16_t port = static_cast(portEdit_->text().toUInt()); if (host.isEmpty() || port == 0) { return; } hub_.reconnect(host, port); } void MainWindow::onConnectedChanged(bool connected) { ledLbl_->setText(connected ? " \u25cf Connected " : " \u25cf Disconnected "); ledLbl_->setStyleSheet(connected ? "color:#a6e3a1; font-weight:bold;" : "color:#f38ba8; font-weight:bold;"); } void MainWindow::onSourcesChanged() { sidebar_->refresh(); trigBar_->refreshSignals(); grid_->onModelChanged(); } void MainWindow::onConfigChanged(const QString&) { sidebar_->refresh(); trigBar_->refreshSignals(); grid_->onModelChanged(); } void MainWindow::onStatsChanged() { if (stats_ && stats_->isVisible()) { stats_->refresh(); } } void MainWindow::onTriggerStateChanged() { trigBar_->onTriggerStateChanged(); } void MainWindow::onHistoryInfoChanged() { const auto& hi = hub_.historyInfo(); histBtn_->setEnabled(hi.enabled && !hi.signals.empty()); } void MainWindow::onTick() { grid_->tick(); if (histBtn_->isChecked()) { histBar_->updateReadout(); } } /* ── Add-source dialog ───────────────────────────────────────────────────── */ void MainWindow::openAddSourceDialog() { QDialog dlg(this); dlg.setWindowTitle("Add Source"); auto* form = new QFormLayout(&dlg); auto* label = new QLineEdit(&dlg); auto* host = new QLineEdit("127.0.0.1", &dlg); auto* port = new QLineEdit("44500", &dlg); port->setValidator(new QIntValidator(1, 65535, &dlg)); auto* mcast = new QLineEdit(&dlg); auto* dport = new QLineEdit("0", &dlg); dport->setValidator(new QIntValidator(0, 65535, &dlg)); form->addRow("Label", label); form->addRow("Host", host); form->addRow("Port", port); form->addRow("Multicast", mcast); form->addRow("Data Port", dport); auto* box = new QDialogButtonBox( QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &dlg); form->addRow(box); connect(box, &QDialogButtonBox::accepted, &dlg, &QDialog::accept); connect(box, &QDialogButtonBox::rejected, &dlg, &QDialog::reject); if (dlg.exec() != QDialog::Accepted) { return; } QString h = host->text().trimmed(); int p = port->text().toInt(); if (h.isEmpty() || p <= 0 || p >= 65536) { return; } std::string addr = h.toStdString() + ":" + std::to_string(p); int dp = dport->text().toInt(); hub_.sendAddSource(label->text().toStdString(), addr, mcast->text().toStdString(), static_cast((dp > 0 && dp < 65536) ? dp : 0)); } } /* namespace shq */