scope windows: the same instruments as the main window

A scope or detached window drew its traces and nothing else: no cursors, no gate band, no trigger
marker, no measurement marks. They are not per-window state to be recreated -- the cursors are one
pair of times, the gate one band, the trigger one instant -- so the main window's row set stays the
**owner** and every window is written from it.

`sync_scope_instruments()` runs in the main tick after the main's own `sync_cursors()`/
`sync_gate()`: a gesture inside a scope window is read back from *that* window's controllers first
(those two calls only adopt while their own plot holds the grab, which is exactly "the drag
happened here"), adopted by the owner, and then every window -- the dragged one included -- is
written from the owner with the same cursor pair, mode and visibility, gate bounds and visibility,
and trigger marker. `RowSet::set_cursors()` is the new pair setter; `trigger_marker_x()`/`_visible()`
are the getters the mirror reads. The measurement marks travel by signal name
(`RowSet::MeasureMark::signal`, filled by the measures pass), so each window finds the row that
plots the signal and draws its ruler or mark there -- or none, when it does not plot it. The search
marks are times on the window and are placed on every window by the same per-frame path, and the
crosshair-axis-labels preference is restated to open windows when it changes.

Probe: `[scope-probe] instruments windows=N cursors=N gate=N trigger=N marks=N (main trigger
on=..)`, after adding a real gated `Top` measurement on `sine` -- the gate is restated visible each
frame by `update_measures()` and marks come only from `show_on_plot` measurements, so the leg uses
the app's own path. Docs: README, the cursors invariant, and the probe readings.
This commit is contained in:
Martino Ferrari committed 2026-11-06 12:21:38 +01:00
1 parent a29f38eaa9
commit 91ca29d181
10 files changed
+211

No files matched your search

+10
View File
@@ -945,6 +945,16 @@ void RowSet::place_cursors(Row& r, plot::CursorPlacement placement) {
}
void RowSet::set_cursors(double a, double b) {
if (a == cursor_a_x_ && b == cursor_b_x_) return;
cursor_a_x_ = a;
cursor_b_x_ = b;
// `Free`: the pair is the owner's already-committed position (a gesture in another window, or
// this window's own last sync); snapping it here would fight the hand still moving it.
for (int i = 0; i < rows_; ++i) place_cursors(row(i), plot::CursorPlacement::Free);
place_crossings();
}
void RowSet::set_cursor_mode(CursorMode mode) {
if (mode == cursor_mode_) return;
cursor_mode_ = mode;
+14
View File
@@ -355,6 +355,16 @@ class RowSet {
void place_crossings();
CursorMode cursor_mode() const noexcept { return cursor_mode_; }
/// Writes both vertical cursors at once: the two-time pair is one instrument with one owner
/// (the main window's row set), and a scope window is written from it each frame -- reading it
/// back from that window's own plots is what lets a drag *there* move the same pair.
void set_cursors(double a, double b);
/// The trigger marker's position and whether it is up: what a scope window is written from, the
/// same way it is written from the cursor pair.
double trigger_marker_x() const noexcept { return trigger_marker_x_; }
bool trigger_marker_visible() const noexcept { return trigger_marker_on_; }
/// The two vertical cursors' positions, in the **display** x the axis reads -- not stream
/// seconds, which they coincided with only while `display_epoch()` was always 0. A live view
/// is newest-relative now, so a cursor left at -1.5 s keeps meaning "1.5 s ago" and the data
@@ -399,6 +409,10 @@ class RowSet {
/// its label stays empty.
struct MeasureMark {
int row = 0;
/// The signal the mark belongs to, so a *scope window* can find the row that plots it (its
/// row numbering is its own): a mark that never carried the name could only be placed in a
/// copy by index, which a window's own layout editor invalidates.
std::string signal;
bool level = false;
double value = 0.0;
plot::u32 color = 0;
+79
View File
@@ -1508,6 +1508,76 @@ void set_scope_layout_editing(App& app, App::ScopeWindow& scope, bool on) {
if (scope.widget != nullptr) gtk_gl_area_queue_render(GTK_GL_AREA(scope.widget));
}
void sync_scope_instruments(App& app) {
if (app.scopes.empty()) return;
// A gesture in a **scope** window moves the same pair of instruments: read it back from that
// window's own controllers first, so the main window adopts it before anything is written out.
// (`sync_cursors()`/`sync_gate()` only adopt while their own plot is the one being dragged,
// which is exactly the question asked here.)
for (const std::unique_ptr<App::ScopeWindow>& scope : app.scopes) {
if (scope->rows == nullptr || scope->area == nullptr) continue;
int cursor_row = -1;
int gate_row = -1;
for (int i = 0; i < scope->rows->row_count(); ++i) {
plot::input::Controller* c = scope->area->row_controller(static_cast<plot::u32>(i));
if (c == nullptr) continue;
if (cursor_row < 0 && c->grabbed_cursor() != plot::kInvalid) cursor_row = i;
if (gate_row < 0 && c->grabbed_region() != plot::kInvalid) gate_row = i;
}
if (cursor_row < 0 && gate_row < 0) continue;
scope->rows->sync_cursors(cursor_row);
scope->rows->sync_gate(gate_row);
if (cursor_row >= 0) {
app.rows->set_cursors(scope->rows->cursor_a_x(), scope->rows->cursor_b_x());
}
if (gate_row >= 0) app.rows->set_gate(scope->rows->gate_x0(), scope->rows->gate_x1());
}
// The main window's row set is the owner: every scope is written from it each frame. The setters
// early-out when nothing changed, so this is a handful of comparisons on a steady frame.
const bool cursors = app.rows->cursors_visible();
const bool gate = app.rows->gate_visible();
const bool trigger = app.rows->trigger_marker_visible();
for (const std::unique_ptr<App::ScopeWindow>& scope : app.scopes) {
if (scope->rows == nullptr) continue;
scope->rows->set_cursors_visible(cursors);
scope->rows->set_cursor_mode(app.rows->cursor_mode());
// The pair is shared even while hidden: a window whose cursors are shown later must show them
// where the others already stand (`set_cursors()` places them only where the mode and the
// visibility let them be drawn).
scope->rows->set_cursors(app.rows->cursor_a_x(), app.rows->cursor_b_x());
scope->rows->set_gate_visible(gate);
scope->rows->set_gate(app.rows->gate_x0(), app.rows->gate_x1());
scope->rows->set_trigger_marker(app.rows->trigger_marker_x(), trigger);
}
}
void mirror_measure_marks(App& app) {
if (app.scopes.empty()) return;
for (const std::unique_ptr<App::ScopeWindow>& scope : app.scopes) {
if (scope->rows == nullptr) continue;
std::vector<RowSet::MeasureMark> marks;
for (const RowSet::MeasureMark& mk : app.measure_marks_display) {
// The window's rows are its own: find the row that plots the mark's signal (a window that
// does not show it draws nothing for it, which is what "the mark is on the trace it
// measures" means).
int row = -1;
for (int r = 0; r < scope->rows->row_count() && row < 0; ++r) {
for (int sl = 0; sl < RowSet::kSlotsPerRow; ++sl) {
if (scope->rows->row(r).signal_name[static_cast<std::size_t>(sl)] == mk.signal) {
row = r;
break;
}
}
}
if (row < 0) continue;
RowSet::MeasureMark t = mk;
t.row = row;
marks.push_back(std::move(t));
}
scope->rows->set_measure_marks(marks);
}
}
void sync_reopen_scopes_item(App& app) {
if (app.reopen_scopes_item == nullptr) return;
const std::size_t n = app.closed_windows.size();
@@ -2999,6 +3069,11 @@ void place_search_marks(App& app) {
gtk_label_set_text(GTK_LABEL(app.search_marks_label), text);
}
app.rows->set_search_marks(xs, current);
// A hit is a time on the window, not a property of a row: every scope window draws the same
// marks (and the same current one) on its own plots.
for (const std::unique_ptr<App::ScopeWindow>& scope : app.scopes) {
if (scope->rows != nullptr) scope->rows->set_search_marks(xs, current);
}
}
@@ -4818,6 +4893,10 @@ gboolean tick(gpointer user) {
}
app->rows->sync_cursors(cursor_drag_row);
app->rows->sync_gate(gate_drag_row);
// The scope windows' instruments, from the main window's one owner: the cursors, the gate band
// and the trigger marker are the same instruments on every window, and a drag on one of them
// *there* comes back through here to the one owner.
sync_scope_instruments(*app);
// The drag's end is a new pair of gate bounds, and the bounds are session state: save there,
// the way a dragged row sizing is saved when its own gesture ends. During the drag every
// frame would only re-arm the same 400 ms debounce, but the *end* is the one moment the
+7
View File
@@ -2108,6 +2108,13 @@ void remember_closed_window(App& app, const App::ScopeWindow& scope);
/// Opens a window from a remembered state: an empty scope window, then its rows, weights and
/// signals. Returns the opened window (nullptr when it could not be made).
App::ScopeWindow* open_scope_window_from_state(App& app, const LayoutState::Window& state);
/// Writes the main window's instruments -- the two cursors, the gate band, the trigger marker --
/// into every scope window, and adopts one dragged *there* back into the main window first. Called
/// once per frame from the main tick, after the main's own `sync_cursors()`/`sync_gate()`.
void sync_scope_instruments(App& app);
/// The main window's measurement marks, translated to each scope window's own rows (by signal
/// name) and placed there: a window that plots a measured signal draws its mark.
void mirror_measure_marks(App& app);
/// Refreshes the *Reopen closed windows* entry: its count and whether it is sensitive.
void sync_reopen_scopes_item(App& app);
/// Turns a scope window's own layout editor on or off, building or taking down its buttons.
+2
View File
@@ -296,6 +296,7 @@ void place_measure_marks(App& app) {
}
}
app.rows->set_measure_marks(app.measure_marks_display);
mirror_measure_marks(app);
}
/// Recomputes every measurement over the window it describes and writes the results into the
@@ -734,6 +735,7 @@ void update_measures(App& app, bool force, bool accumulate) {
// `MeasureMarkSpec` -- the badge text is built where the display conversion happens).
RowSet::MeasureMark mark;
mark.row = row;
mark.signal = m.signal;
mark.level = spec.level;
mark.value = spec.level ? spec.value : spec.value + s0;
mark.color = color;
+3
View File
@@ -287,6 +287,9 @@ void open_preferences(App& app) {
// The crosshair's axis badges, at once like the theme: it is a drawing flag on the
// plots, and a change is the next frame's drawing.
app.rows->set_crosshair_axis_labels(s.crosshair_axis_labels);
for (const std::unique_ptr<App::ScopeWindow>& scope : app.scopes) {
if (scope->rows != nullptr) scope->rows->set_crosshair_axis_labels(s.crosshair_axis_labels);
}
// Only a failure is announced. A change arrives once per keystroke in the directory row, and a
// status line that flashed on each one would be the app shouting about its own bookkeeping.
if (!written) set_notice(app, "could not write the preferences file", 4);
+71
View File
@@ -2018,6 +2018,77 @@ gboolean scope_probe_step(gpointer user) {
remembered_name ? 1 : 0, back_rows, back_slot.c_str(),
a->closed_windows.size());
std::fflush(stderr);
// **The instruments are on every window**: the cursors, the gate band and the trigger
// marker are written from the main window's one owner (the next tick's
// `sync_scope_instruments()`), so the states below are asked for on the main and read on
// the scope.
a->rows->set_cursors_visible(true);
a->rows->set_cursor_mode(RowSet::CursorMode::Both);
a->rows->set_cursors(-1.0, 0.0);
a->rows->set_gate(-1.2, -0.8);
a->rows->set_gate_visible(true);
a->rows->set_trigger_marker(0.0, true);
// A real measurement on 'sine' with `show_on_plot`: the marks path is the one a user's
// measurement takes (`update_measures()` -> `place_measure_marks()` -> the windows), so a
// window that plots 'sine' must draw its ruler and one that does not must draw none.
{
Measure m;
m.signal = "sine";
m.type = MeasureType::Top;
m.options.show_on_plot = true;
// Gated: `update_measures()` restates the gate's visibility every frame (it is up while
// *any* measurement is gated), so the band is genuinely on and its bounds are the ones
// set above rather than a value the mirror would be right to hide.
m.options.gate = true;
a->measures.push_back(std::move(m));
rebuild_measures_pane(*a);
}
a->dbg_scope_step = 5;
g_timeout_add(700, scope_probe_step, a);
return G_SOURCE_REMOVE;
}
case 5: {
// A frame has passed: the tick wrote the main's instruments into every window.
int cursor_ok = 0;
int gate_ok = 0;
int trigger_ok = 0;
int marks_ok = 0;
for (const std::unique_ptr<App::ScopeWindow>& scope : a->scopes) {
if (scope->rows == nullptr) continue;
const bool cursors = scope->rows->cursors_visible() &&
scope->rows->cursor_mode() == RowSet::CursorMode::Both &&
std::fabs(scope->rows->cursor_a_x() + 1.0) < 1e-9 &&
std::fabs(scope->rows->cursor_b_x()) < 1e-9;
const bool gate = scope->rows->gate_visible() &&
std::fabs(scope->rows->gate_x0() + 1.2) < 1e-9 &&
std::fabs(scope->rows->gate_x1() + 0.8) < 1e-9;
const bool trigger =
scope->rows->trigger_marker_visible() == a->rows->trigger_marker_visible() &&
std::fabs(scope->rows->trigger_marker_x() - a->rows->trigger_marker_x()) < 1e-9;
// The mark: the window that plots 'sine' has it, one that does not has none. Which is
// which is read off the same names.
bool plots_sine = false;
for (int r = 0; r < scope->rows->row_count(); ++r) {
for (int sl = 0; sl < RowSet::kSlotsPerRow; ++sl) {
if (scope->rows->row(r).signal_name[static_cast<std::size_t>(sl)] == "sine") {
plots_sine = true;
}
}
}
const int placed = scope->rows->measure_marks_placed();
const bool mark = plots_sine ? placed == 1 : placed == 0;
cursor_ok += cursors ? 1 : 0;
gate_ok += gate ? 1 : 0;
trigger_ok += trigger ? 1 : 0;
marks_ok += mark ? 1 : 0;
}
const int n = static_cast<int>(a->scopes.size());
std::fprintf(stderr,
"[scope-probe] instruments windows=%d cursors=%d gate=%d trigger=%d "
"marks=%d (main trigger on=%d x=%.3f)\n",
n, cursor_ok, gate_ok, trigger_ok, marks_ok,
a->rows->trigger_marker_visible() ? 1 : 0, a->rows->trigger_marker_x());
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
default: