feat: a search's hits are marked on every row, and the panel says how many of them it is showing
This commit is contained in:
1 parent
8ee4effb70
commit
db81ab2700
3 files changed
+140
No files matched your search
@@ -31,6 +31,14 @@ constexpr plot::u32 kRulerBColor = 0x8FA8C8u;
|
||||
/// mistaken for something the user placed.
|
||||
constexpr plot::u32 kMeasureColor = 0x9BE8A0u;
|
||||
constexpr plot::u32 kTriggerColor = 0xFF5B5Bu;
|
||||
/// A found hit's colour: the trigger marker's claim ("the scope fired here") in a cooler hue,
|
||||
/// because a hit is the same condition found in *history* and the two are on screen together --
|
||||
/// the live marker's red must stay the one that means "this is the event the capture is about".
|
||||
constexpr plot::u32 kSearchColor = 0x7FB2FFu;
|
||||
/// The *selected* hit's colour: the one the arrows are standing on, and the one a pause was taken
|
||||
/// around. Sixteen identical lines with no way to tell which of them the readout is quoting is a
|
||||
/// picture that cannot be read, so the current hit is brighter than its neighbours.
|
||||
constexpr plot::u32 kSearchCurrentColor = 0xE8D37Fu;
|
||||
|
||||
/// The smallest difference a `TickFormat::Time` label can *write* at the magnitude `t` -- what
|
||||
/// the last digit of a label at that magnitude is worth. `format_time` writes three significant
|
||||
@@ -202,6 +210,15 @@ void RowSet::install(Row& r, int /*row_index*/, plot::Plot& p) {
|
||||
r.plot->set_cursor_stroke(r.measure_v[mi], kMeasureStrokeWidth,
|
||||
plot::Vec2{kMeasureDashOn, kMeasureDashOff});
|
||||
}
|
||||
// The search hits' pool, parked, created for the same reason the measurement pool above is:
|
||||
// `add_cursor()` only appends, so every mark a scan could ever want exists from the start.
|
||||
for (int m = 0; m < kSearchMarks; ++m) {
|
||||
const std::size_t mi = static_cast<std::size_t>(m);
|
||||
r.search_v[mi] = r.plot->add_cursor(kParked, plot::Color::hex(kSearchColor));
|
||||
r.plot->set_cursor_selectable(r.search_v[mi], false);
|
||||
r.plot->set_cursor_stroke(r.search_v[mi], kMeasureStrokeWidth,
|
||||
plot::Vec2{kMeasureDashOn, kMeasureDashOff});
|
||||
}
|
||||
r.trigger_marker = r.plot->add_cursor(kParked, plot::Color::hex(kTriggerColor));
|
||||
// A marker, not an instrument: it must sit exactly on the edge's time, so its
|
||||
// snap is Free and the controller never rounds it onto a sample.
|
||||
@@ -544,6 +561,32 @@ void RowSet::set_measure_marks(const std::vector<MeasureMark>& marks) {
|
||||
}
|
||||
}
|
||||
|
||||
void RowSet::set_search_marks(const std::vector<double>& xs, int current) {
|
||||
const int n = static_cast<int>(xs.size()) < kSearchMarks ? static_cast<int>(xs.size())
|
||||
: kSearchMarks;
|
||||
search_marks_placed_ = n;
|
||||
for (int i = 0; i < kMaxRows; ++i) {
|
||||
Row& r = row(i);
|
||||
if (!r.plot) continue;
|
||||
// Every row, not the hit's own row: a hit is an *instant on the shared x window*, so the
|
||||
// mark lines up down the column the way the vertical cursors do. There is no "the hit's
|
||||
// row" to pick -- the condition was found on the trigger's channel, which may not be
|
||||
// plotted at all.
|
||||
for (int m = 0; m < n; ++m) {
|
||||
const std::size_t mi = static_cast<std::size_t>(m);
|
||||
r.plot->set_cursor_x(r.search_v[mi], xs[mi], plot::CursorPlacement::Free);
|
||||
// The colour is re-set every frame rather than only when the selection moves: the pool is
|
||||
// re-ordered each frame as the view rolls, so slot `m` is not the same hit it was.
|
||||
r.plot->set_cursor_color(r.search_v[mi],
|
||||
plot::Color::hex(m == current ? kSearchCurrentColor
|
||||
: kSearchColor));
|
||||
}
|
||||
for (int m = n; m < kSearchMarks; ++m) {
|
||||
r.plot->set_cursor_x(r.search_v[static_cast<std::size_t>(m)], kParked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool RowSet::update_time_labels() {
|
||||
Row& first = row(0);
|
||||
if (!first.plot || !figure_) return false;
|
||||
@@ -697,6 +740,9 @@ bool RowSet::readouts_locked() const noexcept {
|
||||
for (const plot::u32 id : r.measure_v) {
|
||||
if (r.plot->cursor(id).selectable) return false;
|
||||
}
|
||||
for (const plot::u32 id : r.search_v) {
|
||||
if (r.plot->cursor(id).selectable) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,10 @@ class RowSet {
|
||||
/// measurements to be shown at once, and a fixed number because a `Plot` fixes its cursor
|
||||
/// set at construction.
|
||||
static constexpr int kMeasureMarks = 6;
|
||||
/// Search hits drawn at once, per row. A scan can return thousands; a plot cannot grow its
|
||||
/// cursor set, and sixteen lines over one window is already the most a reader can count. The
|
||||
/// panel states how many of the hits it is showing.
|
||||
static constexpr int kSearchMarks = 16;
|
||||
|
||||
RowSet();
|
||||
|
||||
@@ -112,6 +116,10 @@ class RowSet {
|
||||
std::array<plot::u32, kMeasureMarks> measure_h{};
|
||||
std::array<plot::u32, kMeasureMarks> measure_v{};
|
||||
|
||||
/// The search hits' marks: the same arrangement as the measurement pool above, vertical only
|
||||
/// (a hit is an instant, never a level).
|
||||
std::array<plot::u32, kSearchMarks> search_v{};
|
||||
|
||||
/// One marker per trace per cursor: where each vertical cursor crosses that trace's
|
||||
/// series, drawn only in `mix` mode. Two per slot because the pair crosses a trace
|
||||
/// at two places, and an annotation cannot be shared between them.
|
||||
@@ -255,6 +263,17 @@ class RowSet {
|
||||
/// measurement is drawn", as opposed to merely computed.
|
||||
int measure_marks_placed() const noexcept { return measure_marks_placed_; }
|
||||
|
||||
/// Places the search hits' marks and parks the rest of the pool. `xs` are **display** x values
|
||||
/// (the caller has already taken `stream_at_x0()` off a stream time), the same convention
|
||||
/// `set_measure_marks()` takes a time mark in, and the marks are unselectable for the same
|
||||
/// reason: a hit is where the scope found the condition, not something the pointer may move.
|
||||
///
|
||||
/// `current` indexes the one the navigation is standing on, which is drawn in its own colour, or
|
||||
/// is negative for none. The caller orders `xs`; this places them in the order it is given.
|
||||
void set_search_marks(const std::vector<double>& xs, int current);
|
||||
/// How many search marks the last call placed, which is `min(xs.size(), kSearchMarks)`.
|
||||
int search_marks_placed() const noexcept { return search_marks_placed_; }
|
||||
|
||||
/// Whether every instrument this app *computed* is out of the pointer's reach: the trigger
|
||||
/// marker and the measurement marks. They are readouts -- where the scope put the edge, what a
|
||||
/// measurement measured -- and a user must not be able to drag them; the report in the dump is
|
||||
@@ -479,6 +498,7 @@ class RowSet {
|
||||
/// back, one row's stale echo at a time.
|
||||
|
||||
int measure_marks_placed_ = 0;
|
||||
int search_marks_placed_ = 0;
|
||||
/// The one stream time each vertical cursor is at, whatever row's copy was last moved.
|
||||
double cursor_a_x_ = 0.0;
|
||||
double cursor_b_x_ = 0.0;
|
||||
|
||||
+74
@@ -32,6 +32,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -165,6 +166,9 @@ struct App {
|
||||
GtkWidget* search_cancel_button = nullptr;
|
||||
GtkWidget* search_status_label = nullptr;
|
||||
GtkWidget* search_pos_label = nullptr;
|
||||
/// "16 of 38 marks shown" -- the pool is smaller than a scan's result, and a display that is
|
||||
/// showing a sixth of what it found has to say so.
|
||||
GtkWidget* search_marks_label = nullptr;
|
||||
GtkWidget* search_prev_button = nullptr;
|
||||
GtkWidget* search_next_button = nullptr;
|
||||
GtkWidget* menu_button = nullptr;
|
||||
@@ -3341,6 +3345,62 @@ void place_measure_marks(App& app) {
|
||||
app.rows->set_measure_marks(app.measure_marks_display);
|
||||
}
|
||||
|
||||
/// Puts the search hits nearest the view on the plots, converting each from the stream seconds
|
||||
/// `BufferSearch` records to the x the axis is reading this frame -- the same conversion
|
||||
/// `place_measure_marks()` makes, and for the same reason. Called every frame: that is what
|
||||
/// makes a mark read-only and what keeps it on its own sample as a live view rolls.
|
||||
///
|
||||
/// Only the hits **inside the visible window** are candidates, nearest the view's centre first, so
|
||||
/// that a zoomed-out view with more hits than marks shows the middle of the cluster rather than an
|
||||
/// arbitrary end of it. The selected hit is placed first regardless, so stepping to a hit never
|
||||
/// leaves it unmarked, and it is the one drawn in the current colour.
|
||||
void place_search_marks(App& app) {
|
||||
std::vector<double> xs;
|
||||
int current = -1;
|
||||
std::size_t in_view = 0;
|
||||
const std::vector<double>& hits = app.search.hits();
|
||||
if (!hits.empty()) {
|
||||
const double s0 = app.model->stream_at_x0();
|
||||
const plot::Range view = app.rows->view_range();
|
||||
const double centre = 0.5 * (view.min + view.max);
|
||||
const int sel = app.search.cursor();
|
||||
|
||||
// Candidates: the display x of every hit in view, paired with its distance from the centre.
|
||||
std::vector<std::pair<double, double>> cand;
|
||||
for (std::size_t i = 0; i < hits.size(); ++i) {
|
||||
const double x = hits[i] - s0;
|
||||
if (x < view.min || x > view.max) continue;
|
||||
++in_view;
|
||||
if (sel >= 0 && i == static_cast<std::size_t>(sel)) continue; // placed first, below
|
||||
cand.push_back({std::fabs(x - centre), x});
|
||||
}
|
||||
std::sort(cand.begin(), cand.end());
|
||||
|
||||
const std::size_t cap = static_cast<std::size_t>(RowSet::kSearchMarks);
|
||||
xs.reserve(cap);
|
||||
if (sel >= 0 && sel < static_cast<int>(hits.size())) {
|
||||
current = 0;
|
||||
xs.push_back(hits[static_cast<std::size_t>(sel)] - s0);
|
||||
}
|
||||
for (std::size_t i = 0; i < cand.size() && xs.size() < cap; ++i) xs.push_back(cand[i].second);
|
||||
}
|
||||
if (app.search_marks_label != nullptr) {
|
||||
char text[128];
|
||||
if (hits.empty()) {
|
||||
std::snprintf(text, sizeof(text), " ");
|
||||
} else if (in_view > xs.size()) {
|
||||
// A truncated display says so: "16 of 38" with no qualifier reads as "there are 38 and you
|
||||
// are seeing 16 of them *everywhere*", which is not the claim.
|
||||
std::snprintf(text, sizeof(text), "%zu of %zu marks shown \xE2\x80\x94 zoom in to see the rest",
|
||||
xs.size(), in_view);
|
||||
} else {
|
||||
std::snprintf(text, sizeof(text), "%zu of %zu in view", xs.size(), hits.size());
|
||||
}
|
||||
gtk_label_set_text(GTK_LABEL(app.search_marks_label), text);
|
||||
}
|
||||
app.rows->set_search_marks(xs, current);
|
||||
}
|
||||
|
||||
/// Recomputes every measurement over the visible window and writes the results into the pane,
|
||||
/// collecting the marks the ones asking to be shown need.
|
||||
///
|
||||
@@ -6128,6 +6188,9 @@ gboolean tick(gpointer user) {
|
||||
app->rows->sync_cursors(cursor_drag_row);
|
||||
update_cursors_pane(*app);
|
||||
update_measures(*app, false);
|
||||
// Every frame, whether or not a scan is running: the marks are in stream seconds and the axis
|
||||
// is newest-relative, so a live window that has rolled since the last frame needs them moved.
|
||||
place_search_marks(*app);
|
||||
|
||||
// The first CONFIG names the signals, which is what the persisted layout is
|
||||
// written against: apply it once, here, and never again (a later CONFIG only
|
||||
@@ -7026,6 +7089,11 @@ GtkWidget* build_trigger_panel(App& app) {
|
||||
gtk_box_append(GTK_BOX(snav), app.search_next_button);
|
||||
gtk_box_append(GTK_BOX(sbox), snav);
|
||||
|
||||
app.search_marks_label = gtk_label_new(" ");
|
||||
gtk_widget_set_halign(app.search_marks_label, GTK_ALIGN_START);
|
||||
gtk_widget_add_css_class(app.search_marks_label, "dim-label");
|
||||
gtk_box_append(GTK_BOX(sbox), app.search_marks_label);
|
||||
|
||||
gtk_expander_set_child(GTK_EXPANDER(search_exp), sbox);
|
||||
gtk_box_append(GTK_BOX(box), search_exp);
|
||||
|
||||
@@ -10967,6 +11035,12 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
rows.measure_marks_placed(), app.measure_labels.size(),
|
||||
app.dbg_measures_pane_w, app.dbg_measures_pane_h);
|
||||
}
|
||||
// `locked` is re-reported here rather than only in the readouts line: this task added a
|
||||
// whole new cursor pool, and a pool created selectable is exactly the regression that a
|
||||
// readout-vs-instrument check is for.
|
||||
std::printf("[udpscope] search marks: placed=%d hits=%zu locked=%d\n",
|
||||
rows.search_marks_placed(), app.search.hits().size(),
|
||||
rows.readouts_locked() ? 1 : 0);
|
||||
std::printf("[udpscope] header: window_button_w=%d window_label_w=%d (bar_w=%d)\n",
|
||||
app.dbg_win_btn_w, app.dbg_win_label_w, app.dbg_bar_w);
|
||||
std::printf("[udpscope] layout: rows=%d window_s=%.6g", rows.row_count(),
|
||||
|
||||
Reference in new issue
Block a user