fixed event/live transition

This commit is contained in:
Martino Ferrari committed 2026-10-13 23:37:49 +02:00
1 parent bb99be5889
commit 881b52748e
9 files changed
+314 -2

No files matched your search

+19 -1
View File
@@ -583,6 +583,22 @@ A replay leaves trigger mode, because a file has no edge to wait for -- an armed
held display draws that snapshot rather than the file that was just loaded. The events pane stays
open through it: it is available while a stored event is on screen.
**Going back to *Live* puts the live view back as it was.** Opening a file changes two things that
are not the user's choice -- the time window becomes the event's own span, because that is what
framing an event means, and trigger mode goes off for the reason above -- and neither reason survives
the way back. So the settings the live view had are taken on the way in and restored on the way out:
the time window, trigger mode, the trigger's channel, level, condition, pre-trigger share and
hold-off. The trigger's channel is remembered by **name**, since a comparison renames its channels
(`square (A)`) and the way back has to find the stream's own. The restore waits for the first CONFIG
of the restarted client, which is a frame or two, and is the last word on the window -- it was
previously the *saved* session's window that came back, which is to say, somebody else's.
A related repair: **the session file is no longer written while a stored event is on screen.** A
comparison rebinds every plot slot to its own renamed channels, and the automatic save wrote those
names into the session -- so the next *Live* re-applied a layout that named no live signal, and the
scope came back with an empty plot. Nothing is lost by skipping: a file view configured nothing about
the stream. The explicit *Save as* is unaffected; that one is an answer to a question the user asked.
Three things worth knowing:
- **The samples are the export format**, so an event is readable by everything that reads an
@@ -1181,7 +1197,9 @@ row's `×` forgets a source by deleting its session file, with no confirmation:
confirmation, and what it destroys is one layout the app rewrites on the next clean exit. A session
is **not written** when no CONFIG arrived during the run — otherwise a source whose streamer was
down during one launch would silently vanish from the chooser list because its file was flattened
to empty signal names, which the filter refuses.
to empty signal names, which the filter refuses — nor while a **stored event is on screen**, for
the same reason stated the other way round: a file view configured nothing about the stream, and a
comparison's renamed slots written into the session came back as an empty plot.
**Math channels.** A math channel is derived from the others by a graph of operations and is then
an *ordinary channel*: the model synthesises a signal entry for it, so an enabled one is acquired,
@@ -59,6 +59,17 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
out — a source whose streamer was merely down during one run would silently vanish from the
only list it can be picked from, for ever. Keeping the old file loses nothing: a run that
configured nothing had nothing worth persisting.
- **It refuses again while `replaying` is set: a file view is not the source's session.** Same
argument, different cause — this view configured nothing about the stream. A plain replay leaves
the rows where the user put them, so its save would merely be pointless; a **group** rebinds every
slot to its own renamed channels and the save wrote `signals = ["square (A)", …]` into the file.
Nothing noticed until the way back: *Go live* re-applies the session, those names match no live
signal, so nothing was enabled and **the live view came back with no channels at all** — on the
window the poisoned file carried, which is also how it swallowed the window restore below it. The
reading is `[events] live restored (after a group)`: the symptom was a stuck `pending=1` beside
`channels=0`, because the restore waits for a channel list that was never going to arrive. The
explicit *Save as* is deliberately **not** guarded: that one is a user's own answer to "save this
view", and its dialog reports the outcome.
## A configuration is a named session, and a source may hold several
@@ -182,6 +182,53 @@ deliberately **not** one of the conditions: a stored event being examined is a f
trigger, and tying the pane to the trigger would close the list under the click that opened an event.
The recording switch lives in the trigger panel, not in the pane it empties.
## Opening a file is a detour, so *Go live* is a restore
A file view overwrites the live view's settings and every overwrite has a reason: the window becomes
the event's own span because that is what *framing* an event means, and the trigger goes off because
a file has no edge to wait for (leaving it on kept a frozen capture on screen *instead* of the file,
so the click looked like it did nothing at all). None of those reasons survive the way back. What
the user had set is theirs, and "going back to live loses the trigger I spent a minute setting up"
is the complaint this answers.
- **`snapshot_live_state()` on the way in, `restore_live_state()` on the way out.** The snapshot is
taken by both ways into a file view -- `replay_event()` and `build_event_group()` -- *before* the
acquisition is stopped, and is a **no-op while `replaying`**: a second event opened from the first,
or a group opened from a replay, would otherwise snapshot a file's settings as "live", which are
the very values the restore exists to undo. The group takes it after its argument checks, because a
group that is refused leaves the live view alone.
- **The trigger channel is stored by *name*.** Same reason `TriggerEngine::configure()` takes one: the
index is a position in a channel list that a file view rebuilds, and a group **renames** its
channels outright (`square (A)`). Putting an index back would select a different signal.
- **`enabled` and `armed` are deliberately not restored from the snapshot.** They belong to the
toolbar toggle, which owns an atomic mirror of `enabled` (`set_trigger_enabled()`) that a plain
field write under the lock would leave stale. The restore writes the rest of the `TriggerConfig`
and then moves the *toggle*, whose handler states both.
- **The restore runs from the tick, not from `go_live_again()`, and *after* the deferred layout
block.** Two writers would otherwise beat it:
- `apply_layout()` ends in `apply_time_window(st.window_s)` -- the window the **session file**
holds, not the one the live view had. `go_live_again()` clears `layout_loaded`, so that block
fires on the next tick; a restore done inside `go_live_again()` is overwritten by the saved value
a frame later. Being the next statement after it in `tick()` is the fix.
- `rebuild_trigger_signals()`, finding a `channel` of `-1` after the rebuild renumbered the
channels, silently settles on the first signal *and* re-defaults the level -- which is why
`trigger_level_touched` is restored with the configuration.
- **It waits for the stream's own channel list, gated on `generation()` having moved.** The restored
name has to be resolved against the live channels, and those do not exist until the first CONFIG of
the restarted client lands on the producer thread. `App::live_restore_generation` is the generation
read when *Go live* was pressed; `live_restore_pending` stays set until it differs **and**
`channels()` is non-empty. A `pending=1` that never clears is therefore a reading about the
*stream*, not about the restore -- see `invariants-session-and-export.md` for the save that made it
stick at `channels=0`.
- **Only the two controls a file view actually moved are restated** -- the signal dropdown and the
level spin. The rest of the trigger panel was never touched, and writing to a control fires its
handler, so the ones with nothing to put back are left alone. Both writes are inside
`updating_panels`.
- **`pinned` is not a pass/fail reading on the way back.** `apply_time_window()` drops the file's pin
and re-arms the live follow, but if the restored trigger was *on* it can latch again within the
frame or two before the probe reads -- and a held capture is pinned, correctly. That is what
`held=` and `mode=` are beside it for.
## A comparison is two plots of one model
*Compare* and *accumulate* are still one model -- the buffers cannot hold two independent loads -- but a
+20
View File
@@ -992,6 +992,7 @@ configuration that carries settings, is the defect this exists for — a saved h
[events] activated row0: replaying=1 pane=1 pane_w=240 pane_box=228 tick=0 window=2.000 axis=[-1.600,0.400] marker=0.000 thumbnail=1 thumb_size=138x46 thumb_visible=1 rows_kept=1 selected=2/2
[events] replayed: channels=3 samples=20001 replaying=1 waveform=1
[events] live again: replaying=0 waveform=0
[events] live restored (after replay): window=0.500000 (want 0.500000) trigger=1 (want 1) channel='square' (want 'square') level=0 (want 0) enabled=1 armed=1 pinned=0 held=0 mode=2 pending=0 gen=10/5
[events] recording off, trigger off: button_visible=1 pane=1 pane_w=240 tick=1 recording=0
[events] recording off, trigger on: button_visible=1 pane=1 pane_w=240 tick=1 recording=0
[events] compare buttons: cached=2 selected=2 compare=1 accumulate=1
@@ -1000,6 +1001,7 @@ configuration that carries settings, is the defect this exists for — a saved h
[events] compare sync: y_a=-3.500 y_b=-3.500 mirrored=1
[events] accumulate: kind=2 rows=1 channels=8 samples0=32964 selected=2 traces0=8 window=2.000
[events] group left: replaying=0 kind=0 waveform=0
[events] live restored (after a group): window=0.500000 (want 0.500000) trigger=1 (want 1) channel='square' (want 'square') level=0 (want 0) enabled=1 armed=1 pinned=0 held=0 mode=2 pending=0 gen=22/16
```
`thumb_size` is the built size of the row's picture: `kEventThumbW` wide, and as tall as the *stored*
@@ -1016,6 +1018,21 @@ header — `480` wide and `44 * signals` tall — and `row_drawn=1` says the row
The probe therefore needs a cache a **previous** run populated: it fires ~4.5 s in, before this run's
first latch.
`[events] live restored (…)` is the way *back*, read once per way into a file view — after a plain
replay and after a group — and four of its fields are a **pair**: the value now, and the value the
live view held before the file view opened over it. The probe captures that expectation itself
(`capture_live_expectation()`, `App::dbg_live_*`) rather than asking the restore path for it: a
reading taken from the mechanism under test asserts nothing. The window is set to 0.5 s just before
the event is opened precisely so the pair can disagree — with the event's own span there would be
nothing to tell "restored" from "untouched".
Both lines are read a *later* step, not in the handler that pressed *Live*: the restore waits for the
restarted client's first CONFIG, so it is performed by a tick ~900 ms on. `pending=0` is what says it
happened; `gen=a/b` is the generation now against the one *Go live* read. A **stuck `pending=1`** is
not a reading about the restore but about the stream behind it — the generation never moved, or the
channel list came back empty, which is how a session file poisoned by a save taken during a group
produced `pending=1 channels=0` and a live view with nothing on it.
| field | value it must read | the defect a wrong reading names |
| --- | --- | --- |
| `n`, `stem`, `seq` | one entry per latch, named by its time and the run's counter | two events in one second overwriting each other, or a cache that is never written |
@@ -1032,6 +1049,9 @@ first latch.
| `axis`, `marker`, `pane_box` (in `activated row0`) | `[-pre, +post]` of the event just opened, `marker=0.000`, and a `pane_box` close to `pane_w` | an opened event framed on something other than its own pin; a trigger line left **parked** (a large negative) rather than on the edge, which is a file opened without its marker; or a pane whose *rendered* width is 0 while the divider says 240 -- the position is what was asked for, `pane_box` is what the pane actually got, and a divider at 240 with a zero-width pane is still no pane |
| `rows_kept`, `selected` (in `activated row0`) | `rows_kept=1` and `selected=N/N` -- the counts either side of the activation equal | the list **rebuilt** by the activation. It is the row *object* that is compared, not its index: a rebuilt list has a row 0 too, and only identity tells a list that was left alone from one that was torn down under the user's scroll position and their comparison selection. `mark_events_on_screen()` must rewrite the titles in place |
| `replaying` (last line) | 0 | a replay with no way back: *Live* must start a fresh client and thread |
| `window`, `trigger`, `channel`, `level` (in `live restored`) | each equal to its `(want …)` | the way back out of a file view discarding what the live view was set to. One wrong half names which writer won: a `window` that reads the **session file's** value is the deferred `apply_layout()` beating the restore; `trigger=0` is the toggle the file view switched off and nobody switched back; an empty or shifted `channel` is an index restored instead of a name, after a rebuild renumbered (or a group renamed) the channels; and a `level` that moved on its own is `rebuild_trigger_signals()` re-defaulting it because `trigger_level_touched` was not restored with the configuration |
| `pending`, `gen` (in `live restored`) | `pending=0`, and the two generations different | the restore still waiting. It is gated on the stream's first CONFIG, so `pending=1` says the channel list the stored trigger name needs never arrived -- with `channels=0` behind it, a session file that enabled nothing |
| `pinned`, `held`, `mode` (in `live restored`) | `pinned=0`, **or** `pinned=1` with `held=1` | a file's pin surviving the way back, which is a live view that cannot be navigated. `apply_time_window()` drops it; the second reading is the legitimate case -- a restored trigger that was *on* can latch again before the probe reads, and a held capture is pinned, correctly |
| `pane_w` | the divider's position: 0 while the pane is closed, its remembered width (240 by default) while it is open | the pane reserving a column while closed (a paned hands its start child its minimum unless the position says otherwise), or a width that is not remembered |
| `tick` | 1 in live mode, **0** once a file is on screen | the signal list still offering "acquire this signal" for samples that are already stored |
| `window` (in `activated row0`) | the **event's** span -- the probe sets 0.5 s just before the click, so reading the event's span says opening it framed it | a replay that leaves the previous window alone, or one that restates it every frame instead of once |
+10
View File
@@ -3294,6 +3294,16 @@ gboolean tick(gpointer user) {
}
}
// ... and *after* it, which is the point: the block above ends in `apply_time_window()` with the
// window the session file holds, so a restore done before it would be overwritten by the saved
// value. The generation is the other half of the wait -- the stream's first CONFIG, which is what
// gives the stored trigger channel a list to be found in.
if (app->live_restore_pending && !app->replaying && !model.channels().empty() &&
model.generation() != app->live_restore_generation) {
app->live_restore_pending = false;
restore_live_state(*app);
}
// What the display shows, decided once for the whole frame: the producer can open or
// close a capture between any two reads of the model, and the bind, the epoch, the pin
// and the status line all have to describe the same picture.
+36
View File
@@ -452,6 +452,30 @@ struct App {
std::vector<EventRecord> group;
/// True while a stored event is on screen instead of the live stream.
bool replaying = false;
/// What the live view was set to when a file view was opened over it, so that *Go live* can put it
/// back. A file view overwrites all of it and for good reasons -- the window is pinned to the
/// event's own span because that is what framing an event means, and the trigger goes off because a
/// file has no edge to wait for -- but none of those reasons survive the return to the stream, and
/// what the user set is theirs. Taken on the way *in*, and only when the view being left is the live
/// one, so a replay opened from inside a group does not snapshot the group's settings as "live".
///
/// The trigger's channel is kept by **name**: the index is a position in a channel list that a file
/// view rebuilds (a group renames its channels outright), so putting the index back would select a
/// different signal, or none.
struct LiveState {
bool valid = false;
double window_s = 0.0;
bool trigger_on = false;
TriggerConfig trigger;
std::string trigger_channel;
bool level_touched = false;
};
LiveState live_state;
/// Set by *Go live*, cleared by the tick that performs the restore, with the model generation it
/// has to see move first -- the stream's own channel list is what the stored trigger channel is
/// resolved against.
bool live_restore_pending = false;
unsigned live_restore_generation = 0;
/// Set when a latch frames a new snapshot, consumed at the *end* of that tick: the record needs the
/// rows bound to the event (for the thumbnail) and the bind happens after the display decision.
bool record_pending = false;
@@ -749,6 +773,16 @@ struct App {
/// position and their comparison selection.
GtkListBoxRow* dbg_events_row0 = nullptr;
int dbg_events_selected = 0;
/// What the *live* view was set to just before the probe opened an event: the time window, the
/// trigger's mode, its channel (by name, the only stable identity) and its level. Read back after
/// each way out of a file view, because a file view overwrites all four -- the window is pinned to
/// the event's own span and the trigger is switched off, since a file has no edge to wait for --
/// and going live has to put them back. The probe keeps its own expectation rather than asking
/// `App`: a reading taken from the mechanism under test asserts nothing.
double dbg_live_window = 0.0;
bool dbg_live_trigger_on = false;
std::string dbg_live_trigger_channel;
double dbg_live_level = 0.0;
/// Which step of the plot-context-menu probe runs next (`UDPSCOPE_PROBE_ROWMENU_MS`), and the
/// popover it is measuring.
int dbg_rowmenu_step = 0;
@@ -1335,6 +1369,8 @@ void update_capture_row(App& app);
bool record_event(App& app);
bool replay_event(App& app, const EventRecord& record);
bool go_live_again(App& app);
void snapshot_live_state(App& app);
void restore_live_state(App& app);
bool read_event_samples(const std::string& path, EventSamples* out, std::string* error);
std::vector<EventRecord> selected_events(App& app);
void update_group_buttons(App& app);
+93 -1
View File
@@ -141,11 +141,94 @@ bool record_event(App& app) {
return true;
}
/// Remembers the live view's settings before a file view is opened over them.
///
/// A no-op unless the view being left *is* the live one: replaying a second event from inside the
/// first, or a group opened from a replay, must not overwrite the snapshot with a file's settings --
/// those are the very values *Go live* exists to undo.
void snapshot_live_state(App& app) {
if (app.replaying || app.model == nullptr || app.rows == nullptr) return;
App::LiveState& s = app.live_state;
s.valid = true;
s.window_s = app.rows->time_window();
s.trigger_on = app.trigger_toggle != nullptr &&
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.trigger_toggle)) != FALSE;
s.level_touched = app.trigger_level_touched;
std::lock_guard<std::mutex> lock(app.model->mutex());
s.trigger = app.model->trigger();
s.trigger_channel = s.trigger.channel >= 0 &&
s.trigger.channel < static_cast<int>(app.model->channels().size())
? app.model->channels()[static_cast<std::size_t>(s.trigger.channel)].name
: std::string();
}
/// Puts those settings back on the way out of a file view.
///
/// Deferred by `go_live_again()` to the first tick that sees the stream's *own* channel list, which
/// is what makes this the last writer over the two things that would otherwise overwrite it: the
/// deferred layout re-apply (which ends in `apply_time_window()` with the *saved* window, not the
/// live one) and `rebuild_trigger_signals()`, which, finding a `channel` of `-1` after a rebuild
/// renumbered or renamed the channels, silently settles on the first signal and re-defaults the level
/// with it.
///
/// Inside, the order is the whole of it:
///
/// - The configuration goes in under the lock, with the channel resolved from the stored *name*.
/// `enabled` and `armed` are deliberately *not* written: they belong to the toolbar toggle, which
/// owns an atomic mirror of `enabled` that a plain field write would leave stale.
/// - The two controls a file view actually moved are restated from it -- the signal dropdown and the
/// level spin. The rest of the panel was never touched, and writing to a control fires its handler,
/// so the ones that have nothing to put back are left alone.
/// - `apply_time_window()`, the only path allowed to restate the time scale; it also drops the event
/// pin the file view left on the axis and re-arms the live follow.
/// - The toggle last, because its handler reads the configuration it is turning on.
void restore_live_state(App& app) {
App::LiveState& s = app.live_state;
if (!s.valid) return;
s.valid = false; // consumed: the next way in takes its own snapshot
int channel = -1;
{
std::lock_guard<std::mutex> lock(app.model->mutex());
const std::vector<ChannelInfo>& channels = app.model->channels();
for (std::size_t i = 0; i < channels.size(); ++i) {
if (channels[i].name == s.trigger_channel) {
channel = static_cast<int>(i);
break;
}
}
TriggerConfig& tc = app.model->trigger();
const bool enabled = tc.enabled;
const bool armed = tc.armed;
tc = s.trigger;
tc.enabled = enabled;
tc.armed = armed;
tc.channel = channel;
}
app.trigger_level_touched = s.level_touched;
app.updating_panels = true;
if (channel >= 0 && app.trigger_signal_drop != nullptr) {
gtk_drop_down_set_selected(GTK_DROP_DOWN(app.trigger_signal_drop),
static_cast<guint>(channel));
}
if (app.trigger_level_spin != nullptr) {
gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_level_spin), s.trigger.condition.level);
}
app.updating_panels = false;
apply_time_window(app, s.window_s);
if (app.trigger_toggle != nullptr &&
(gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app.trigger_toggle)) != FALSE) !=
s.trigger_on) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.trigger_toggle), s.trigger_on ? TRUE : FALSE);
}
}
/// Replays a cached event: the same loader `--open` uses, on the event's file. The acquisition is
/// stopped first, because a live producer overwrites the loaded samples within a frame -- so the
/// scope becomes a file viewer until *Go live* starts it again.
bool replay_event(App& app, const EventRecord& record) {
if (app.acq == nullptr || app.model == nullptr) return false;
// Before the stop, while the live view is still the view: everything below overwrites it.
snapshot_live_state(app);
app.acq->stop();
WaveformLoadReport report;
std::string error;
@@ -199,6 +282,13 @@ bool go_live_again(App& app) {
app.layout_loaded = false; // the session's layout applies to the source again
app.event_held = false;
app.model->set_display_epoch(0.0);
// What the live view had, put back from the tick rather than here: the stream's own channel list is
// what the stored trigger channel has to be resolved against, and it does not exist until the first
// CONFIG of the restarted client lands on the producer thread. The generation read here is what the
// tick compares against to know that it has (see `restore_live_state()` for what else that ordering
// buys).
app.live_restore_generation = app.model->generation();
app.live_restore_pending = app.live_state.valid;
try {
app.acq->start();
} catch (const std::exception& e) {
@@ -307,7 +397,9 @@ bool build_event_group(App& app, const std::vector<EventRecord>& records, EventG
// The acquisition first: a live producer keeps pushing into the model it was started with -- and it
// pushes *four* stream signals into the group's eight channels, so the group's own samples are the
// ones that go missing.
// ones that go missing. The live view's settings are taken ahead of it, for `go_live_again()`:
// after the checks above, because a group that is refused leaves the live view alone.
snapshot_live_state(app);
app.acq->stop();
std::vector<EventSamples> events(records.size());
+8
View File
@@ -232,6 +232,14 @@ void save_layout_now(App& app) {
// run would silently vanish from the only place it can be picked. Keeping the old file loses
// nothing: this run configured nothing.
if (app.model->signals().empty()) return;
// A file view is not the source's session, and the same argument applies: this view configured
// nothing about the stream. A replay leaves the rows where the user put them, so its save would
// merely be pointless -- but a *group* rebinds every slot to its own renamed channels, and a save
// taken while one was up wrote `signals = ["square (A)", ...]` into the file. The next load then
// matched no live signal, enabled nothing, and the live view came back with no channels at all,
// on the window the poisoned file carried. Skipping costs nothing: the live session's own file is
// already on disk, and `go_live_again()` puts the rest back.
if (app.replaying) return;
if (app.config_name.empty()) app.config_name = active_configuration(app.source_host,
app.source_port);
const std::string path =
+70
View File
@@ -3107,6 +3107,62 @@ gboolean rowmenu_probe_step(gpointer user) {
return G_SOURCE_REMOVE;
}
/// The live view's own settings, taken just before a file view is opened over them, so that every
/// way back out can be read against what was there. The trigger is read under the lock and its
/// channel by *name*: a file view rebuilds the channel list, so an index means nothing on the way
/// back. The probe keeps its own copy rather than asking the restore path for one -- a reading taken
/// from the mechanism under test asserts nothing.
void capture_live_expectation(App& a) {
a.dbg_live_window = a.rows->time_window();
a.dbg_live_trigger_on =
a.trigger_toggle != nullptr &&
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(a.trigger_toggle)) != FALSE;
std::lock_guard<std::mutex> lock(a.model->mutex());
const TriggerConfig& tc = a.model->trigger();
a.dbg_live_level = tc.condition.level;
a.dbg_live_trigger_channel = tc.channel >= 0 && tc.channel < int(a.model->channels().size())
? a.model->channels()[std::size_t(tc.channel)].name
: std::string();
}
/// What came back when a file view was left: every setting the live view had before it opened, read
/// against the expectation the probe captured itself. Four of the five readings are a *pair* on
/// purpose -- a file view overwrites the window, the trigger mode, the trigger's channel and its
/// level, so a line where the two halves of a pair disagree names the one that was not put back.
/// `pinned` is the fifth and has no pair: a file pins the axis to the event's span, and going live
/// must leave the axis following again, so the only passing reading is 0.
void report_live_restored(App& a, const char* what) {
std::string channel;
double level = 0.0;
bool enabled = false;
bool armed = false;
{
std::lock_guard<std::mutex> lock(a.model->mutex());
const TriggerConfig& tc = a.model->trigger();
enabled = tc.enabled;
armed = tc.armed;
level = tc.condition.level;
if (tc.channel >= 0 && tc.channel < int(a.model->channels().size())) {
channel = a.model->channels()[std::size_t(tc.channel)].name;
}
}
std::fprintf(stderr,
"[events] live restored (%s): window=%.6f (want %.6f) trigger=%d (want %d) "
"channel='%s' (want '%s') level=%.6g (want %.6g) enabled=%d armed=%d pinned=%d "
"held=%d mode=%d pending=%d gen=%u/%u\n",
what, a.rows->time_window(), a.dbg_live_window,
a.trigger_toggle != nullptr &&
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(a.trigger_toggle))
? 1
: 0,
a.dbg_live_trigger_on ? 1 : 0, channel.c_str(), a.dbg_live_trigger_channel.c_str(),
level, a.dbg_live_level, enabled ? 1 : 0, armed ? 1 : 0,
a.rows->window_pinned() ? 1 : 0, a.event_held ? 1 : 0,
static_cast<int>(a.model->display_mode()), a.live_restore_pending ? 1 : 0,
a.model->generation(), a.live_restore_generation);
std::fflush(stderr);
}
void events_probe_next(App& a, int step, guint delay_ms) {
a.dbg_events_step = step;
g_timeout_add(delay_ms, events_probe_step, &a);
@@ -3227,6 +3283,7 @@ gboolean events_probe_step(gpointer user) {
// says whether opening the event framed it: with the same window there would be nothing to tell
// "framed" from "untouched".
apply_time_window(*a, 0.5);
capture_live_expectation(*a);
GtkListBoxRow* row = gtk_list_box_get_row_at_index(GTK_LIST_BOX(a->events_list), 0);
// Selected *and* remembered, because what the activation must not do is rebuild the list:
// a selection the user made for a comparison, and the row object itself, are what a
@@ -3312,6 +3369,7 @@ gboolean events_probe_step(gpointer user) {
std::fprintf(stderr, "[events] live again: replaying=%d waveform=%d\n", a->replaying ? 1 : 0,
a->waveform_name.empty() ? 0 : 1);
report_live_restored(*a, "after replay");
// The pane's *other* precondition, through the real switch, in both trigger states: disabling
// recording has to take the button and the pane with it, and arming the trigger afterwards must not
// bring back a pane over an empty cache.
@@ -3360,6 +3418,10 @@ gboolean events_group_probe_step(gpointer user) {
a->events_compare != nullptr && gtk_widget_get_sensitive(a->events_compare) ? 1 : 0,
a->events_accumulate != nullptr && gtk_widget_get_sensitive(a->events_accumulate) ? 1
: 0);
// The live settings this leg will be read against, taken *here* rather than inherited from the
// replay leg: a group is a second, independent way into a file view, and its way back has to put
// back what was on screen when the button was pressed.
capture_live_expectation(*a);
if (a->events_compare != nullptr) g_signal_emit_by_name(a->events_compare, "clicked");
// Read one step later: a rebuilt row's traces are bound by the next *frame*, so a reading taken in
// the same handler as the click reads the frame before the bind.
@@ -3502,6 +3564,14 @@ gboolean events_group_probe_step(gpointer user) {
std::fprintf(stderr, "[events] group left: replaying=%d kind=%d waveform=%d\n", a->replaying ? 1 : 0,
static_cast<int>(a->group_kind), a->waveform_name.empty() ? 0 : 1);
std::fflush(stderr);
// The restore is not this handler's: it waits for the restarted client's first CONFIG, so it is a
// *later tick* that performs it. Reading here would read the frame before the stream came back.
events_group_probe_next(*a, 61, 900);
return G_SOURCE_REMOVE;
}
if (a->dbg_events_step == 61) {
report_live_restored(*a, "after a group");
return G_SOURCE_REMOVE;
}