323 lines
11 KiB
C++
323 lines
11 KiB
C++
/**
|
|
* @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 <QToolBar>
|
|
#include <QToolButton>
|
|
#include <QComboBox>
|
|
#include <QLineEdit>
|
|
#include <QLabel>
|
|
#include <QTimer>
|
|
#include <QDockWidget>
|
|
#include <QMenu>
|
|
#include <QAction>
|
|
#include <QDialog>
|
|
#include <QFormLayout>
|
|
#include <QDialogButtonBox>
|
|
#include <QIntValidator>
|
|
#include <cmath>
|
|
|
|
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<int>(PlotLayout::kCount); i++) {
|
|
QAction* a = layMenu->addAction(QString::fromUtf8(names[i]));
|
|
PlotLayout l = static_cast<PlotLayout>(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<int>::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<uint16_t>(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<uint16_t>((dp > 0 && dp < 65536) ? dp : 0));
|
|
}
|
|
|
|
} /* namespace shq */
|