Files
MARTe-Integrated-Components/Client/streamhub-qt/PlotWidget.cpp
2026-06-26 09:11:10 +02:00

886 lines
34 KiB
C++

/**
* @file PlotWidget.cpp
*/
#include "PlotWidget.h"
#include "Hub.h"
#include "Theme.h"
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QToolButton>
#include <QLabel>
#include <QPainter>
#include <QPainterPath>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QDragEnterEvent>
#include <QDropEvent>
#include <QMimeData>
#include <QMenu>
#include <QColorDialog>
#include <QInputDialog>
#include <QDateTime>
#include <QPolygonF>
#include <algorithm>
#include <cmath>
#include <cstdio>
using namespace StreamHubClient;
namespace shq {
static const char* kMimeSignal = "application/x-shq-signal";
static const size_t kMaxPush = 2400;
static const double kLiveHiResMaxWin = 600.0;
static double nowSec() {
return QDateTime::currentMSecsSinceEpoch() / 1000.0;
}
/* ── ported math helpers ─────────────────────────────────────────────────── */
static QString fmtVal(double v) {
if (!std::isfinite(v)) { return QStringLiteral("?"); }
char buf[32];
double a = std::fabs(v);
if (v == 0.0) std::snprintf(buf, sizeof(buf), "0");
else if (a >= 1e4 || a < 1e-3) std::snprintf(buf, sizeof(buf), "%.2e", v);
else std::snprintf(buf, sizeof(buf), "%.3g", v);
return QString::fromLatin1(buf);
}
static void resolveVScale(PlotAssignment& a, const SignalView& sig,
const std::vector<double>& rawV) {
if (a.vs.mode == 1) {
double rmin = sig.meta.rangeMin, rmax = sig.meta.rangeMax;
if (rmax > rmin) {
a.vs.resolvedDiv = std::max((rmax - rmin) / 8.0, 1e-30);
a.vs.resolvedOffset = (rmin + rmax) / 2.0;
return;
}
}
if (a.vs.mode == 2) {
a.vs.resolvedDiv = std::max(a.vs.divValue, 1e-30);
a.vs.resolvedOffset = a.vs.offset;
return;
}
double mn = 1e300, mx = -1e300;
for (double v : rawV) { if (v < mn) mn = v; if (v > mx) mx = v; }
if (!std::isfinite(mn) || mn > mx) { mn = -1.0; mx = 1.0; }
if (mn == mx) { mn -= 1.0; mx += 1.0; }
a.vs.resolvedDiv = std::max((mx - mn) / 6.0, 1e-30);
a.vs.resolvedOffset = (mx + mn) / 2.0;
}
static double normalizeY(double raw, const VScale& vs) {
return (raw - vs.resolvedOffset) / vs.resolvedDiv + vs.screenPos;
}
static bool dataMinMax(const std::vector<double>& v, double& mn, double& mx) {
mn = 1e300; mx = -1e300;
for (double x : v) { if (std::isfinite(x)) { if (x < mn) mn = x; if (x > mx) mx = x; } }
return mx >= mn;
}
static void bandNormalize(const std::vector<double>& raw, std::vector<double>& out,
int ki, int n, bool quantize) {
double bandH = 8.0 / std::max(n, 1);
double centerY = 4.0 - (ki + 0.5) * bandH;
double hi = centerY + bandH * 0.35;
double lo = centerY - bandH * 0.35;
double mn, mx;
bool haveMM = dataMinMax(raw, mn, mx);
out.resize(raw.size());
if (quantize) {
double thr = haveMM ? (mn + mx) / 2.0 : 0.5;
for (size_t i = 0; i < raw.size(); i++) {
double v = raw[i];
out[i] = !std::isfinite(v) ? NAN : (v >= thr ? hi : lo);
}
} else {
if (!haveMM) { mn = 0.0; mx = 1.0; }
if (mn == mx) { mn -= 1.0; mx += 1.0; }
double range = mx - mn, bandRange = hi - lo;
for (size_t i = 0; i < raw.size(); i++) {
double v = raw[i];
out[i] = !std::isfinite(v) ? NAN : lo + (v - mn) / range * bandRange;
}
}
}
static double sampleAt(const std::vector<double>& t, const std::vector<double>& v,
double x) {
if (t.empty() || v.size() != t.size()) { return NAN; }
auto it = std::lower_bound(t.begin(), t.end(), x);
size_t i = static_cast<size_t>(it - t.begin());
if (i >= t.size()) { i = t.size() - 1; }
else if (i > 0 && (x - t[i-1]) < (t[i] - x)) { i--; }
return v[i];
}
/*===========================================================================*/
/* PlotCanvas */
/*===========================================================================*/
class PlotCanvas : public QWidget {
public:
explicit PlotCanvas(PlotWidget* w) : QWidget(w), w_(w) {
setMinimumSize(80, 60);
setAcceptDrops(true);
setMouseTracking(true);
}
protected:
void paintEvent(QPaintEvent*) override;
void wheelEvent(QWheelEvent* e) override;
void mousePressEvent(QMouseEvent* e) override;
void mouseMoveEvent(QMouseEvent* e) override;
void mouseReleaseEvent(QMouseEvent* e) override;
void dragEnterEvent(QDragEnterEvent* e) override;
void dropEvent(QDropEvent* e) override;
private:
/* geometry of the plot drawing rect */
QRectF plotRect() const {
const double L = 56, R = 12, T = 8, B = 24;
return QRectF(L, T, std::max(10.0, width() - L - R),
std::max(10.0, height() - T - B));
}
double xToPx(double t, double xMin, double xMax, const QRectF& r) const {
if (xMax <= xMin) return r.left();
return r.left() + (t - xMin) / (xMax - xMin) * r.width();
}
double pxToX(double px, double xMin, double xMax, const QRectF& r) const {
if (r.width() <= 0) return xMin;
return xMin + (px - r.left()) / r.width() * (xMax - xMin);
}
double yToPx(double yDiv, const QRectF& r) const {
return r.top() + (4.05 - yDiv) / 8.10 * r.height();
}
void drawMarker(QPainter& p, double cx, double cy, int marker, double sz);
PlotWidget* w_;
/* interaction state */
bool panning_ = false;
int dragCursor_ = 0; /* 0 none, 1 A, 2 B */
QPoint lastPos_;
double curXMin_ = 0, curXMax_ = 1; /* last painted X range (for hit-test) */
};
void PlotCanvas::drawMarker(QPainter& p, double cx, double cy, int marker, double sz) {
double h = sz;
switch (marker) {
case 0: p.drawEllipse(QPointF(cx, cy), h, h); break; /* circle */
case 1: p.drawRect(QRectF(cx-h, cy-h, 2*h, 2*h)); break; /* square */
case 2: { QPolygonF d; d << QPointF(cx,cy-h)<<QPointF(cx+h,cy)
<<QPointF(cx,cy+h)<<QPointF(cx-h,cy); p.drawPolygon(d); } break;
case 3: { QPolygonF u; u<<QPointF(cx,cy-h)<<QPointF(cx+h,cy+h)
<<QPointF(cx-h,cy+h); p.drawPolygon(u); } break; /* up */
case 4: { QPolygonF d; d<<QPointF(cx,cy+h)<<QPointF(cx+h,cy-h)
<<QPointF(cx-h,cy-h); p.drawPolygon(d); } break; /* down */
case 7: p.drawLine(QPointF(cx-h,cy-h),QPointF(cx+h,cy+h));
p.drawLine(QPointF(cx-h,cy+h),QPointF(cx+h,cy-h)); break;/* cross */
case 8: p.drawLine(QPointF(cx-h,cy),QPointF(cx+h,cy));
p.drawLine(QPointF(cx,cy-h),QPointF(cx,cy+h)); break; /* plus */
default: p.drawEllipse(QPointF(cx, cy), h, h); break;
}
}
void PlotCanvas::paintEvent(QPaintEvent*) {
QPainter p(this);
p.setRenderHint(QPainter::Antialiasing, true);
Hub* hub = w_->hub_;
GlobalView* gv = w_->gv_;
auto& slots = w_->slots_;
auto& sources = hub->sources();
const QRectF r = plotRect();
const double wallNow = nowSec();
/* background + border */
p.fillRect(rect(), col::base());
p.fillRect(r, col::crust());
const CaptureFrame* cap = hub->capture();
const bool trigView = (cap != nullptr) && gv->trigView;
auto& zc = hub->zoomCache(w_->plotIdx_);
auto& hc = hub->histZoomCache(w_->plotIdx_);
const bool paused = w_->paused_;
bool& live = w_->live_;
/* ── pause snapshot ─────────────────────────────────────────────────── */
auto& snap = w_->snap_;
if (paused) {
if (!snap.valid) {
snap.keys.clear(); snap.t.clear(); snap.v.clear();
for (const auto& a : slots) {
std::vector<double> tt, vv;
if (a.sourceIdx >= 0 && a.sourceIdx < (int)sources.size() &&
a.signalIdx >= 0 &&
a.signalIdx < (int)sources[a.sourceIdx].signals.size()) {
sources[a.sourceIdx].signals[a.signalIdx]
.buf.readLast((size_t)hub->maxPoints(), tt, vv);
}
snap.keys.push_back(hub->slotKey(a));
snap.t.push_back(std::move(tt));
snap.v.push_back(std::move(vv));
}
snap.valid = true;
if (live) { w_->initPlotX(wallNow); live = false; }
}
} else if (snap.valid) {
snap.valid = false;
}
/* ── gather data per slot ───────────────────────────────────────────── */
std::vector<std::vector<double>> tStore(slots.size()), vStore(slots.size());
const bool liveHiRes = !trigView && live && !paused &&
gv->windowSec <= kLiveHiResMaxWin && zc.valid &&
(zc.t1 - zc.t0) >= gv->windowSec * 0.9 && (wallNow - zc.t1) < 3.0;
const bool useZoomData = !trigView && !paused && zc.valid &&
(liveHiRes ||
(!live && zc.t0 <= w_->plotXMin_ + 1e-9 && zc.t1 >= w_->plotXMax_ - 1e-9));
bool useHistData = !trigView && !paused && !live && hc.valid &&
hc.t0 <= w_->plotXMin_ + 1e-9 && hc.t1 >= w_->plotXMax_ - 1e-9;
if (useHistData) {
bool any = false;
for (const auto& hs : hc.pts) { if (!hs.t.empty()) { any = true; break; } }
if (!any) { useHistData = false; }
}
auto readBase = [&](size_t si, const SignalView& sig, const std::string& key) {
if (paused && snap.valid) {
for (size_t k = 0; k < snap.keys.size(); k++) {
if (snap.keys[k] == key) { tStore[si] = snap.t[k]; vStore[si] = snap.v[k]; return; }
}
}
sig.buf.readLast((size_t)hub->maxPoints(), tStore[si], vStore[si]);
};
for (size_t si = 0; si < slots.size(); si++) {
auto& a = slots[si];
if (a.sourceIdx < 0 || a.sourceIdx >= (int)sources.size()) continue;
if (a.signalIdx < 0 || a.signalIdx >= (int)sources[a.sourceIdx].signals.size()) continue;
const auto& sig = sources[a.sourceIdx].signals[a.signalIdx];
const std::string key = hub->slotKey(a);
if (trigView) {
for (const auto& cs : cap->signals) {
if (cs.key != key) continue;
size_t n = std::min(cs.t.size(), cs.v.size());
tStore[si].reserve(n); vStore[si].reserve(n);
for (size_t i = 0; i < n; i++) {
tStore[si].push_back(cs.t[i] - cap->trigTime);
vStore[si].push_back(cs.v[i]);
}
break;
}
} else if (useZoomData) {
bool found = false;
for (const auto& zs : zc.pts) {
if (zs.name != key) continue;
tStore[si] = zs.t; vStore[si] = zs.v; found = true; break;
}
if (!found) readBase(si, sig, key);
} else if (useHistData) {
bool found = false;
for (const auto& hs : hc.pts) {
if (hs.name != key) continue;
tStore[si] = hs.t; vStore[si] = hs.v; found = true; break;
}
if (!found) readBase(si, sig, key);
} else {
readBase(si, sig, key);
}
resolveVScale(a, sig, vStore[si]);
}
/* ── X range ────────────────────────────────────────────────────────── */
double xMin, xMax;
if (trigView) {
if (w_->trigZoomed_) { xMin = w_->plotXMin_; xMax = w_->plotXMax_; }
else { xMin = -cap->preSec; xMax = cap->postSec; }
} else if (live && !paused) {
if (liveHiRes) { xMax = zc.t1; xMin = zc.t1 - gv->windowSec; }
else { xMax = wallNow; xMin = wallNow - gv->windowSec; }
} else {
xMin = w_->plotXMin_; xMax = w_->plotXMax_;
}
if (xMax <= xMin) { xMax = xMin + 1.0; }
curXMin_ = xMin; curXMax_ = xMax;
/* ── grid + ticks ───────────────────────────────────────────────────── */
p.setPen(QPen(QColor(0x31,0x32,0x44,160), 1.0));
/* Y grid: 9 division lines */
const auto& av = (w_->vMode_ == 0 && w_->activeSlot_ >= 0 &&
w_->activeSlot_ < (int)slots.size())
? slots[w_->activeSlot_].vs : VScale();
p.setFont(QFont(font().family(), 8));
for (int d = -4; d <= 4; d++) {
double y = yToPx(d, r);
p.setPen(QPen(QColor(0x31,0x32,0x44, d==0?220:120), d==0?1.2:1.0));
p.drawLine(QPointF(r.left(), y), QPointF(r.right(), y));
QString lbl;
if (w_->vMode_ == 0 && w_->activeSlot_ >= 0 &&
w_->activeSlot_ < (int)slots.size()) {
double rawVal = av.resolvedOffset + (d - av.screenPos) * av.resolvedDiv;
lbl = fmtVal(rawVal);
} else {
lbl = QString::number(d);
}
p.setPen(QColor(0xa6,0xad,0xc8));
p.drawText(QRectF(0, y-8, r.left()-4, 16),
Qt::AlignRight|Qt::AlignVCenter, lbl);
}
/* X grid: 11 ticks */
for (int t = 0; t <= 10; t++) {
double xv = xMin + (xMax - xMin) * t / 10.0;
double x = xToPx(xv, xMin, xMax, r);
p.setPen(QPen(QColor(0x31,0x32,0x44,120), 1.0));
p.drawLine(QPointF(x, r.top()), QPointF(x, r.bottom()));
p.setPen(QColor(0xa6,0xad,0xc8));
QString xl = trigView ? fmtVal(xv) + "s" : QString::number(xv, 'f', 3);
int flags = (t==0?Qt::AlignLeft:(t==10?Qt::AlignRight:Qt::AlignHCenter))
| Qt::AlignTop;
p.drawText(QRectF(x-40, r.bottom()+2, 80, 14), flags, xl);
}
p.setPen(QPen(col::surface0(), 1.0));
p.drawRect(r);
/* clip drawing to the plot rect */
p.save();
p.setClipRect(r);
/* ── clip data to visible window, LTTB, normalize, draw ─────────────── */
int nTraces = 0;
for (const auto& a : slots) {
if (a.sourceIdx >= 0 && a.sourceIdx < (int)sources.size()) nTraces++;
}
std::vector<double> tDec, vDec, vNorm;
int ki = 0;
for (size_t si = 0; si < slots.size(); si++) {
auto& a = slots[si];
if (a.sourceIdx < 0 || a.sourceIdx >= (int)sources.size()) continue;
if (a.signalIdx < 0 || a.signalIdx >= (int)sources[a.sourceIdx].signals.size()) continue;
const auto& sig = sources[a.sourceIdx].signals[a.signalIdx];
int myKi = ki++;
if (!sig.visible) continue;
auto tv = tStore[si];
auto vv = vStore[si];
if (tv.empty()) continue;
/* clip to [xMin,xMax] keeping one sample each side */
auto lo = std::lower_bound(tv.begin(), tv.end(), xMin);
auto hi = std::upper_bound(lo, tv.end(), xMax);
if (lo != tv.begin()) --lo;
if (hi != tv.end()) ++hi;
size_t i0 = (size_t)(lo - tv.begin());
size_t i1 = (size_t)(hi - tv.begin());
if (i0 > 0 || i1 < tv.size()) {
tv = std::vector<double>(tv.begin()+i0, tv.begin()+i1);
vv = std::vector<double>(vv.begin()+i0, vv.begin()+i1);
}
if (tv.empty()) continue;
size_t nOut = LTTBDecimate(tv, vv, tDec, vDec, kMaxPush);
if (nOut == 0) continue;
if (w_->vMode_ == 1) bandNormalize(vDec, vNorm, myKi, nTraces, true);
else if (w_->vMode_ == 2) bandNormalize(vDec, vNorm, myKi, nTraces, a.vs.digitalInMixed);
else {
vNorm.resize(nOut);
for (size_t k = 0; k < nOut; k++) vNorm[k] = normalizeY(vDec[k], a.vs);
}
QColor c = sig.color;
p.setPen(QPen(c, sig.lineWidth));
QPolygonF poly;
poly.reserve((int)nOut);
for (size_t k = 0; k < nOut; k++) {
if (!std::isfinite(vNorm[k])) {
if (poly.size() > 1) p.drawPolyline(poly);
poly.clear();
continue;
}
poly << QPointF(xToPx(tDec[k], xMin, xMax, r), yToPx(vNorm[k], r));
}
if (poly.size() > 1) p.drawPolyline(poly);
else if (poly.size() == 1) p.drawPoint(poly.front());
if (sig.marker >= 0) {
p.setBrush(c);
for (const QPointF& pt : poly) drawMarker(p, pt.x(), pt.y(), sig.marker, 2.5);
p.setBrush(Qt::NoBrush);
}
}
/* trigger instant marker at t=0 */
if (trigView) {
double x = xToPx(0.0, xMin, xMax, r);
p.setPen(QPen(QColor(255,255,0,200), 1.5, Qt::DashLine));
p.drawLine(QPointF(x, r.top()), QPointF(x, r.bottom()));
}
/* cursors A/B */
if (gv->cursorsOn) {
bool aBeyond = (gv->cursorA < xMin || gv->cursorA > xMax);
bool bBeyond = (gv->cursorB < xMin || gv->cursorB > xMax);
if (aBeyond && bBeyond) {
gv->cursorA = xMin + (xMax-xMin)*0.25;
gv->cursorB = xMin + (xMax-xMin)*0.75;
}
double xa = xToPx(gv->cursorA, xMin, xMax, r);
double xb = xToPx(gv->cursorB, xMin, xMax, r);
p.setPen(QPen(col::peach(), 1.2));
p.drawLine(QPointF(xa, r.top()), QPointF(xa, r.bottom()));
p.drawText(QPointF(xa+2, r.top()+12), "A");
p.setPen(QPen(col::mauve(), 1.2));
p.drawLine(QPointF(xb, r.top()), QPointF(xb, r.bottom()));
p.drawText(QPointF(xb+2, r.top()+12), "B");
}
p.restore();
/* update cursor readout label + HIST flag handled in PlotWidget::tick */
w_->cursorLbl_->setVisible(gv->cursorsOn);
if (gv->cursorsOn) {
double dT = gv->cursorB - gv->cursorA;
QString s = QString("dT=%1s 1/dT=%2Hz")
.arg(fmtVal(dT))
.arg(fmtVal(dT != 0.0 ? 1.0/std::fabs(dT) : 0.0));
for (size_t si = 0; si < slots.size(); si++) {
auto& a = slots[si];
if (a.sourceIdx < 0 || a.sourceIdx >= (int)sources.size()) continue;
if (tStore[si].empty()) continue;
const auto& sig = sources[a.sourceIdx].signals[a.signalIdx];
double vA = sampleAt(tStore[si], vStore[si], gv->cursorA);
double vB = sampleAt(tStore[si], vStore[si], gv->cursorB);
s += QString(" %1: A=%2 B=%3 d=%4")
.arg(QString::fromStdString(sig.meta.name))
.arg(fmtVal(vA)).arg(fmtVal(vB)).arg(fmtVal(vB-vA));
}
w_->cursorLbl_->setText(s);
}
}
/* ── interaction ─────────────────────────────────────────────────────────── */
void PlotCanvas::wheelEvent(QWheelEvent* e) {
Hub* hub = w_->hub_;
GlobalView* gv = w_->gv_;
auto& slots = w_->slots_;
const CaptureFrame* cap = hub->capture();
const bool trigView = (cap != nullptr) && gv->trigView;
bool& live = w_->live_;
double dy = e->angleDelta().y();
if (dy == 0.0) { e->ignore(); return; }
const double factor = (dy > 0) ? 0.8 : 1.25;
const bool ctrl = e->modifiers() & Qt::ControlModifier;
const bool shift = e->modifiers() & Qt::ShiftModifier;
const double now = nowSec();
auto enterTrigZoom = [&]() {
if (trigView && !w_->trigZoomed_) {
w_->setStoredX(-cap->preSec, cap->postSec);
w_->trigZoomed_ = true;
}
};
auto xZoomStored = [&](double f) {
if (trigView) enterTrigZoom();
if (now - w_->lastHistPushMs_ > 0.6) { w_->pushZoomHist(); w_->lastHistPushMs_ = now; }
double cx = (w_->plotXMin_ + w_->plotXMax_) * 0.5;
double half = (w_->plotXMax_ - w_->plotXMin_) * 0.5 * f;
w_->setStoredX(cx - half, cx + half);
};
auto makeManual = [&](PlotAssignment& a) {
if (a.vs.mode != 2) {
a.vs.divValue = std::max(a.vs.resolvedDiv, 1e-30);
a.vs.offset = a.vs.resolvedOffset;
a.vs.mode = 2;
}
};
if (ctrl) {
if (!trigView && live) gv->windowSec = std::clamp(gv->windowSec*factor, 1e-4, 3600.0);
else xZoomStored(factor);
} else if (shift) {
if (w_->activeSlot_ >= 0 && w_->activeSlot_ < (int)slots.size()) {
auto& a = slots[w_->activeSlot_];
makeManual(a);
a.vs.screenPos += (dy > 0) ? 0.5 : -0.5;
}
} else {
if (w_->activeSlot_ >= 0 && w_->activeSlot_ < (int)slots.size()) {
auto& a = slots[w_->activeSlot_];
makeManual(a);
a.vs.divValue = std::max(a.vs.divValue * factor, 1e-30);
} else {
if (!trigView && live) gv->windowSec = std::clamp(gv->windowSec*factor, 1e-4, 3600.0);
else xZoomStored(factor);
}
}
update();
e->accept();
}
void PlotCanvas::mousePressEvent(QMouseEvent* e) {
GlobalView* gv = w_->gv_;
const QRectF r = plotRect();
lastPos_ = e->pos();
if (e->button() == Qt::LeftButton && gv->cursorsOn) {
double xa = xToPx(gv->cursorA, curXMin_, curXMax_, r);
double xb = xToPx(gv->cursorB, curXMin_, curXMax_, r);
if (std::fabs(e->pos().x() - xa) < 6) { dragCursor_ = 1; return; }
if (std::fabs(e->pos().x() - xb) < 6) { dragCursor_ = 2; return; }
}
if (e->button() == Qt::RightButton) { panning_ = true; }
}
void PlotCanvas::mouseMoveEvent(QMouseEvent* e) {
GlobalView* gv = w_->gv_;
Hub* hub = w_->hub_;
const QRectF r = plotRect();
const CaptureFrame* cap = hub->capture();
const bool trigView = (cap != nullptr) && gv->trigView;
bool& live = w_->live_;
if (dragCursor_ != 0) {
double x = pxToX(e->pos().x(), curXMin_, curXMax_, r);
if (dragCursor_ == 1) gv->cursorA = x; else gv->cursorB = x;
update();
return;
}
if (panning_) {
if (trigView && !w_->trigZoomed_) {
w_->setStoredX(-cap->preSec, cap->postSec);
w_->trigZoomed_ = true;
}
if (!trigView && live) { w_->initPlotX(nowSec()); live = false; }
double dxPix = e->pos().x() - lastPos_.x();
lastPos_ = e->pos();
double xRange = w_->plotXMax_ - w_->plotXMin_;
if (r.width() > 0) {
double dt = -dxPix / r.width() * xRange;
w_->setStoredX(w_->plotXMin_ + dt, w_->plotXMax_ + dt);
}
update();
}
}
void PlotCanvas::mouseReleaseEvent(QMouseEvent* e) {
if (e->button() == Qt::RightButton) panning_ = false;
if (e->button() == Qt::LeftButton) dragCursor_ = 0;
}
void PlotCanvas::dragEnterEvent(QDragEnterEvent* e) {
if (e->mimeData()->hasFormat(kMimeSignal)) e->acceptProposedAction();
}
void PlotCanvas::dropEvent(QDropEvent* e) {
QByteArray d = e->mimeData()->data(kMimeSignal);
if (d.size() >= (int)(2*sizeof(qint32))) {
const qint32* p = reinterpret_cast<const qint32*>(d.constData());
w_->addAssignment(p[0], p[1]);
e->acceptProposedAction();
}
}
/*===========================================================================*/
/* PlotWidget */
/*===========================================================================*/
PlotWidget::PlotWidget(Hub* hub, GlobalView* gv, int plotIdx, QWidget* parent)
: QWidget(parent), hub_(hub), gv_(gv), plotIdx_(plotIdx) {
auto* lay = new QVBoxLayout(this);
lay->setContentsMargins(2, 2, 2, 2);
lay->setSpacing(2);
header_ = new QWidget(this);
headerLay_ = new QHBoxLayout(header_);
headerLay_->setContentsMargins(2, 0, 2, 0);
headerLay_->setSpacing(3);
lay->addWidget(header_);
cursorLbl_ = new QLabel(this);
cursorLbl_->setStyleSheet("color:#fab387; font-size:10px;");
cursorLbl_->setVisible(false);
lay->addWidget(cursorLbl_);
canvas_ = new PlotCanvas(this);
lay->addWidget(canvas_, 1);
connect(hub_, &Hub::zoomReceived, this, &PlotWidget::onZoomReceived);
connect(hub_, &Hub::historyZoomReceived, this, &PlotWidget::onHistoryZoomReceived);
connect(hub_, &Hub::captureReceived, this, &PlotWidget::onCaptureReceived);
rebuildHeader();
}
void PlotWidget::addAssignment(int sourceIdx, int signalIdx) {
for (const auto& a : slots_) {
if (a.sourceIdx == sourceIdx && a.signalIdx == signalIdx) return;
}
PlotAssignment pa; pa.sourceIdx = sourceIdx; pa.signalIdx = signalIdx;
slots_.push_back(pa);
rebuildHeader();
canvas_->update();
}
void PlotWidget::onModelChanged() { rebuildHeader(); }
void PlotWidget::onPauseChanged() {
paused_ = gv_->paused;
canvas_->update();
}
void PlotWidget::setLive(bool live) {
live_ = live;
if (live) { zoomHist_.clear(); hub_->zoomCache(plotIdx_).valid = false; }
rebuildHeader();
canvas_->update();
}
void PlotWidget::setStoredX(double mn, double mx) {
if (mx > mn + 1e-12) { plotXMin_ = mn; plotXMax_ = mx; }
}
void PlotWidget::initPlotX(double tMax) {
plotXMin_ = tMax - gv_->windowSec;
plotXMax_ = tMax;
}
void PlotWidget::pushZoomHist() {
if (!zoomHist_.empty() &&
zoomHist_.back().first == plotXMin_ &&
zoomHist_.back().second == plotXMax_) return;
if (zoomHist_.size() >= 64) zoomHist_.erase(zoomHist_.begin());
zoomHist_.emplace_back(plotXMin_, plotXMax_);
}
void PlotWidget::onZoomReceived(int p) { if (p == plotIdx_) canvas_->update(); }
void PlotWidget::onHistoryZoomReceived(int p) { if (p == plotIdx_) canvas_->update(); }
void PlotWidget::onCaptureReceived() {
trigZoomed_ = false; zoomHist_.clear(); canvas_->update();
}
/* ── periodic tick: repaint + throttled hi-res/history zoom requests ─────── */
void PlotWidget::tick() {
Hub* hub = hub_;
GlobalView* gv = gv_;
const CaptureFrame* cap = hub->capture();
const bool trigView = (cap != nullptr) && gv->trigView;
const double now = nowSec();
if (!trigView && !paused_) {
std::string csv;
for (const auto& a : slots_) {
std::string k = hub->slotKey(a);
if (k.empty()) continue;
if (!csv.empty()) csv += ",";
csv += k;
}
auto& zc = hub->zoomCache(plotIdx_);
auto& hc = hub->histZoomCache(plotIdx_);
if (live_ && gv->windowSec <= kLiveHiResMaxWin) {
if (!csv.empty() && !zc.pending && now - lastLiveZoomMs_ > 0.25) {
hub->requestZoom(plotIdx_, now - gv->windowSec, now, csv);
lastLiveZoomMs_ = now;
}
} else if (!live_) {
double t0 = plotXMin_, t1 = plotXMax_;
if (t0 != lastT0_ || t1 != lastT1_) {
lastT0_ = t0; lastT1_ = t1; rangeChangedMs_ = now;
} else if (rangeChangedMs_ > 0.0 && now - rangeChangedMs_ > 0.35 && !csv.empty()) {
if (!zc.pending && !(zc.valid && zc.t0 == t0 && zc.t1 == t1))
hub->requestZoom(plotIdx_, t0, t1, csv);
const auto& hi = hub->historyInfo();
if (hi.enabled && !hc.pending && !(hc.valid && hc.t0 == t0 && hc.t1 == t1))
hub->requestHistoryZoom(plotIdx_, t0, t1, csv);
rangeChangedMs_ = 0.0;
}
}
}
canvas_->update();
}
/* ── header (badges + controls) ──────────────────────────────────────────── */
static void clearLayout(QLayout* lay) {
QLayoutItem* it;
while ((it = lay->takeAt(0)) != nullptr) {
if (it->widget()) it->widget()->deleteLater();
delete it;
}
}
void PlotWidget::rebuildHeader() {
clearLayout(headerLay_);
auto& sources = hub_->sources();
for (int i = 0; i < (int)slots_.size(); i++) {
auto& a = slots_[i];
if (a.sourceIdx < 0 || a.sourceIdx >= (int)sources.size()) continue;
if (a.signalIdx < 0 || a.signalIdx >= (int)sources[a.sourceIdx].signals.size()) continue;
const auto& sig = sources[a.sourceIdx].signals[a.signalIdx];
auto* b = new QToolButton(header_);
b->setCheckable(true);
b->setChecked(activeSlot_ == i);
b->setText(QString("%1 %2/div")
.arg(QString::fromStdString(sig.meta.name))
.arg(fmtVal(a.vs.resolvedDiv)));
QColor c = sig.color;
QString fg = (activeSlot_ == i) ? "#11111b" : "#11111b";
QColor bg = (activeSlot_ == i) ? col::blue() : c;
b->setStyleSheet(QString("QToolButton{background:%1;color:%2;border-radius:4px;"
"padding:1px 6px;font-size:10px;font-weight:bold;}")
.arg(bg.name()).arg(fg));
b->setContextMenuPolicy(Qt::CustomContextMenu);
connect(b, &QToolButton::clicked, this, [this, i]() {
activeSlot_ = (activeSlot_ == i) ? -1 : i;
rebuildHeader(); canvas_->update();
});
connect(b, &QWidget::customContextMenuRequested, this, [this, i, b](const QPoint& pos) {
showBadgeMenu(i, b->mapToGlobal(pos));
});
headerLay_->addWidget(b);
}
/* Live */
auto* liveBtn = new QToolButton(header_);
liveBtn->setText("Live");
liveBtn->setCheckable(true);
liveBtn->setChecked(live_);
connect(liveBtn, &QToolButton::clicked, this, [this]() {
setLive(true); paused_ = false;
});
headerLay_->addWidget(liveBtn);
if (!live_ || (gv_->trigView && trigZoomed_)) {
auto* back = new QToolButton(header_); back->setText("Back");
back->setEnabled(!zoomHist_.empty());
connect(back, &QToolButton::clicked, this, [this]() {
if (!zoomHist_.empty()) {
setStoredX(zoomHist_.back().first, zoomHist_.back().second);
zoomHist_.pop_back(); rebuildHeader(); canvas_->update();
}
});
headerLay_->addWidget(back);
auto* fit = new QToolButton(header_);
fit->setText(gv_->trigView ? "Reset" : "Fit");
connect(fit, &QToolButton::clicked, this, [this]() {
if (gv_->trigView) { trigZoomed_ = false; zoomHist_.clear(); }
else {
double mn = 1e300, mx = -1e300;
auto& sources = hub_->sources();
for (auto& a : slots_) {
if (a.sourceIdx < 0 || a.sourceIdx >= (int)sources.size()) continue;
std::vector<double> tt, vv;
sources[a.sourceIdx].signals[a.signalIdx].buf.readLast(hub_->maxPoints(), tt, vv);
if (!tt.empty()) { mn = std::min(mn, tt.front()); mx = std::max(mx, tt.back()); }
}
if (mx > mn) { pushZoomHist(); setStoredX(mn, mx); }
}
rebuildHeader(); canvas_->update();
});
headerLay_->addWidget(fit);
}
/* N / D / M */
const char* vl[3] = {"N", "D", "M"};
for (int vm = 0; vm < 3; vm++) {
auto* vb = new QToolButton(header_);
vb->setText(vl[vm]);
vb->setCheckable(true);
vb->setChecked(vMode_ == vm);
connect(vb, &QToolButton::clicked, this, [this, vm]() {
vMode_ = vm; rebuildHeader(); canvas_->update();
});
headerLay_->addWidget(vb);
}
headerLay_->addStretch(1);
}
void PlotWidget::showBadgeMenu(int slotIdx, const QPoint& globalPos) {
auto& sources = hub_->sources();
if (slotIdx < 0 || slotIdx >= (int)slots_.size()) return;
auto& a = slots_[slotIdx];
if (a.sourceIdx < 0 || a.sourceIdx >= (int)sources.size()) return;
auto& sig = sources[a.sourceIdx].signals[a.signalIdx];
QMenu m;
m.addAction(QString::fromStdString(sig.meta.name))->setEnabled(false);
m.addSeparator();
m.addAction("Color…", [&]() {
QColor c = QColorDialog::getColor(sig.color, this, "Trace color");
if (c.isValid()) { sig.color = c; rebuildHeader(); canvas_->update(); }
});
QMenu* wm = m.addMenu("Width");
for (double w : {0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0}) {
wm->addAction(QString::number(w, 'f', 1), [&, w]() { sig.lineWidth = w; canvas_->update(); });
}
QMenu* mk = m.addMenu("Marker");
const char* mn[] = {"None","Circle","Square","Diamond","Up","Down","Cross","Plus"};
const int mv[] = {-1, 0, 1, 2, 3, 4, 7, 8};
for (int j = 0; j < 8; j++) {
int val = mv[j];
mk->addAction(mn[j], [&, val]() { sig.marker = val; canvas_->update(); });
}
if (vMode_ == 2) {
QAction* dg = m.addAction("Digital (mixed)");
dg->setCheckable(true); dg->setChecked(a.vs.digitalInMixed);
connect(dg, &QAction::toggled, this, [&](bool on){ a.vs.digitalInMixed = on; canvas_->update(); });
}
m.addSeparator();
QMenu* vs = m.addMenu("V-scale");
const char* modes[] = {"Auto", "Range", "Manual"};
for (int mm = 0; mm < 3; mm++) {
QAction* act = vs->addAction(modes[mm]);
act->setCheckable(true); act->setChecked(a.vs.mode == mm);
connect(act, &QAction::triggered, this, [&, mm]() { a.vs.mode = mm; rebuildHeader(); canvas_->update(); });
}
vs->addSeparator();
vs->addAction("Manual V/div…", [&]() {
bool ok; double v = QInputDialog::getDouble(this, "V/div", "Units per division",
a.vs.mode==2?a.vs.divValue:a.vs.resolvedDiv, -1e12, 1e12, 6, &ok);
if (ok) { a.vs.divValue = v; a.vs.mode = 2; rebuildHeader(); canvas_->update(); }
});
vs->addAction("Offset…", [&]() {
bool ok; double v = QInputDialog::getDouble(this, "Offset", "Center value",
a.vs.mode==2?a.vs.offset:a.vs.resolvedOffset, -1e12, 1e12, 6, &ok);
if (ok) { a.vs.offset = v; a.vs.mode = 2; rebuildHeader(); canvas_->update(); }
});
vs->addAction("Position (div)…", [&]() {
bool ok; double v = QInputDialog::getDouble(this, "Position", "Divisions from center",
a.vs.screenPos, -8, 8, 2, &ok);
if (ok) { a.vs.screenPos = v; canvas_->update(); }
});
m.addSeparator();
m.addAction("Remove from plot", [&]() {
if (activeSlot_ == slotIdx) activeSlot_ = -1;
else if (activeSlot_ > slotIdx) activeSlot_--;
slots_.erase(slots_.begin() + slotIdx);
rebuildHeader(); canvas_->update();
});
m.exec(globalPos);
}
} /* namespace shq */