|
|
|
@@ -78,6 +78,49 @@ static double normalizeY(double raw, const VScale& vs) {
|
|
|
|
|
return (raw - vs.resolvedOffset) / vs.resolvedDiv + vs.screenPos;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Resolve the one scale every trace shares in unified mode: same rules as the
|
|
|
|
|
* per-signal version applied to the union of the plot — range takes the union
|
|
|
|
|
* of the declared ranges, auto fits the union of the data. */
|
|
|
|
|
static void resolveUnifiedVScale(VScale& vs,
|
|
|
|
|
const std::vector<PlotAssignment>& slots,
|
|
|
|
|
const std::vector<Source>& sources,
|
|
|
|
|
const std::vector<std::vector<double> >& vStore) {
|
|
|
|
|
if (vs.mode == 2) {
|
|
|
|
|
vs.resolvedDiv = std::max(vs.divValue, 1e-30);
|
|
|
|
|
vs.resolvedOffset = vs.offset;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
double mn = 1e300, mx = -1e300;
|
|
|
|
|
if (vs.mode == 1) {
|
|
|
|
|
for (const auto& a : slots) {
|
|
|
|
|
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& m = sources[a.sourceIdx].signals[a.signalIdx].meta;
|
|
|
|
|
if (!(m.rangeMax > m.rangeMin)) continue;
|
|
|
|
|
if (m.rangeMin < mn) mn = m.rangeMin;
|
|
|
|
|
if (m.rangeMax > mx) mx = m.rangeMax;
|
|
|
|
|
}
|
|
|
|
|
if (mx > mn) {
|
|
|
|
|
vs.resolvedDiv = std::max((mx - mn) / 8.0, 1e-30);
|
|
|
|
|
vs.resolvedOffset = (mn + mx) / 2.0;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
mn = 1e300; mx = -1e300; /* no usable range: fall through to auto */
|
|
|
|
|
}
|
|
|
|
|
for (const auto& vv : vStore) {
|
|
|
|
|
for (double v : vv) {
|
|
|
|
|
if (!std::isfinite(v)) continue;
|
|
|
|
|
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; }
|
|
|
|
|
vs.resolvedDiv = std::max((mx - mn) / 6.0, 1e-30);
|
|
|
|
|
vs.resolvedOffset = (mx + mn) / 2.0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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; } }
|
|
|
|
@@ -189,6 +232,50 @@ void PlotCanvas::drawMarker(QPainter& p, double cx, double cy, int marker, doubl
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @brief What the plot renders on the trigger-relative axis, if anything. */
|
|
|
|
|
struct TrigView {
|
|
|
|
|
bool rel = false; /* render against t - trig instead of wall clock */
|
|
|
|
|
bool fromCap = false; /* data comes from the capture frame, not the ring */
|
|
|
|
|
double trigT = 0.0;
|
|
|
|
|
double preS = 0.0;
|
|
|
|
|
double postS = 0.0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* Two ways to end up in trigger-relative time. Either a v2 capture frame has
|
|
|
|
|
* arrived, or a trigger has fired and its window is still filling. In the
|
|
|
|
|
* second case the hub sends nothing until the whole window has been produced —
|
|
|
|
|
* several seconds for a long window at a high rate — so the trace is drawn from
|
|
|
|
|
* the local rings onto the final axis, growing left to right. Filling wins
|
|
|
|
|
* over the last capture: once a new trigger fires the old waveform is history.
|
|
|
|
|
* A capture latches its own pre/post at fire time, so later edits in the
|
|
|
|
|
* trigger bar must not move the axis of a finished capture. */
|
|
|
|
|
static TrigView resolveTrigView(Hub* hub, const GlobalView* gv, bool paused) {
|
|
|
|
|
TrigView tv;
|
|
|
|
|
if (!gv->trigView) { return tv; }
|
|
|
|
|
|
|
|
|
|
const TriggerCfgState& t = hub->trigger();
|
|
|
|
|
if (!paused && t.status == "collecting" && t.hasTrigTime) {
|
|
|
|
|
tv.rel = true;
|
|
|
|
|
tv.trigT = t.trigTime;
|
|
|
|
|
/* Prefer the window the hub latched at fire time; the local config is
|
|
|
|
|
* only a fallback for hubs that do not report it, and may have been
|
|
|
|
|
* edited since the trigger fired. */
|
|
|
|
|
tv.preS = t.hasFiredWin ? t.firedPreS
|
|
|
|
|
: t.windowSec * t.prePercent * 0.01;
|
|
|
|
|
tv.postS = t.hasFiredWin ? t.firedPostS : t.windowSec - tv.preS;
|
|
|
|
|
return tv;
|
|
|
|
|
}
|
|
|
|
|
const CaptureFrame* cap = hub->capture();
|
|
|
|
|
if (cap != nullptr) {
|
|
|
|
|
tv.rel = true;
|
|
|
|
|
tv.fromCap = true;
|
|
|
|
|
tv.trigT = cap->trigTime;
|
|
|
|
|
tv.preS = cap->preSec;
|
|
|
|
|
tv.postS = cap->postSec;
|
|
|
|
|
}
|
|
|
|
|
return tv;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void PlotCanvas::paintEvent(QPaintEvent*) {
|
|
|
|
|
QPainter p(this);
|
|
|
|
|
p.setRenderHint(QPainter::Antialiasing, true);
|
|
|
|
@@ -205,13 +292,14 @@ void PlotCanvas::paintEvent(QPaintEvent*) {
|
|
|
|
|
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_;
|
|
|
|
|
|
|
|
|
|
const TrigView tv = resolveTrigView(hub, gv, paused);
|
|
|
|
|
const CaptureFrame* cap = hub->capture();
|
|
|
|
|
|
|
|
|
|
/* ── pause snapshot ─────────────────────────────────────────────────── */
|
|
|
|
|
auto& snap = w_->snap_;
|
|
|
|
|
if (paused) {
|
|
|
|
@@ -239,15 +327,15 @@ void PlotCanvas::paintEvent(QPaintEvent*) {
|
|
|
|
|
/* ── gather data per slot ───────────────────────────────────────────── */
|
|
|
|
|
std::vector<std::vector<double>> tStore(slots.size()), vStore(slots.size());
|
|
|
|
|
|
|
|
|
|
const bool liveHiRes = !trigView && live && !paused &&
|
|
|
|
|
const bool liveHiRes = !tv.rel && 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 &&
|
|
|
|
|
const bool useZoomData = !tv.rel && !paused && zc.valid &&
|
|
|
|
|
(liveHiRes ||
|
|
|
|
|
(!live && zc.t0 <= w_->plotXMin_ + 1e-9 && zc.t1 >= w_->plotXMax_ - 1e-9));
|
|
|
|
|
|
|
|
|
|
bool useHistData = !trigView && !paused && !live && hc.valid &&
|
|
|
|
|
bool useHistData = !tv.rel && !paused && !live && hc.valid &&
|
|
|
|
|
hc.t0 <= w_->plotXMin_ + 1e-9 && hc.t1 >= w_->plotXMax_ - 1e-9;
|
|
|
|
|
if (useHistData) {
|
|
|
|
|
bool any = false;
|
|
|
|
@@ -271,17 +359,25 @@ void PlotCanvas::paintEvent(QPaintEvent*) {
|
|
|
|
|
const auto& sig = sources[a.sourceIdx].signals[a.signalIdx];
|
|
|
|
|
const std::string key = hub->slotKey(a);
|
|
|
|
|
|
|
|
|
|
if (trigView) {
|
|
|
|
|
if (tv.fromCap) {
|
|
|
|
|
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);
|
|
|
|
|
tStore[si].push_back(cs.t[i] - tv.trigT);
|
|
|
|
|
vStore[si].push_back(cs.v[i]);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} else if (tv.rel) {
|
|
|
|
|
/* Filling: local ring, clipped to the (absolute) trigger window and
|
|
|
|
|
* shifted onto the trigger-relative axis. */
|
|
|
|
|
sig.buf.readRange(tv.trigT - tv.preS, tv.trigT + tv.postS,
|
|
|
|
|
tStore[si], vStore[si]);
|
|
|
|
|
for (size_t i = 0; i < tStore[si].size(); i++) {
|
|
|
|
|
tStore[si][i] -= tv.trigT;
|
|
|
|
|
}
|
|
|
|
|
} else if (useZoomData) {
|
|
|
|
|
bool found = false;
|
|
|
|
|
for (const auto& zs : zc.pts) {
|
|
|
|
@@ -302,11 +398,18 @@ void PlotCanvas::paintEvent(QPaintEvent*) {
|
|
|
|
|
resolveVScale(a, sig, vStore[si]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (w_->vMode_ == 3) {
|
|
|
|
|
resolveUnifiedVScale(w_->uniVS_, slots, sources, vStore);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── X range ────────────────────────────────────────────────────────── */
|
|
|
|
|
double xMin, xMax;
|
|
|
|
|
if (trigView) {
|
|
|
|
|
if (tv.rel) {
|
|
|
|
|
if (w_->trigZoomed_) { xMin = w_->plotXMin_; xMax = w_->plotXMax_; }
|
|
|
|
|
else { xMin = -cap->preSec; xMax = cap->postSec; }
|
|
|
|
|
/* Full window from the start, even while filling: a trace growing into
|
|
|
|
|
* a fixed axis reads as progress, whereas an axis that grows with the
|
|
|
|
|
* data shifts the whole trace every frame. */
|
|
|
|
|
else { xMin = -tv.preS; xMax = tv.postS; }
|
|
|
|
|
} else if (live && !paused) {
|
|
|
|
|
if (liveHiRes) { xMax = zc.t1; xMin = zc.t1 - gv->windowSec; }
|
|
|
|
|
else { xMax = wallNow; xMin = wallNow - gv->windowSec; }
|
|
|
|
@@ -319,19 +422,25 @@ void PlotCanvas::paintEvent(QPaintEvent*) {
|
|
|
|
|
/* ── 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();
|
|
|
|
|
/* Which scale labels the axis: the active signal's in normal mode, the one
|
|
|
|
|
* the whole plot shares in unified mode (where nothing has to be selected).
|
|
|
|
|
* Banded modes have no single scale, so they keep the plain division numbers. */
|
|
|
|
|
const VScale* axisVS = nullptr;
|
|
|
|
|
if (w_->vMode_ == 0 && w_->activeSlot_ >= 0 &&
|
|
|
|
|
w_->activeSlot_ < (int)slots.size()) {
|
|
|
|
|
axisVS = &slots[w_->activeSlot_].vs;
|
|
|
|
|
} else if (w_->vMode_ == 3) {
|
|
|
|
|
axisVS = &w_->uniVS_;
|
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
if (axisVS != nullptr) {
|
|
|
|
|
lbl = fmtVal(axisVS->resolvedOffset +
|
|
|
|
|
(d - axisVS->screenPos) * axisVS->resolvedDiv);
|
|
|
|
|
} else {
|
|
|
|
|
lbl = QString::number(d);
|
|
|
|
|
}
|
|
|
|
@@ -346,7 +455,7 @@ void PlotCanvas::paintEvent(QPaintEvent*) {
|
|
|
|
|
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);
|
|
|
|
|
QString xl = tv.rel ? 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);
|
|
|
|
@@ -396,8 +505,10 @@ void PlotCanvas::paintEvent(QPaintEvent*) {
|
|
|
|
|
if (w_->vMode_ == 1) bandNormalize(vDec, vNorm, myKi, nTraces, true);
|
|
|
|
|
else if (w_->vMode_ == 2) bandNormalize(vDec, vNorm, myKi, nTraces, a.vs.digitalInMixed);
|
|
|
|
|
else {
|
|
|
|
|
/* unified shares one scale, normal gives each trace its own */
|
|
|
|
|
const VScale& nvs = (w_->vMode_ == 3) ? w_->uniVS_ : a.vs;
|
|
|
|
|
vNorm.resize(nOut);
|
|
|
|
|
for (size_t k = 0; k < nOut; k++) vNorm[k] = normalizeY(vDec[k], a.vs);
|
|
|
|
|
for (size_t k = 0; k < nOut; k++) vNorm[k] = normalizeY(vDec[k], nvs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QColor c = sig.color;
|
|
|
|
@@ -423,7 +534,7 @@ void PlotCanvas::paintEvent(QPaintEvent*) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* trigger instant marker at t=0 */
|
|
|
|
|
if (trigView) {
|
|
|
|
|
if (tv.rel) {
|
|
|
|
|
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()));
|
|
|
|
@@ -476,8 +587,7 @@ 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;
|
|
|
|
|
const TrigView tv = resolveTrigView(hub, gv, w_->paused_);
|
|
|
|
|
bool& live = w_->live_;
|
|
|
|
|
|
|
|
|
|
double dy = e->angleDelta().y();
|
|
|
|
@@ -488,43 +598,51 @@ void PlotCanvas::wheelEvent(QWheelEvent* e) {
|
|
|
|
|
const double now = nowSec();
|
|
|
|
|
|
|
|
|
|
auto enterTrigZoom = [&]() {
|
|
|
|
|
if (trigView && !w_->trigZoomed_) {
|
|
|
|
|
w_->setStoredX(-cap->preSec, cap->postSec);
|
|
|
|
|
if (tv.rel && !w_->trigZoomed_) {
|
|
|
|
|
w_->setStoredX(-tv.preS, tv.postS);
|
|
|
|
|
w_->trigZoomed_ = true;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
auto xZoomStored = [&](double f) {
|
|
|
|
|
if (trigView) enterTrigZoom();
|
|
|
|
|
if (tv.rel) 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;
|
|
|
|
|
/* Seed manual from the resolved values so the gesture sticks. */
|
|
|
|
|
auto makeManual = [&](VScale& vs) {
|
|
|
|
|
if (vs.mode != 2) {
|
|
|
|
|
vs.divValue = std::max(vs.resolvedDiv, 1e-30);
|
|
|
|
|
vs.offset = vs.resolvedOffset;
|
|
|
|
|
vs.mode = 2;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/* Scroll adjusts the scale the axis is labelled with: the active signal's in
|
|
|
|
|
* normal mode, the plot's shared one in unified mode (nothing to select). */
|
|
|
|
|
VScale* wheelVS = nullptr;
|
|
|
|
|
if (w_->vMode_ == 3) {
|
|
|
|
|
wheelVS = &w_->uniVS_;
|
|
|
|
|
} else if (w_->activeSlot_ >= 0 && w_->activeSlot_ < (int)slots.size()) {
|
|
|
|
|
wheelVS = &slots[w_->activeSlot_].vs;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (ctrl) {
|
|
|
|
|
if (!trigView && live) gv->windowSec = std::clamp(gv->windowSec*factor, 1e-4, 3600.0);
|
|
|
|
|
if (!tv.rel && 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;
|
|
|
|
|
if (wheelVS != nullptr) {
|
|
|
|
|
makeManual(*wheelVS);
|
|
|
|
|
wheelVS->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);
|
|
|
|
|
if (wheelVS != nullptr) {
|
|
|
|
|
makeManual(*wheelVS);
|
|
|
|
|
wheelVS->divValue = std::max(wheelVS->divValue * factor, 1e-30);
|
|
|
|
|
} else {
|
|
|
|
|
if (!trigView && live) gv->windowSec = std::clamp(gv->windowSec*factor, 1e-4, 3600.0);
|
|
|
|
|
if (!tv.rel && live) gv->windowSec = std::clamp(gv->windowSec*factor, 1e-4, 3600.0);
|
|
|
|
|
else xZoomStored(factor);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -549,8 +667,7 @@ 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;
|
|
|
|
|
const TrigView tv = resolveTrigView(hub, gv, w_->paused_);
|
|
|
|
|
bool& live = w_->live_;
|
|
|
|
|
|
|
|
|
|
if (dragCursor_ != 0) {
|
|
|
|
@@ -560,11 +677,11 @@ void PlotCanvas::mouseMoveEvent(QMouseEvent* e) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (panning_) {
|
|
|
|
|
if (trigView && !w_->trigZoomed_) {
|
|
|
|
|
w_->setStoredX(-cap->preSec, cap->postSec);
|
|
|
|
|
if (tv.rel && !w_->trigZoomed_) {
|
|
|
|
|
w_->setStoredX(-tv.preS, tv.postS);
|
|
|
|
|
w_->trigZoomed_ = true;
|
|
|
|
|
}
|
|
|
|
|
if (!trigView && live) { w_->initPlotX(nowSec()); live = false; }
|
|
|
|
|
if (!tv.rel && live) { w_->initPlotX(nowSec()); live = false; }
|
|
|
|
|
double dxPix = e->pos().x() - lastPos_.x();
|
|
|
|
|
lastPos_ = e->pos();
|
|
|
|
|
double xRange = w_->plotXMax_ - w_->plotXMin_;
|
|
|
|
@@ -677,11 +794,10 @@ void PlotWidget::onCaptureReceived() {
|
|
|
|
|
void PlotWidget::tick() {
|
|
|
|
|
Hub* hub = hub_;
|
|
|
|
|
GlobalView* gv = gv_;
|
|
|
|
|
const CaptureFrame* cap = hub->capture();
|
|
|
|
|
const bool trigView = (cap != nullptr) && gv->trigView;
|
|
|
|
|
const TrigView tv = resolveTrigView(hub, gv, paused_);
|
|
|
|
|
const double now = nowSec();
|
|
|
|
|
|
|
|
|
|
if (!trigView && !paused_) {
|
|
|
|
|
if (!tv.rel && !paused_) {
|
|
|
|
|
std::string csv;
|
|
|
|
|
for (const auto& a : slots_) {
|
|
|
|
|
std::string k = hub->slotKey(a);
|
|
|
|
@@ -736,9 +852,13 @@ void PlotWidget::rebuildHeader() {
|
|
|
|
|
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)));
|
|
|
|
|
/* In unified mode every badge would repeat the same div value, which
|
|
|
|
|
* the header's Y-Scale button already shows — so show just the name. */
|
|
|
|
|
b->setText(vMode_ == 3
|
|
|
|
|
? QString::fromStdString(sig.meta.name)
|
|
|
|
|
: 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;
|
|
|
|
@@ -797,11 +917,17 @@ void PlotWidget::rebuildHeader() {
|
|
|
|
|
headerLay_->addWidget(fit);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* N / D / M */
|
|
|
|
|
const char* vl[3] = {"N", "D", "M"};
|
|
|
|
|
for (int vm = 0; vm < 3; vm++) {
|
|
|
|
|
/* N / U / D / M */
|
|
|
|
|
const char* vl[4] = {"N", "U", "D", "M"};
|
|
|
|
|
const char* vtip[4] = {"Normal: one vertical scale per signal",
|
|
|
|
|
"Unified: one vertical scale shared by every signal",
|
|
|
|
|
"Digital", "Mixed"};
|
|
|
|
|
const int vmode[4] = {0, 3, 1, 2};
|
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
|
|
|
const int vm = vmode[i];
|
|
|
|
|
auto* vb = new QToolButton(header_);
|
|
|
|
|
vb->setText(vl[vm]);
|
|
|
|
|
vb->setText(vl[i]);
|
|
|
|
|
vb->setToolTip(vtip[i]);
|
|
|
|
|
vb->setCheckable(true);
|
|
|
|
|
vb->setChecked(vMode_ == vm);
|
|
|
|
|
connect(vb, &QToolButton::clicked, this, [this, vm]() {
|
|
|
|
@@ -810,9 +936,57 @@ void PlotWidget::rebuildHeader() {
|
|
|
|
|
headerLay_->addWidget(vb);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* Unified mode's single scale belongs to the plot, not to any one signal,
|
|
|
|
|
* so it is edited from here rather than from a badge's context menu. */
|
|
|
|
|
if (vMode_ == 3) {
|
|
|
|
|
auto* yb = new QToolButton(header_);
|
|
|
|
|
yb->setText(QString("Y-Scale: %1/div").arg(fmtVal(uniVS_.resolvedDiv)));
|
|
|
|
|
yb->setToolTip("Vertical scale shared by every signal in this plot");
|
|
|
|
|
connect(yb, &QToolButton::clicked, this, [this, yb]() {
|
|
|
|
|
showUnifiedVScaleMenu(yb->mapToGlobal(QPoint(0, yb->height())));
|
|
|
|
|
});
|
|
|
|
|
headerLay_->addWidget(yb);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
headerLay_->addStretch(1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Populate @a vs with the Auto/Range/Manual entries driving @a evs. */
|
|
|
|
|
void PlotWidget::buildVScaleMenu(QMenu* vs, VScale& evs) {
|
|
|
|
|
const char* modes[] = {"Auto", "Range", "Manual"};
|
|
|
|
|
for (int mm = 0; mm < 3; mm++) {
|
|
|
|
|
QAction* act = vs->addAction(modes[mm]);
|
|
|
|
|
act->setCheckable(true); act->setChecked(evs.mode == mm);
|
|
|
|
|
connect(act, &QAction::triggered, this, [this, &evs, mm]() {
|
|
|
|
|
evs.mode = mm; rebuildHeader(); canvas_->update();
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
vs->addSeparator();
|
|
|
|
|
vs->addAction("Manual V/div…", [this, &evs]() {
|
|
|
|
|
bool ok; double v = QInputDialog::getDouble(this, "V/div", "Units per division",
|
|
|
|
|
evs.mode==2?evs.divValue:evs.resolvedDiv, -1e12, 1e12, 6, &ok);
|
|
|
|
|
if (ok) { evs.divValue = v; evs.mode = 2; rebuildHeader(); canvas_->update(); }
|
|
|
|
|
});
|
|
|
|
|
vs->addAction("Offset…", [this, &evs]() {
|
|
|
|
|
bool ok; double v = QInputDialog::getDouble(this, "Offset", "Center value",
|
|
|
|
|
evs.mode==2?evs.offset:evs.resolvedOffset, -1e12, 1e12, 6, &ok);
|
|
|
|
|
if (ok) { evs.offset = v; evs.mode = 2; rebuildHeader(); canvas_->update(); }
|
|
|
|
|
});
|
|
|
|
|
vs->addAction("Position (div)…", [this, &evs]() {
|
|
|
|
|
bool ok; double v = QInputDialog::getDouble(this, "Position", "Divisions from center",
|
|
|
|
|
evs.screenPos, -8, 8, 2, &ok);
|
|
|
|
|
if (ok) { evs.screenPos = v; canvas_->update(); }
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void PlotWidget::showUnifiedVScaleMenu(const QPoint& globalPos) {
|
|
|
|
|
QMenu m;
|
|
|
|
|
m.addAction("Y-Scale — all signals")->setEnabled(false);
|
|
|
|
|
m.addSeparator();
|
|
|
|
|
buildVScaleMenu(&m, uniVS_);
|
|
|
|
|
m.exec(globalPos);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void PlotWidget::showBadgeMenu(int slotIdx, const QPoint& globalPos) {
|
|
|
|
|
auto& sources = hub_->sources();
|
|
|
|
|
if (slotIdx < 0 || slotIdx >= (int)slots_.size()) return;
|
|
|
|
@@ -846,30 +1020,12 @@ void PlotWidget::showBadgeMenu(int slotIdx, const QPoint& globalPos) {
|
|
|
|
|
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(); });
|
|
|
|
|
/* In unified mode the plot has one scale for every trace, so it is edited
|
|
|
|
|
* from the header's Y-Scale button instead of from any one signal. */
|
|
|
|
|
if (vMode_ != 3) {
|
|
|
|
|
m.addSeparator();
|
|
|
|
|
buildVScaleMenu(m.addMenu("V-scale"), a.vs);
|
|
|
|
|
}
|
|
|
|
|
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", [&]() {
|
|
|
|
|