Compare commits
8
Commits
0e891ada04
...
b02419dee3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b02419dee3 | ||
|
|
5eb20f3cdb | ||
|
|
8b3240cacb | ||
|
|
aaf544be1e | ||
|
|
d34b97158e | ||
|
|
47d49e75ff | ||
|
|
b090cc2a21 | ||
|
|
c3d588f03d |
No files matched your search
@@ -335,3 +335,33 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
|||||||
reads the *reverse* of the file: `header-bar.ui` declares menu, sidebar, math, measures, cursors and the
|
reads the *reverse* of the file: `header-bar.ui` declares menu, sidebar, math, measures, cursors and the
|
||||||
screen shows cursors, measures, math, sidebar, menu. The three comments this replaced each had it wrong
|
screen shows cursors, measures, math, sidebar, menu. The three comments this replaced each had it wrong
|
||||||
once, which is why the order is the file's now and a byte-for-byte diff of the probe is the check.
|
once, which is why the order is the file's now and a byte-for-byte diff of the probe is the check.
|
||||||
|
|
||||||
|
- **A widget can end up in the wrong container, and only a *structural* read notices.** `math_body` was
|
||||||
|
written as a `<child>` of the *window* instead of the box above it when the math editor's frame moved into a
|
||||||
|
`.ui`. A `GtkWindow` takes exactly one default `<child>`, so nothing failed: the body -- with the canvas and
|
||||||
|
the parameter form inside it -- was left parentless, `visible` but never `mapped`, size `0x0`, drawing
|
||||||
|
nothing. The symptom was "the editor opens and the canvas is blank", with the window sized around its
|
||||||
|
remaining children. `check_ui` passes such a file; `xml-stripblanks` passes it; so does every probe that
|
||||||
|
reads *values* off the widgets inside. What catches it is a reading that walks *up* from the widget
|
||||||
|
(`in_dialog=1`) or asks whether it was ever given an allocation (`mapped=1`, `canvas=420x836`). When a frame
|
||||||
|
moves into a file, the probe that proves it has to read the frame's *structure*, not only the numbers it
|
||||||
|
holds.
|
||||||
|
|
||||||
|
- **A popover's autohide is only as good as its grab.** `GtkPopover` dismisses itself when it loses the
|
||||||
|
pointer grab -- and the grab is the thing that can be missing: a window manager moving it, a nested popover
|
||||||
|
(a `GtkDropDown`'s menu, which the time-window row has) taking it, and *every* headless run, where there is
|
||||||
|
no manager to hand one out and the probes have to turn autohide off before a popover will stay up to be
|
||||||
|
measured. The symptom is "sometimes it is awkward to close": a click anywhere else leaves the bubble up.
|
||||||
|
`on_window_popover_press()` states the rule outright instead -- any press that reaches the window's own
|
||||||
|
surface closes the window popover. That needs no hit test beyond the button, because a popover *is* its own
|
||||||
|
surface and a click inside it never arrives; the button is left to its own click, whose toggle is what
|
||||||
|
closes it. `[windowpop]` reads both halves, with autohide off so it is testing the rule and not the grab.
|
||||||
|
|
||||||
|
- **A notice shares the status line with the glance, so it has to *look* like a message.** The status bar
|
||||||
|
renders `app.notice + " | " + glance` in one label, which is exactly where a reader skimming the
|
||||||
|
routine text ("live · 9.97 kS/s · 2 s window") will not see it: a refused drop that said "plot 1 holds four
|
||||||
|
signals already" was read as no feedback at all. While a notice is fresh the label carries the stock
|
||||||
|
`.warning` class, so a message looks like a message and the class goes when the notice does. Two corollaries
|
||||||
|
worth knowing: the line composes on its own cadence, so a notice can appear a beat after it was set (do not
|
||||||
|
read the label in the same tick a handler ran), and a refusal should say what to *do* about it -- "drop it on
|
||||||
|
another plot, or add a row with the layout editor (L)" rather than the bare statement of the limit.
|
||||||
@@ -36,12 +36,40 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
|||||||
`visible_samples()` reading row 0's axis after the window was destroyed, and
|
`visible_samples()` reading row 0's axis after the window was destroyed, and
|
||||||
`UDPSCOPE_EXIT_AFTER_MS` arms the trigger first, so the dump can land in `Waiting`. The live
|
`UDPSCOPE_EXIT_AFTER_MS` arms the trigger first, so the dump can land in `Waiting`. The live
|
||||||
`ch N '<name>' ... y=[..]` lines are the race-free reading of the same thing.
|
`ch N '<name>' ... y=[..]` lines are the race-free reading of the same thing.
|
||||||
|
- **A file view has no producer, so a structural change would leave it empty.** `set_math_channels()`
|
||||||
|
rebuilds the channels *and* the buffer, and a rebuild drops the history: right for a stream, whose producer
|
||||||
|
refills it from the next packet, and wrong for a file, which has nothing to refill it with. Adding a math
|
||||||
|
channel to an open waveform therefore emptied *every* channel -- `samples=0` on the sources too, which a user
|
||||||
|
reads as "the plot is broken" -- and the same emptiness follows any structural change in a file view.
|
||||||
|
`reload_file_view()` (in `ui.cpp`) re-reads the file after such a change, which is also what computes the
|
||||||
|
derived channels over it: the load's own pushes run the math once the set is in place. It fires only when the
|
||||||
|
buffer was *really* dropped -- `stored_slots() > 0` means the change found nothing to do -- and it says so
|
||||||
|
on stderr (`reloaded <path> after a structural change`), because a second read of a file is worth
|
||||||
|
accounting for. `set_math_channels()` in turn treats an identical list as no change at all, so a no-op
|
||||||
|
application (the session apply, a re-open) neither rebuilds nor drops nor re-reads; the comparison leaves the
|
||||||
|
nodes' `x`/`y` out on purpose, since those are the canvas's own and no op reads them -- a graph that only
|
||||||
|
*moved* is not a change, and rebuilding for it would drop the buffer to store two numbers. It is why `--open`
|
||||||
|
works at all with a session that defines math channels, since `main()` loads the file *before* the UI (and
|
||||||
|
so before the session's graphs) exists.
|
||||||
|
- **Every path that fills a store must skip a derived channel, not just the packet path.**
|
||||||
|
`ChannelInfo::math` states the rule for the ingest; `push_loaded_slot()` broke it, so loading a file pushed
|
||||||
|
its own value into each derived ring and `push_math_sample()` then pushed the computed one: two samples per
|
||||||
|
slot, i.e. a trace at double rate. The reading that catches it is the one already in this file --
|
||||||
|
`math '<name>': N samples vs M time slots` must be **equal** -- and a file view now has to be read as well
|
||||||
|
as a live one (`--open <the file>` with `UDPSCOPE_PROBE_MATH_MS`). An export writes every *plotted* channel,
|
||||||
|
so a file carries the derived ones as ordinary columns: those are real channels and are filled normally,
|
||||||
|
which is why the fix is in the push and not in the loader.
|
||||||
`math cost: S slots, N ns/slot (T timed), X ms total est` is the per-slot cost; see the
|
`math cost: S slots, N ns/slot (T timed), X ms total est` is the per-slot cost; see the
|
||||||
math-cost invariant below for why it is sampled, why it is an upper bound, and why a figure
|
math-cost invariant below for why it is sampled, why it is an upper bound, and why a figure
|
||||||
anywhere in 78--101 ns/slot is one reading of the same thing.
|
anywhere in 78--101 ns/slot is one reading of the same thing.
|
||||||
The probe also asserts the new editor mechanics. `dialog build:` reads the identity row back as well --
|
The probe also asserts the new editor mechanics. `dialog build:` reads the identity row back as well --
|
||||||
`name='math3' unit=''` -- which is the pair the `.ui` holds the widgets for and the code fills from the
|
`name='math3' unit=''` -- which is the pair the `.ui` holds the widgets for and the code fills from the
|
||||||
channel being edited, so a row that stopped being the one the code writes shows there. Each line carries
|
channel being edited, so a row that stopped being the one the code writes shows there.
|
||||||
|
`editor frame:` is the same idea one level down: it reads the canvas's allocation, whether that canvas is
|
||||||
|
*mapped*, and whether the canvas and the name entry are inside the dialog at all -- delayed by 500 ms, because
|
||||||
|
an allocation only exists after a layout. A canvas that is visible but parentless never maps and stays `0x0`,
|
||||||
|
and reading values off the widgets *inside* the editor cannot see that. It is the reading that catches
|
||||||
|
`math_body` written as a child of the window instead of the box above it. Each line carries
|
||||||
`(want ...)` and the reading
|
`(want ...)` and the reading
|
||||||
taken from a real run is the reference:
|
taken from a real run is the reference:
|
||||||
* `[math-probe] hit: inside node=0 which=-1, past-edge node=0 which=-1 (want 0 -1 0 -1)` — the
|
* `[math-probe] hit: inside node=0 which=-1, past-edge node=0 which=-1 (want 0 -1 0 -1)` — the
|
||||||
@@ -252,3 +280,23 @@ filter's history remains the last *real* samples it saw, and the trace resumes a
|
|||||||
with at most the transient a re-start costs. The exit dump's `math '<name>': finite=N/M` line is
|
with at most the transient a re-start costs. The exit dump's `math '<name>': finite=N/M` line is
|
||||||
the assertion: `finite` must be about `M` minus the hole share, and `finite=0` beside non-zero
|
the assertion: `finite` must be about `M` minus the hole share, and `finite=0` beside non-zero
|
||||||
`hole_slots` is this defect returned.
|
`hole_slots` is this defect returned.
|
||||||
|
|
||||||
|
- **A graph *edit* does not move the plan; adding or removing a channel does.** The plan is a partition over
|
||||||
|
the channels, so `set_math_channels()` takes a surgical path whenever the *set* is unchanged -- same channels,
|
||||||
|
names and enables: the store is never touched, every other channel keeps its samples **and** its runtimes, and
|
||||||
|
only the edited graphs are prepared again. `math_apply_locked()` computes the whole list on a recompute
|
||||||
|
(the graphs are walked in order, so a channel's inputs must be there) but writes only the channels in the
|
||||||
|
affected mask; the affected set is the edited channels plus whatever *names* them, since one channel's output
|
||||||
|
is another's input. Before this, every edit rebuilt the store and every channel in the window started again,
|
||||||
|
which is the "modifying one expression invalidates all the others" report.
|
||||||
|
`[math-probe] graph edit: stored_slots N -> M` reads it: M must be at least N -- the store keeps filling
|
||||||
|
while the edit runs -- and never 0.
|
||||||
|
|
||||||
|
- **A math channel whose source is switched off is switched off with it.** `Source` resolves to a *channel*, so
|
||||||
|
a derived channel cannot be computed from a signal nobody acquires: left enabled it would silently compute
|
||||||
|
nothing, which is what "all math signals depending on it do not compute anymore" looked like from outside.
|
||||||
|
`on_signal_switch()` disables the channels that read that signal -- **transitively**, since one math channel
|
||||||
|
may name another -- through the same path the pane's own switch uses (`MathChannel::enabled` and
|
||||||
|
`apply_math_channels()`), and takes their traces off the plots along with the signal's. The channels that do
|
||||||
|
not read it are untouched: `[enable-all-probe]`'s `math_on=[…]` is the half that reads this. Re-enabling the
|
||||||
|
signal does **not** switch them back on, for the same reason re-enabling does not restore the traces.
|
||||||
@@ -173,3 +173,12 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
|||||||
because `relayout_areas()` is the one place that builds a figure and re-points the area at it.
|
because `relayout_areas()` is the one place that builds a figure and re-points the area at it.
|
||||||
`UDPSCOPE_PROBE_COLOR_MS` reads the result back out of `plot::Plot::trace()` and out of the
|
`UDPSCOPE_PROBE_COLOR_MS` reads the result back out of `plot::Plot::trace()` and out of the
|
||||||
chip's own markup, since asking the resolver twice would assert nothing.
|
chip's own markup, since asking the resolver twice would assert nothing.
|
||||||
|
|
||||||
|
- **Switching a signal off takes its traces off every plot.** The switch means "do not acquire this", and a
|
||||||
|
trace left behind draws nothing: it holds one of the four slots a plot has, reads `--`, and blocks the drop
|
||||||
|
that would fill it with something live. `on_signal_switch()` clears every slot by the **signal name** the
|
||||||
|
slot remembers -- never the channel index, which is exactly what the enable change renumbers -- and says what
|
||||||
|
it did in a notice, because a trace that vanishes silently is its own kind of confusing. The follow-ups (a
|
||||||
|
relayout, the panels' chips, the layout save) are done once by the caller rather than per slot. Re-enabling
|
||||||
|
does **not** put the traces back: the arrangement is the user's, and guessing where they were is worse than
|
||||||
|
leaving them off.
|
||||||
@@ -1214,3 +1214,64 @@ builds, plus `nested` — the list being *inside* that revealer, which is the on
|
|||||||
(a list parented elsewhere reads 1 everywhere else). `[events]` is the other half, and it drives the pane end
|
(a list parented elsewhere reads 1 everywhere else). `[events]` is the other half, and it drives the pane end
|
||||||
to end: recording off, recording on, the trigger, the history button that reveals it, a row click and the
|
to end: recording off, recording on, the trigger, the history button that reveals it, a row click and the
|
||||||
replay, then Compare and Accumulate — so the file's buttons are read by being used.
|
replay, then Compare and Accumulate — so the file's buttons are read by being used.
|
||||||
|
|
||||||
|
## `[windowpop]`
|
||||||
|
|
||||||
|
```
|
||||||
|
[windowpop] opened=1 visible=1 click_gestures=2
|
||||||
|
[windowpop] after a press elsewhere: active=0 (want 0)
|
||||||
|
[windowpop] after a press on the button: active=1 (want 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
`UDPSCOPE_PROBE_WINDOWPOP_MS`: the time-window popover and how it closes. Autohide is turned *off* for the
|
||||||
|
probe, exactly as the menu probe does it -- a popover dismisses itself when GTK holds its *grab*, and a bare
|
||||||
|
`xvfb-run` has no window manager to hand one out -- so what is measured is the state a user meets when the
|
||||||
|
grab goes missing, where the press has to close the bubble anyway. `click_gestures` reads that the controller
|
||||||
|
is *connected* as well as that its handler works; the last press is on the button itself, which must be left
|
||||||
|
to its own toggle (`want 1` rather than `0`).
|
||||||
|
|
||||||
|
## `[drop-probe]` and what a refused drop says
|
||||||
|
|
||||||
|
```
|
||||||
|
[drop-probe] full row refused the drop: 0 (want 0)
|
||||||
|
[drop-probe] refused feedback: notice='plot 1 holds four signals already: drop it on another plot, or add a row with the layout editor (L)' warning=1
|
||||||
|
[drop-probe] the status line, composed: warning=1 status='plot 1 holds four signals already: ... | live · 9.97 kS/s · 2 s window'
|
||||||
|
```
|
||||||
|
|
||||||
|
`UDPSCOPE_PROBE_DROP_MS`. The first line is the refusal itself (a full row takes no more signals); the
|
||||||
|
second is that it *said so* -- the message and the stock `.warning` class the status line carries while a
|
||||||
|
notice is fresh; the third is the composed line the user reads, which is why the label is only checked here
|
||||||
|
and not in the second: the status line folds the notice into itself on its own cadence, so a beat after the
|
||||||
|
refusal it may still hold the previous text.
|
||||||
|
|
||||||
|
## `[enable-all-probe]` and switching a signal off
|
||||||
|
|
||||||
|
```
|
||||||
|
[enable-all-probe] 4 channel(s) enabled and assigned to plots
|
||||||
|
[enable-all-probe] switched 'sine' off: traces_left=0 (want 0) notice=''sine' is off: 1 trace left the plots'
|
||||||
|
```
|
||||||
|
|
||||||
|
`UDPSCOPE_ENABLE_ALL_MS` enables every signal and assigns the acquired channels across the rows, then
|
||||||
|
switches one off **through its own acquire tick in the signal list** -- the tick is a box inside the row, which
|
||||||
|
is the shape `signal_tick_visible()` walks -- and reads the plots back. `traces_left=0` is the rule: a disabled
|
||||||
|
signal leaves every plot rather than staying as a slot that draws nothing, and the notice is what says so.
|
||||||
|
|
||||||
|
The switch step runs late, so that a math probe armed *behind* this one has installed its graphs by then; the
|
||||||
|
two halves after it are the other rule. `math_off` is the derived channels that read this signal -- they cannot
|
||||||
|
be computed from a signal nobody acquires, so they go off with it -- and `math_on` is the ones that do not,
|
||||||
|
which must be untouched:
|
||||||
|
|
||||||
|
```
|
||||||
|
[enable-all-probe] switched 'sine' off: traces_left=0 (want 0) math_off=[math x2,math3] math_on=[math lp] notice=''sine' is off: 1 trace left the plots; math off: math x2 and 1 more'
|
||||||
|
```
|
||||||
|
|
||||||
|
## `[math-probe] graph edit:`
|
||||||
|
|
||||||
|
```
|
||||||
|
[math-probe] graph edit: stored_slots 800 -> 900 (want the second at least the first, and non-zero)
|
||||||
|
```
|
||||||
|
|
||||||
|
`UDPSCOPE_PROBE_MATH_MS` applies an edit to one graph through the editor. The plan is a partition over the
|
||||||
|
channels and an edit does not move it, so the store survives: the count *grows* (the stream keeps filling
|
||||||
|
while the edit runs) and never falls to 0, which is what a rebuild would do. Every channel then still reads
|
||||||
|
`N samples vs M time slots` with `N == M`, so no channel was left behind either.
|
||||||
@@ -58,6 +58,17 @@ struct MathNode {
|
|||||||
double hi = 1.0;
|
double hi = 1.0;
|
||||||
double x = 0.0; ///< the editor's box position, logical pixels.
|
double x = 0.0; ///< the editor's box position, logical pixels.
|
||||||
double y = 0.0;
|
double y = 0.0;
|
||||||
|
|
||||||
|
/// Whether two nodes are the same node for the *engine's* purposes.
|
||||||
|
///
|
||||||
|
/// `x`/`y` are the canvas's own and no op reads them, so a graph that only moved is not a change:
|
||||||
|
/// rebuilding for it replaces the buffer and drops its history -- and in a file view that means
|
||||||
|
/// re-reading the file -- to store two numbers nothing but the editor draws with.
|
||||||
|
bool operator==(const MathNode& other) const {
|
||||||
|
return op == other.op && source == other.source && in_a == other.in_a && in_b == other.in_b &&
|
||||||
|
gain == other.gain && offset == other.offset && fc == other.fc && q == other.q &&
|
||||||
|
lo == other.lo && hi == other.hi;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A derived channel: a graph of nodes and which node is the answer.
|
/// A derived channel: a graph of nodes and which node is the answer.
|
||||||
@@ -67,6 +78,11 @@ struct MathChannel {
|
|||||||
bool enabled = false;
|
bool enabled = false;
|
||||||
std::vector<MathNode> nodes;
|
std::vector<MathNode> nodes;
|
||||||
int output = -1; ///< the node whose value is the channel's.
|
int output = -1; ///< the node whose value is the channel's.
|
||||||
|
|
||||||
|
bool operator==(const MathChannel& other) const {
|
||||||
|
return name == other.name && unit == other.unit && enabled == other.enabled &&
|
||||||
|
output == other.output && nodes == other.nodes;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/// The per-channel runtime: the filter memory and the per-node scratch. Kept apart from the
|
/// The per-channel runtime: the filter memory and the per-node scratch. Kept apart from the
|
||||||
|
|||||||
+127
-2
@@ -257,6 +257,65 @@ void ScopeModel::append_math_signals_locked() {
|
|||||||
|
|
||||||
void ScopeModel::set_math_channels(std::vector<MathChannel> channels) {
|
void ScopeModel::set_math_channels(std::vector<MathChannel> channels) {
|
||||||
std::lock_guard<std::mutex> lock(mutex_);
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
// An identical list is not a change. The rebuild below replaces the buffer and drops its history --
|
||||||
|
// which a file view then has to re-read from disk -- and `apply_math_channels()` runs this on every
|
||||||
|
// session start, where a session with no graphs hands over an empty list against a model that has
|
||||||
|
// none either. Comparing the graphs is what makes that call free.
|
||||||
|
if (channels == math_) return;
|
||||||
|
// An edit of an existing graph -- the same channels, names and enables -- does not move the *plan*, which
|
||||||
|
// is a partition over the channels. Nothing has to be rebuilt, and every other channel keeps its history:
|
||||||
|
// only the edited graphs, and the channels that read them, are recomputed.
|
||||||
|
bool same_set = channels.size() == math_.size();
|
||||||
|
for (std::size_t i = 0; same_set && i < channels.size(); ++i) {
|
||||||
|
same_set = channels[i].name == math_[i].name && channels[i].enabled == math_[i].enabled;
|
||||||
|
}
|
||||||
|
if (same_set) {
|
||||||
|
const auto math_index_of = [this](const std::string& name) -> int {
|
||||||
|
for (std::size_t k = 0; k < math_.size(); ++k) {
|
||||||
|
if (math_[k].name == name) return static_cast<int>(k);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
std::vector<bool> affected(math_.size(), false);
|
||||||
|
std::vector<bool> edited(math_.size(), false);
|
||||||
|
for (std::size_t i = 0; i < channels.size(); ++i) {
|
||||||
|
if (!(channels[i] == math_[i])) {
|
||||||
|
affected[i] = true;
|
||||||
|
edited[i] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A channel's output is another's *input*, so whatever names a changed channel is recomputed too -- and
|
||||||
|
// nothing else is touched at all.
|
||||||
|
for (bool moved = true; moved;) {
|
||||||
|
moved = false;
|
||||||
|
for (std::size_t i = 0; i < channels.size(); ++i) {
|
||||||
|
if (!affected[i]) continue;
|
||||||
|
for (const MathNode& node : channels[i].nodes) {
|
||||||
|
if (node.op != MathOp::Source) continue;
|
||||||
|
const int k = math_index_of(node.source);
|
||||||
|
if (k >= 0 && !affected[static_cast<std::size_t>(k)]) {
|
||||||
|
affected[static_cast<std::size_t>(k)] = true;
|
||||||
|
moved = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
math_ = std::move(channels);
|
||||||
|
const auto index_of = [this](const std::string& name) -> int {
|
||||||
|
for (std::size_t c = 0; c < channels_.size(); ++c) {
|
||||||
|
if (channels_[c].name == name) return static_cast<int>(c);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
// Every affected channel is prepared again: a runtime carried over from the old graph is history for a
|
||||||
|
// filter nobody asked for, and one whose *inputs* changed is history for inputs it no longer has.
|
||||||
|
for (std::size_t i = 0; i < math_.size(); ++i) {
|
||||||
|
if (affected[i]) math_prepare(math_[i], math_rt_[i], part_.slot_dt(), index_of);
|
||||||
|
}
|
||||||
|
recompute_math_locked(affected);
|
||||||
|
generation_.fetch_add(1, std::memory_order_release);
|
||||||
|
return;
|
||||||
|
}
|
||||||
math_ = std::move(channels);
|
math_ = std::move(channels);
|
||||||
// Lift the previous synthetic entries out of the signal list; `append_math_signals_locked()`
|
// Lift the previous synthetic entries out of the signal list; `append_math_signals_locked()`
|
||||||
// puts the current ones back, so an edit is the same kind of structural rebuild as a CONFIG.
|
// puts the current ones back, so an edit is the same kind of structural rebuild as a CONFIG.
|
||||||
@@ -301,6 +360,10 @@ void ScopeModel::push_loaded_slot(const std::vector<f32>& values, f64 t, f64 abs
|
|||||||
const std::size_t channels = channels_.size();
|
const std::size_t channels = channels_.size();
|
||||||
std::vector<f32> slot(channels, 0.0f);
|
std::vector<f32> slot(channels, 0.0f);
|
||||||
for (std::size_t i = 0; i < channels; ++i) {
|
for (std::size_t i = 0; i < channels; ++i) {
|
||||||
|
// A derived channel has no column of its own: `push_math_sample()` below fills it, and pushing the
|
||||||
|
// file's value here as well would put *two* samples in its ring per slot -- a trace at double rate.
|
||||||
|
// The same rule the packet path states in `ChannelInfo::math`: the ingest must skip it.
|
||||||
|
if (channels_[i].math) continue;
|
||||||
const f32 v = i < values.size() ? values[i] : 0.0f;
|
const f32 v = i < values.size() ? values[i] : 0.0f;
|
||||||
store.values(i).push(v);
|
store.values(i).push(v);
|
||||||
slot[i] = v;
|
slot[i] = v;
|
||||||
@@ -310,7 +373,18 @@ void ScopeModel::push_loaded_slot(const std::vector<f32>& values, f64 t, f64 abs
|
|||||||
push_math_sample(slot);
|
push_math_sample(slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::uint64_t ScopeModel::stored_slots() {
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
return store_ ? store_->time().size() : 0;
|
||||||
|
}
|
||||||
|
|
||||||
void ScopeModel::push_math_sample(const std::vector<f32>& sample) {
|
void ScopeModel::push_math_sample(const std::vector<f32>& sample) {
|
||||||
|
// The producer's entry: every derived channel, and timed (see the cost sampling below).
|
||||||
|
math_apply_locked(sample, nullptr, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScopeModel::math_apply_locked(const std::vector<f32>& sample, const std::vector<bool>* write_mask,
|
||||||
|
bool timed) {
|
||||||
// The common case is no derived channels at all, and this runs once per *stored slot* -- so the
|
// The common case is no derived channels at all, and this runs once per *stored slot* -- so the
|
||||||
// early-out is what keeps a stream with no math from paying for the feature.
|
// early-out is what keeps a stream with no math from paying for the feature.
|
||||||
if (math_.empty()) return;
|
if (math_.empty()) return;
|
||||||
@@ -319,9 +393,14 @@ void ScopeModel::push_math_sample(const std::vector<f32>& sample) {
|
|||||||
// slot is itself able to drop packets, and timing one slot at a time charges it a whole clock
|
// slot is itself able to drop packets, and timing one slot at a time charges it a whole clock
|
||||||
// read. A run amortises both: one pair of reads per 64 measured slots, one pair per 65536 slots
|
// read. A run amortises both: one pair of reads per 64 measured slots, one pair per 65536 slots
|
||||||
// overall, and the figure converges over the millions this counter sees.
|
// overall, and the figure converges over the millions this counter sees.
|
||||||
const std::uint64_t slot = math_slot_count_++;
|
// The sampling counts *producer* slots: a recompute must not inflate `math_slots_`, which the dump
|
||||||
|
// reports as the run's own cadence.
|
||||||
|
std::uint64_t slot = 0;
|
||||||
|
if (timed) {
|
||||||
|
slot = math_slot_count_++;
|
||||||
math_slots_.store(math_slot_count_, std::memory_order_relaxed);
|
math_slots_.store(math_slot_count_, std::memory_order_relaxed);
|
||||||
const bool timed = slot % kMathCostSample == 0;
|
timed = slot % kMathCostSample == 0;
|
||||||
|
}
|
||||||
if (timed) math_cost_t0_ = now_ns();
|
if (timed) math_cost_t0_ = now_ns();
|
||||||
// Values for this slot, extended as the derived ones are computed: a graph may name another math
|
// Values for this slot, extended as the derived ones are computed: a graph may name another math
|
||||||
// channel, and nodes are evaluated in list order, so by the time one is asked for it is there.
|
// channel, and nodes are evaluated in list order, so by the time one is asked for it is there.
|
||||||
@@ -334,7 +413,11 @@ void ScopeModel::push_math_sample(const std::vector<f32>& sample) {
|
|||||||
if (channel < 0 || m >= math_rt_.size()) continue;
|
if (channel < 0 || m >= math_rt_.size()) continue;
|
||||||
const double out =
|
const double out =
|
||||||
math_step(math_[m], math_rt_[m], math_values_.data(), math_values_.size());
|
math_step(math_[m], math_rt_[m], math_values_.data(), math_values_.size());
|
||||||
|
// A recompute computes every channel -- the graphs are a list walked in order, so a channel's inputs
|
||||||
|
// have to be there -- but only the affected ones take the sample.
|
||||||
|
if (write_mask == nullptr || (*write_mask)[m]) {
|
||||||
live().values(static_cast<std::size_t>(channel)).push(static_cast<f32>(out));
|
live().values(static_cast<std::size_t>(channel)).push(static_cast<f32>(out));
|
||||||
|
}
|
||||||
if (static_cast<std::size_t>(channel) < math_values_.size()) {
|
if (static_cast<std::size_t>(channel) < math_values_.size()) {
|
||||||
math_values_[static_cast<std::size_t>(channel)] = static_cast<f32>(out);
|
math_values_[static_cast<std::size_t>(channel)] = static_cast<f32>(out);
|
||||||
}
|
}
|
||||||
@@ -347,6 +430,48 @@ void ScopeModel::push_math_sample(const std::vector<f32>& sample) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ScopeModel::recompute_math_locked(const std::vector<bool>& affected) {
|
||||||
|
if (!store_ || math_.empty()) return;
|
||||||
|
BufferStore& store = *store_;
|
||||||
|
const std::size_t slots = static_cast<std::size_t>(store.time().size());
|
||||||
|
if (slots == 0) return;
|
||||||
|
|
||||||
|
std::vector<bool> derived(channels_.size(), false);
|
||||||
|
for (std::size_t m = 0; m < math_.size(); ++m) {
|
||||||
|
const int channel = m < math_channel_.size() ? math_channel_[m] : -1;
|
||||||
|
if (channel < 0) continue;
|
||||||
|
derived[static_cast<std::size_t>(channel)] = true;
|
||||||
|
// Only the affected channels start again. The rest keep their samples *and* their runtimes: their graphs
|
||||||
|
// are untouched, so the state they hold is the answer to the inputs they have always had.
|
||||||
|
if (affected[m]) store.values(static_cast<std::size_t>(channel)).clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// One bulk copy per channel per chunk, through the ring's own reader. A derived channel is fed a zero:
|
||||||
|
// `math_apply_locked()` overwrites its value as it walks the list in order, and reading its half-rebuilt
|
||||||
|
// ring here would read out of range.
|
||||||
|
constexpr std::size_t kChunk = 1u << 14;
|
||||||
|
const std::size_t base = static_cast<std::size_t>(store.time().base());
|
||||||
|
std::vector<std::vector<f32>> inputs(channels_.size());
|
||||||
|
for (std::size_t c = 0; c < channels_.size(); ++c) {
|
||||||
|
if (!derived[c]) inputs[c].resize(kChunk);
|
||||||
|
}
|
||||||
|
std::vector<f32> slot(channels_.size(), 0.0f);
|
||||||
|
for (std::size_t start = 0; start < slots; start += kChunk) {
|
||||||
|
const std::size_t n = std::min(kChunk, slots - start);
|
||||||
|
for (std::size_t c = 0; c < channels_.size(); ++c) {
|
||||||
|
if (derived[c]) continue;
|
||||||
|
store.values(c).copy_from(base + start, n, inputs[c].data());
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < n; ++i) {
|
||||||
|
for (std::size_t c = 0; c < channels_.size(); ++c) {
|
||||||
|
slot[c] = derived[c] ? 0.0f : inputs[c][i];
|
||||||
|
}
|
||||||
|
math_apply_locked(slot, &affected, /*timed=*/false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The pyramid is the consumer's and the tick publishes it; the samples are written here.
|
||||||
|
}
|
||||||
|
|
||||||
void ScopeModel::rebuild_channels() {
|
void ScopeModel::rebuild_channels() {
|
||||||
channels_.clear();
|
channels_.clear();
|
||||||
// Which channel each math channel ended up with, by name: a disabled one has none, and the
|
// Which channel each math channel ended up with, by name: a disabled one has none, and the
|
||||||
|
|||||||
@@ -281,6 +281,19 @@ class ScopeModel {
|
|||||||
/// evaluated from those, in order, so a math channel may use another one.
|
/// evaluated from those, in order, so a math channel may use another one.
|
||||||
void push_math_sample(const std::vector<f32>& sample);
|
void push_math_sample(const std::vector<f32>& sample);
|
||||||
|
|
||||||
|
/// Applies the graphs to one stored slot. `write_mask`, when given, names the derived channels whose
|
||||||
|
/// rings may be written -- `nullptr` writes all of them, which is what the producer does; a recompute
|
||||||
|
/// passes the channels a graph change actually affects, so the others keep the samples they have.
|
||||||
|
void math_apply_locked(const std::vector<f32>& sample, const std::vector<bool>* write_mask, bool timed);
|
||||||
|
|
||||||
|
/// Recomputes the derived channels in `affected` from the samples the store already holds: their rings are
|
||||||
|
/// rebuilt from scratch, and the channels a chart change does *not* touch are left exactly as they were.
|
||||||
|
void recompute_math_locked(const std::vector<bool>& affected);
|
||||||
|
|
||||||
|
/// How many slots the store currently holds. A file view reads it to tell a structural change that
|
||||||
|
/// dropped the buffer (0) from one that could not have (see `reload_file_view()` in ui.cpp).
|
||||||
|
std::uint64_t stored_slots();
|
||||||
|
|
||||||
/// Pushes one stored slot of a **file** load -- the values, the shared clock and the absolute
|
/// Pushes one stored slot of a **file** load -- the values, the shared clock and the absolute
|
||||||
/// clock -- and then the derived channels, in the producer's own order and under the model's own
|
/// clock -- and then the derived channels, in the producer's own order and under the model's own
|
||||||
/// lock. The loader cannot simply hold the lock and push into `live()` itself: every setter it
|
/// lock. The loader cannot simply hold the lock and push into `live()` itself: every setter it
|
||||||
|
|||||||
+133
@@ -2534,6 +2534,63 @@ void remove_trace(App& app, int row, int slot) {
|
|||||||
app.rows->assign(row, slot, std::string(), std::string(), 0);
|
app.rows->assign(row, slot, std::string(), std::string(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Disables every math channel that (transitively) reads `name`, and answers with their names.
|
||||||
|
///
|
||||||
|
/// A derived channel cannot be computed from a signal nobody acquires: `Source` resolves to a *channel*, and a
|
||||||
|
/// disabled signal has none -- so a channel left enabled would silently compute nothing. The rule is that the
|
||||||
|
/// channels which depend on a signal follow it off, which is also what the pane's own switch would do by hand.
|
||||||
|
/// The dependency is transitive: one math channel may name another, so a channel reading a disabled one goes
|
||||||
|
/// too, and the loop runs to a fixpoint for that.
|
||||||
|
std::vector<std::string> disable_dependent_math_channels(App& app, const std::string& name) {
|
||||||
|
std::vector<std::string> off;
|
||||||
|
if (name.empty()) return off;
|
||||||
|
const auto math_index_of = [&app](const std::string& n) -> int {
|
||||||
|
for (std::size_t k = 0; k < app.math.size(); ++k) {
|
||||||
|
if (app.math[k].name == n) return static_cast<int>(k);
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
};
|
||||||
|
for (bool moved = true; moved;) {
|
||||||
|
moved = false;
|
||||||
|
for (MathChannel& channel : app.math) {
|
||||||
|
if (!channel.enabled) continue;
|
||||||
|
for (const MathNode& node : channel.nodes) {
|
||||||
|
if (node.op != MathOp::Source) continue;
|
||||||
|
const int k = math_index_of(node.source);
|
||||||
|
const bool reads_it = node.source == name;
|
||||||
|
const bool reads_a_disabled_one = k >= 0 && !app.math[static_cast<std::size_t>(k)].enabled;
|
||||||
|
if (!reads_it && !reads_a_disabled_one) continue;
|
||||||
|
channel.enabled = false;
|
||||||
|
off.push_back(channel.name);
|
||||||
|
moved = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return off;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Takes a signal off every plot, and says how many traces went.
|
||||||
|
///
|
||||||
|
/// Switching a signal off means "do not acquire this", so a trace of it left on a plot draws nothing: a slot
|
||||||
|
/// that reads `--`, takes the place a live signal could have had, and blocks the drop that would fill it. The
|
||||||
|
/// slot is what is cleared, and the caller does the follow-ups once (a relayout, the panels, the layout save)
|
||||||
|
/// rather than per slot.
|
||||||
|
int remove_signal_from_plots(App& app, const std::string& name) {
|
||||||
|
if (name.empty()) return 0;
|
||||||
|
int removed = 0;
|
||||||
|
for (int row = 0; row < app.rows->row_count(); ++row) {
|
||||||
|
for (int slot = 0; slot < RowSet::kSlotsPerRow; ++slot) {
|
||||||
|
// The slot remembers the *signal* (and its element), never the channel index -- the channel list is
|
||||||
|
// renumbered on every enable change, which is what this is reacting to.
|
||||||
|
if (app.rows->row(row).signal_name[static_cast<std::size_t>(slot)] != name) continue;
|
||||||
|
remove_trace(app, row, slot);
|
||||||
|
++removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
void on_remove_clicked(GtkButton* button, gpointer user) {
|
void on_remove_clicked(GtkButton* button, gpointer user) {
|
||||||
(void)user;
|
(void)user;
|
||||||
auto* target = static_cast<RemoveTarget*>(g_object_get_data(G_OBJECT(button), "udpscope-remove"));
|
auto* target = static_cast<RemoveTarget*>(g_object_get_data(G_OBJECT(button), "udpscope-remove"));
|
||||||
@@ -4009,10 +4066,16 @@ gboolean tick(gpointer user) {
|
|||||||
}
|
}
|
||||||
if (glance.size() > app->glance_max_chars) app->glance_max_chars = glance.size();
|
if (glance.size() > app->glance_max_chars) app->glance_max_chars = glance.size();
|
||||||
|
|
||||||
|
// A notice is not the glance. It is prefixed onto the same line, which is exactly where a reader
|
||||||
|
// skimming the routine text will not see it -- "there is no clear feedback" was that, with a refusal
|
||||||
|
// ("plot 2 holds four signals already") looking like part of the status. The stock `.warning` class is
|
||||||
|
// what makes a message look like a message, and it goes when the notice does.
|
||||||
if (!app->notice.empty() && g_get_monotonic_time() < app->notice_until_us) {
|
if (!app->notice.empty() && g_get_monotonic_time() < app->notice_until_us) {
|
||||||
|
gtk_widget_add_css_class(app->status_label, "warning");
|
||||||
const std::string with_notice = app->notice + " | " + glance;
|
const std::string with_notice = app->notice + " | " + glance;
|
||||||
gtk_label_set_text(GTK_LABEL(app->status_label), with_notice.c_str());
|
gtk_label_set_text(GTK_LABEL(app->status_label), with_notice.c_str());
|
||||||
} else {
|
} else {
|
||||||
|
gtk_widget_remove_css_class(app->status_label, "warning");
|
||||||
app->notice.clear();
|
app->notice.clear();
|
||||||
gtk_label_set_text(GTK_LABEL(app->status_label), glance.c_str());
|
gtk_label_set_text(GTK_LABEL(app->status_label), glance.c_str());
|
||||||
}
|
}
|
||||||
@@ -4114,6 +4177,32 @@ GtkWidget* build_window_control(App& app) {
|
|||||||
return app.window_button;
|
return app.window_button;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Any press in the window closes the time-window popover.
|
||||||
|
///
|
||||||
|
/// `GtkPopover`'s autohide does this by itself for as long as GTK holds the popover's grab -- and the grab
|
||||||
|
/// is the thing that can go missing (a window manager moving it, a nested popover taking it for the unit
|
||||||
|
/// menu, and every headless run, where there is no manager to hand one out at all), after which a click
|
||||||
|
/// anywhere else in the window leaves the bubble up. That is the "sometimes it is awkward to close".
|
||||||
|
///
|
||||||
|
/// A press that reaches the window's own surface was *not* inside the popover: a popover is its own
|
||||||
|
/// surface, so a click within it never arrives here. That is what makes this the rule stated outright
|
||||||
|
/// rather than a grab away from working -- and it needs no hit test beyond leaving the button alone,
|
||||||
|
/// whose own click is what toggles the bubble shut.
|
||||||
|
void on_window_popover_press(GtkGestureClick*, int, double x, double y, gpointer user) {
|
||||||
|
App* app = static_cast<App*>(user);
|
||||||
|
if (app->window_button == nullptr ||
|
||||||
|
!gtk_menu_button_get_active(GTK_MENU_BUTTON(app->window_button))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// The button itself is left to its own click, which toggles the bubble shut -- closing it here would
|
||||||
|
// only have the toggle open it again. Anything else on the window's surface closes it.
|
||||||
|
for (GtkWidget* w = gtk_widget_pick(app->window, x, y, GTK_PICK_DEFAULT);
|
||||||
|
w != nullptr && w != app->window; w = gtk_widget_get_parent(w)) {
|
||||||
|
if (w == app->window_button) return;
|
||||||
|
}
|
||||||
|
gtk_menu_button_popdown(GTK_MENU_BUTTON(app->window_button));
|
||||||
|
}
|
||||||
|
|
||||||
/// The one place a key turns into an action. Toggles go through
|
/// The one place a key turns into an action. Toggles go through
|
||||||
/// gtk_toggle_button_set_active(), which emits ::toggled and so runs exactly the handler a
|
/// gtk_toggle_button_set_active(), which emits ::toggled and so runs exactly the handler a
|
||||||
/// click would; the non-toggles call their click handlers with a null button, which they
|
/// click would; the non-toggles call their click handlers with a null button, which they
|
||||||
@@ -4179,6 +4268,37 @@ gboolean on_app_key_pressed(GtkEventControllerKey*, guint keyval, guint, GdkModi
|
|||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Re-reads the file a *file view* is showing; true when something was reloaded.
|
||||||
|
///
|
||||||
|
/// Any structural change -- a math channel added, a signal enabled, a plan change -- rebuilds the buffer
|
||||||
|
/// and drops its history. That is right for a stream, whose producer refills it from the next packet. A
|
||||||
|
/// file has no producer, so the same rebuild leaves *every* channel reading `0 samples` and the view is
|
||||||
|
/// simply empty: "adding a math channel breaks the plot" was this, and it is the same emptiness whichever
|
||||||
|
/// structural change caused it. The file's own path is the only thing that can refill it, so it is loaded
|
||||||
|
/// again -- which is also what computes the derived channels over it, since the load's own pushes run the
|
||||||
|
/// math once the set is in place.
|
||||||
|
bool reload_file_view(App& app) {
|
||||||
|
if (!app.replaying && app.model->config().open_waveform.empty()) return false;
|
||||||
|
// Nothing to restore: a change that really happened leaves the store empty in a file view -- the
|
||||||
|
// rebuild replaces it and there is no producer to refill it -- while one that found nothing to do
|
||||||
|
// leaves it full. Reading the file again for the second kind is the waste this avoids.
|
||||||
|
if (app.model->stored_slots() > 0) return false;
|
||||||
|
const std::string path =
|
||||||
|
!app.waveform_name.empty() ? app.waveform_name : app.model->config().open_waveform;
|
||||||
|
if (path.empty()) return false;
|
||||||
|
WaveformLoadReport report;
|
||||||
|
std::string error;
|
||||||
|
if (!load_waveform_into_model(*app.model, path, &report, &error)) {
|
||||||
|
set_notice(app, "cannot reload " + path + " after the change: " + error, 6);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Said out loud because it is the one place a file is read twice: a structural change really did
|
||||||
|
// drop the buffer, and the file is the only thing that can put it back.
|
||||||
|
std::fprintf(stderr, "[udpscope] reloaded %s after a structural change: %llu samples back\n",
|
||||||
|
path.c_str(), static_cast<unsigned long long>(report.samples));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
/// The header bar carries only the controls you reach for *while watching a signal*;
|
/// The header bar carries only the controls you reach for *while watching a signal*;
|
||||||
/// everything you set up once lives in the overflow menu. Nine text buttons in a
|
/// everything you set up once lives in the overflow menu. Nine text buttons in a
|
||||||
/// header bar is a wall of words to read past every time you want the one you need,
|
/// header bar is a wall of words to read past every time you want the one you need,
|
||||||
@@ -4628,6 +4748,13 @@ void on_activate(GtkApplication* application, gpointer user) {
|
|||||||
g_signal_connect(keys, "key-pressed", G_CALLBACK(on_app_key_pressed), app);
|
g_signal_connect(keys, "key-pressed", G_CALLBACK(on_app_key_pressed), app);
|
||||||
gtk_widget_add_controller(app->window, keys);
|
gtk_widget_add_controller(app->window, keys);
|
||||||
|
|
||||||
|
// Capture phase, unlike the keys above: a press belongs to whatever widget is under it (the plot's
|
||||||
|
// gestures among them), and the point here is to see every one of them, not to win none of them.
|
||||||
|
GtkGesture* press = gtk_gesture_click_new();
|
||||||
|
gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(press), GTK_PHASE_CAPTURE);
|
||||||
|
g_signal_connect(press, "pressed", G_CALLBACK(on_window_popover_press), app);
|
||||||
|
gtk_widget_add_controller(app->window, GTK_EVENT_CONTROLLER(press));
|
||||||
|
|
||||||
gtk_window_present(GTK_WINDOW(app->window));
|
gtk_window_present(GTK_WINDOW(app->window));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5248,6 +5375,12 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
|||||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_KEYS_MS")) {
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_KEYS_MS")) {
|
||||||
g_timeout_add(static_cast<guint>(std::atoi(env)), keys_probe, &app);
|
g_timeout_add(static_cast<guint>(std::atoi(env)), keys_probe, &app);
|
||||||
}
|
}
|
||||||
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_WINDOWPOP_MS")) {
|
||||||
|
g_timeout_add(static_cast<guint>(std::atoi(env)), windowpop_probe_step, &app);
|
||||||
|
}
|
||||||
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_KEYS_MS")) {
|
||||||
|
g_timeout_add(static_cast<guint>(std::atoi(env)), keys_probe, &app);
|
||||||
|
}
|
||||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_COLOR_MS")) {
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_COLOR_MS")) {
|
||||||
g_timeout_add(static_cast<guint>(std::atoi(env)), color_probe, &app);
|
g_timeout_add(static_cast<guint>(std::atoi(env)), color_probe, &app);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,8 +46,6 @@
|
|||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
|
||||||
</child>
|
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox" id="math_body">
|
<object class="GtkBox" id="math_body">
|
||||||
<property name="orientation">horizontal</property>
|
<property name="orientation">horizontal</property>
|
||||||
@@ -81,4 +79,6 @@
|
|||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
</interface>
|
</interface>
|
||||||
@@ -1189,6 +1189,10 @@ void place_measure_marks(App& app);
|
|||||||
void place_search_marks(App& app);
|
void place_search_marks(App& app);
|
||||||
void update_measures(App& app, bool force, bool accumulate);
|
void update_measures(App& app, bool force, bool accumulate);
|
||||||
void apply_math_channels(App& app);
|
void apply_math_channels(App& app);
|
||||||
|
|
||||||
|
/// Re-reads the file a file view is showing after a structural change rebuilt the buffer and dropped
|
||||||
|
/// its history -- see the definition in ui.cpp. True when something was reloaded.
|
||||||
|
bool reload_file_view(App& app);
|
||||||
void add_math_channel(App& app);
|
void add_math_channel(App& app);
|
||||||
std::vector<std::string> math_source_names(App& app);
|
std::vector<std::string> math_source_names(App& app);
|
||||||
std::string math_dialog_problem(const MathDialog* d);
|
std::string math_dialog_problem(const MathDialog* d);
|
||||||
@@ -1246,6 +1250,8 @@ void on_y_auto_clicked(GtkButton* button, gpointer user);
|
|||||||
void on_fixed_range_apply(GtkButton*, gpointer user);
|
void on_fixed_range_apply(GtkButton*, gpointer user);
|
||||||
void on_y_fixed_clicked(GtkButton* button, gpointer user);
|
void on_y_fixed_clicked(GtkButton* button, gpointer user);
|
||||||
void remove_trace(App& app, int row, int slot);
|
void remove_trace(App& app, int row, int slot);
|
||||||
|
int remove_signal_from_plots(App& app, const std::string& name);
|
||||||
|
std::vector<std::string> disable_dependent_math_channels(App& app, const std::string& name);
|
||||||
void on_remove_clicked(GtkButton* button, gpointer user);
|
void on_remove_clicked(GtkButton* button, gpointer user);
|
||||||
GtkWidget* menu_icon_button(const char* icon, const char* label);
|
GtkWidget* menu_icon_button(const char* icon, const char* label);
|
||||||
void on_row_menu_show_remove(GtkButton*, gpointer user);
|
void on_row_menu_show_remove(GtkButton*, gpointer user);
|
||||||
@@ -1345,6 +1351,11 @@ gboolean zoomkeys_probe(gpointer user);
|
|||||||
void menus_probe_next(App& a, int step, guint delay_ms);
|
void menus_probe_next(App& a, int step, guint delay_ms);
|
||||||
void menu_row_labels(GtkWidget* box, GtkWidget** label, GtkWidget** accel);
|
void menu_row_labels(GtkWidget* box, GtkWidget** label, GtkWidget** accel);
|
||||||
gboolean menus_probe_step(gpointer user);
|
gboolean menus_probe_step(gpointer user);
|
||||||
|
gboolean windowpop_probe_step(gpointer user);
|
||||||
|
|
||||||
|
/// Closes the time-window popover when a press lands elsewhere in the window (see ui.cpp). Declared for
|
||||||
|
/// the probe, which drives it and presses the button to check the one case that must *not* close it.
|
||||||
|
void on_window_popover_press(GtkGestureClick* gesture, int n_press, double x, double y, gpointer user);
|
||||||
gboolean color_probe(gpointer user);
|
gboolean color_probe(gpointer user);
|
||||||
gboolean reseq_probe(gpointer user);
|
gboolean reseq_probe(gpointer user);
|
||||||
void capture_probe_next(App& a, int step, guint delay_ms);
|
void capture_probe_next(App& a, int step, guint delay_ms);
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ void apply_math_channels(App& app) {
|
|||||||
// `set_math_channels()` takes the model's lock itself -- holding it here is a self-deadlock on a
|
// `set_math_channels()` takes the model's lock itself -- holding it here is a self-deadlock on a
|
||||||
// non-recursive mutex, and it is exactly how the first version of the probe hung the app.
|
// non-recursive mutex, and it is exactly how the first version of the probe hung the app.
|
||||||
app.model->set_math_channels(app.math);
|
app.model->set_math_channels(app.math);
|
||||||
|
// The structural rebuild above replaced the buffer and dropped its history. A stream's producer
|
||||||
|
// refills that; a file's nothing does, so the view is re-read -- and with the math set now in place
|
||||||
|
// the reload computes the derived channels over the file as it goes.
|
||||||
|
reload_file_view(app);
|
||||||
rebuild_math_pane(app);
|
rebuild_math_pane(app);
|
||||||
schedule_layout_save(app);
|
schedule_layout_save(app);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -386,6 +386,35 @@ gboolean math_probe(gpointer user) {
|
|||||||
if (ed->gain != nullptr) gtk_spin_button_set_value(GTK_SPIN_BUTTON(ed->gain), 2.0);
|
if (ed->gain != nullptr) gtk_spin_button_set_value(GTK_SPIN_BUTTON(ed->gain), 2.0);
|
||||||
if (ed->offset != nullptr) gtk_spin_button_set_value(GTK_SPIN_BUTTON(ed->offset), 0.0);
|
if (ed->offset != nullptr) gtk_spin_button_set_value(GTK_SPIN_BUTTON(ed->offset), 0.0);
|
||||||
on_math_param_changed(nullptr, ed);
|
on_math_param_changed(nullptr, ed);
|
||||||
|
// The frame, structurally: the canvas being *in* the dialog and given an allocation. A `.ui`
|
||||||
|
// mistake this has already paid for once -- `math_body` nested under the window instead of the
|
||||||
|
// outer box -- leaves the canvas visible but parentless: it never maps, stays 0x0 and draws
|
||||||
|
// nothing, which a user reads as "the editor opens and the canvas is blank". The values below
|
||||||
|
// cannot see that, and the reading is delayed because an allocation only exists after a layout.
|
||||||
|
g_timeout_add(500, +[](gpointer u) -> gboolean {
|
||||||
|
App* app = static_cast<App*>(u);
|
||||||
|
if (app->math_dialog == nullptr) {
|
||||||
|
std::fprintf(stderr, "[math-probe] editor frame: (dialog closed before the reading)\n");
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
MathDialog* m = static_cast<MathDialog*>(
|
||||||
|
g_object_get_data(G_OBJECT(app->math_dialog), "udpscope-math-dialog"));
|
||||||
|
if (m == nullptr) return G_SOURCE_REMOVE;
|
||||||
|
const auto in_dialog = [app](GtkWidget* w) {
|
||||||
|
for (GtkWidget* p = w; p != nullptr; p = gtk_widget_get_parent(p)) {
|
||||||
|
if (p == app->math_dialog) return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[math-probe] editor frame: canvas=%dx%d mapped=%d in_dialog=%d "
|
||||||
|
"name_in_dialog=%d\n",
|
||||||
|
m->canvas != nullptr ? gtk_widget_get_width(m->canvas) : -1,
|
||||||
|
m->canvas != nullptr ? gtk_widget_get_height(m->canvas) : -1,
|
||||||
|
m->canvas != nullptr ? gtk_widget_get_mapped(m->canvas) : -1,
|
||||||
|
in_dialog(m->canvas), in_dialog(m->name));
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}, a);
|
||||||
const std::size_t built = ed->nodes.size();
|
const std::size_t built = ed->nodes.size();
|
||||||
const double gain_read = ed->nodes.empty() ? 0.0 : ed->nodes.back().gain;
|
const double gain_read = ed->nodes.empty() ? 0.0 : ed->nodes.back().gain;
|
||||||
const double offset_read = ed->nodes.empty() ? 0.0 : ed->nodes.back().offset;
|
const double offset_read = ed->nodes.empty() ? 0.0 : ed->nodes.back().offset;
|
||||||
@@ -395,7 +424,15 @@ gboolean math_probe(gpointer user) {
|
|||||||
ed->name != nullptr ? gtk_editable_get_text(GTK_EDITABLE(ed->name)) : "-";
|
ed->name != nullptr ? gtk_editable_get_text(GTK_EDITABLE(ed->name)) : "-";
|
||||||
const std::string unit_read =
|
const std::string unit_read =
|
||||||
ed->unit != nullptr ? gtk_editable_get_text(GTK_EDITABLE(ed->unit)) : "-";
|
ed->unit != nullptr ? gtk_editable_get_text(GTK_EDITABLE(ed->unit)) : "-";
|
||||||
|
// The edit must not cost the buffer. The plan is a partition over the channels and an edit does not
|
||||||
|
// move it, so every channel keeps its history and only the edited graph -- and whatever reads it -- is
|
||||||
|
// recomputed. Before this, `set_math_channels()` rebuilt the store and every channel started again.
|
||||||
|
const std::uint64_t slots_before = a->model->stored_slots();
|
||||||
on_math_apply(nullptr, ed); // frees the dialog: nothing below may touch `ed`
|
on_math_apply(nullptr, ed); // frees the dialog: nothing below may touch `ed`
|
||||||
|
const std::uint64_t slots_after = a->model->stored_slots();
|
||||||
|
std::fprintf(stderr, "[math-probe] graph edit: stored_slots %llu -> %llu (want the second at least the first, and non-zero)\n",
|
||||||
|
static_cast<unsigned long long>(slots_before),
|
||||||
|
static_cast<unsigned long long>(slots_after));
|
||||||
const std::size_t saved = a->math.empty()
|
const std::size_t saved = a->math.empty()
|
||||||
? 0u
|
? 0u
|
||||||
: a->math[static_cast<std::size_t>(index)].nodes.size();
|
: a->math[static_cast<std::size_t>(index)].nodes.size();
|
||||||
@@ -1835,6 +1872,64 @@ gboolean enable_all_probe(gpointer user) {
|
|||||||
std::fprintf(stderr, "[enable-all-probe] %zu channel(s) enabled and assigned to plots\n",
|
std::fprintf(stderr, "[enable-all-probe] %zu channel(s) enabled and assigned to plots\n",
|
||||||
a->model->channel_count());
|
a->model->channel_count());
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
|
// Then switch one off through its own switch in the signal list: a disabled signal must leave the plots,
|
||||||
|
// not sit there drawing nothing in a slot a live one could take. Late enough that a math probe armed behind
|
||||||
|
// this one has had its graphs installed -- the dependants are the point of the second line.
|
||||||
|
g_timeout_add(2000, +[](gpointer u) -> gboolean {
|
||||||
|
App* app = static_cast<App*>(u);
|
||||||
|
if (app->model->channels().empty() || app->signal_list == nullptr) return G_SOURCE_REMOVE;
|
||||||
|
const std::size_t signal = app->model->channels().front().signal;
|
||||||
|
const std::string name = app->model->channels().front().name;
|
||||||
|
GtkWidget* tick = nullptr;
|
||||||
|
for (GtkWidget* row_widget = gtk_widget_get_first_child(app->signal_list);
|
||||||
|
row_widget != nullptr && tick == nullptr;
|
||||||
|
row_widget = gtk_widget_get_next_sibling(row_widget)) {
|
||||||
|
// The tick sits a box inside the row, which is the shape `signal_tick_visible()` walks too.
|
||||||
|
for (GtkWidget* box = gtk_widget_get_first_child(row_widget); box != nullptr && tick == nullptr;
|
||||||
|
box = gtk_widget_get_next_sibling(box)) {
|
||||||
|
for (GtkWidget* w = gtk_widget_get_first_child(box); w != nullptr;
|
||||||
|
w = gtk_widget_get_next_sibling(w)) {
|
||||||
|
if (!GTK_IS_CHECK_BUTTON(w)) continue;
|
||||||
|
if (GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(w), "udpscope-signal")) != signal) continue;
|
||||||
|
tick = w;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (tick == nullptr) {
|
||||||
|
std::fprintf(stderr, "[enable-all-probe] no acquire tick found for signal %zu\n", signal);
|
||||||
|
std::fflush(stderr);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
gtk_check_button_set_active(GTK_CHECK_BUTTON(tick), FALSE);
|
||||||
|
int left = 0;
|
||||||
|
for (int r = 0; r < app->rows->row_count(); ++r) {
|
||||||
|
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
|
||||||
|
if (app->rows->row(r).signal_name[static_cast<std::size_t>(s)] == name) ++left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Which *math* channels the disabled signal took with it: a derived channel cannot be computed from a
|
||||||
|
// signal nobody acquires, so the ones that read it follow it off -- and their traces go too.
|
||||||
|
std::string off_list;
|
||||||
|
for (const MathChannel& m : app->math) {
|
||||||
|
if (m.enabled) continue;
|
||||||
|
if (!off_list.empty()) off_list += ",";
|
||||||
|
off_list += m.name;
|
||||||
|
}
|
||||||
|
// Both halves: the channels that read it went off, and the ones that do not are still on.
|
||||||
|
std::string on_list;
|
||||||
|
for (const MathChannel& m : app->math) {
|
||||||
|
if (!m.enabled) continue;
|
||||||
|
if (!on_list.empty()) on_list += ",";
|
||||||
|
on_list += m.name;
|
||||||
|
}
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[enable-all-probe] switched '%s' off: traces_left=%d (want 0) math_off=[%s] "
|
||||||
|
"math_on=[%s] notice='%s'\n",
|
||||||
|
name.c_str(), left, off_list.c_str(), on_list.c_str(), app->notice.c_str());
|
||||||
|
std::fflush(stderr);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}, a);
|
||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2471,6 +2566,63 @@ void menus_probe_next(App& a, int step, guint delay_ms) {
|
|||||||
g_timeout_add(delay_ms, menus_probe_step, &a);
|
g_timeout_add(delay_ms, menus_probe_step, &a);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `UDPSCOPE_PROBE_WINDOWPOP_MS`: the time-window popover, closed by a press elsewhere in the window.
|
||||||
|
///
|
||||||
|
/// Autohide is turned off for the probe, exactly as the menu probe does it: a popover dismisses itself
|
||||||
|
/// when GTK holds its *grab*, and a bare `xvfb-run` has no window manager to hand one out -- so with
|
||||||
|
/// autohide on the bubble would not stay up to be tested. Autohide off is the state a user meets when
|
||||||
|
/// the grab goes missing, which is the report this guards: the press has to close it anyway. The second
|
||||||
|
/// half presses the button itself, which must *not* close it (its own click toggles).
|
||||||
|
gboolean windowpop_probe_step(gpointer user) {
|
||||||
|
static int step = 0;
|
||||||
|
App* a = static_cast<App*>(user);
|
||||||
|
if (a->window == nullptr || a->window_button == nullptr) return G_SOURCE_REMOVE;
|
||||||
|
const auto active = [a] {
|
||||||
|
return gtk_menu_button_get_active(GTK_MENU_BUTTON(a->window_button)) ? 1 : 0;
|
||||||
|
};
|
||||||
|
if (step == 0) {
|
||||||
|
GtkPopover* pop = gtk_menu_button_get_popover(GTK_MENU_BUTTON(a->window_button));
|
||||||
|
if (pop != nullptr) gtk_popover_set_autohide(pop, FALSE);
|
||||||
|
gtk_menu_button_popup(GTK_MENU_BUTTON(a->window_button));
|
||||||
|
step = 1;
|
||||||
|
g_timeout_add(300, windowpop_probe_step, a);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
// How many click gestures the window carries, so that "the handler works" and "the handler is
|
||||||
|
// connected" are read as two facts rather than one.
|
||||||
|
int clicks = 0;
|
||||||
|
{
|
||||||
|
GListModel* controllers = gtk_widget_observe_controllers(a->window);
|
||||||
|
const guint n = controllers != nullptr ? g_list_model_get_n_items(controllers) : 0;
|
||||||
|
for (guint i = 0; i < n; ++i) {
|
||||||
|
GObject* item = G_OBJECT(g_list_model_get_item(controllers, i));
|
||||||
|
if (item != nullptr) {
|
||||||
|
if (GTK_IS_GESTURE_CLICK(item)) ++clicks;
|
||||||
|
g_object_unref(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (controllers != nullptr) g_object_unref(controllers);
|
||||||
|
}
|
||||||
|
std::fprintf(stderr, "[windowpop] opened=%d visible=%d click_gestures=%d\n", active(),
|
||||||
|
gtk_widget_is_visible(GTK_WIDGET(gtk_menu_button_get_popover(
|
||||||
|
GTK_MENU_BUTTON(a->window_button))))
|
||||||
|
? 1
|
||||||
|
: 0,
|
||||||
|
clicks);
|
||||||
|
on_window_popover_press(nullptr, 0, 20.0, 200.0, a);
|
||||||
|
std::fprintf(stderr, "[windowpop] after a press elsewhere: active=%d (want 0)\n", active());
|
||||||
|
gtk_menu_button_popup(GTK_MENU_BUTTON(a->window_button));
|
||||||
|
graphene_rect_t r = GRAPHENE_RECT_INIT(0, 0, 0, 0);
|
||||||
|
if (gtk_widget_compute_bounds(a->window_button, a->window, &r)) {
|
||||||
|
on_window_popover_press(nullptr, 0, r.origin.x + r.size.width / 2.0,
|
||||||
|
r.origin.y + r.size.height / 2.0, a);
|
||||||
|
}
|
||||||
|
std::fprintf(stderr, "[windowpop] after a press on the button: active=%d (want 1)\n", active());
|
||||||
|
gtk_menu_button_popdown(GTK_MENU_BUTTON(a->window_button));
|
||||||
|
std::fflush(stderr);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
gboolean menus_probe_step(gpointer user) {
|
gboolean menus_probe_step(gpointer user) {
|
||||||
App* a = static_cast<App*>(user);
|
App* a = static_cast<App*>(user);
|
||||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||||
@@ -3281,6 +3433,49 @@ gboolean drop_probe(gpointer user) {
|
|||||||
ok ? 1 : 0, a->rows->row(0).name[0].c_str(), a->rows->row(0).name[1].c_str(),
|
ok ? 1 : 0, a->rows->row(0).name[0].c_str(), a->rows->row(0).name[1].c_str(),
|
||||||
a->rows->row(0).name[2].c_str(), a->rows->row(0).name[3].c_str());
|
a->rows->row(0).name[2].c_str(), a->rows->row(0).name[3].c_str());
|
||||||
}
|
}
|
||||||
|
// Fill the row first, so that the drop below is the *refusal*: a drop can otherwise land in a slot
|
||||||
|
// whose signal is named but not acquired, which is a success rather than the case under test.
|
||||||
|
{
|
||||||
|
const std::vector<ChannelInfo>& channels = a->model->channels();
|
||||||
|
for (int slot = 0; slot < RowSet::kSlotsPerRow && !channels.empty(); ++slot) {
|
||||||
|
if (a->rows->slot_bound(0, slot)) continue;
|
||||||
|
const ChannelInfo& ch = channels[static_cast<std::size_t>(slot) % channels.size()];
|
||||||
|
a->rows->assign(0, slot, ch.name, ch.name, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The refusal's *feedback*, a tick later: the status line composes the notice into itself on the
|
||||||
|
// next tick, so this is where both the text and the class that makes it visible can be read.
|
||||||
|
g_timeout_add(500, +[](gpointer u) -> gboolean {
|
||||||
|
App* app = static_cast<App*>(u);
|
||||||
|
// And the drop that the full row has to refuse, fired here so its feedback is what the reading below
|
||||||
|
// sees rather than the last success's.
|
||||||
|
GValue refused_value = G_VALUE_INIT;
|
||||||
|
g_value_init(&refused_value, G_TYPE_STRING);
|
||||||
|
g_value_set_string(&refused_value, "1");
|
||||||
|
const gboolean refused = on_drop_channel(nullptr, &refused_value, 40.0, 20.0, app);
|
||||||
|
g_value_unset(&refused_value);
|
||||||
|
std::fprintf(stderr, "[drop-probe] full row refused the drop: %d (want 0)\n", refused ? 1 : 0);
|
||||||
|
// The message and its class, which are set the moment the drop is refused. The *label* is read in
|
||||||
|
// the later line instead: the status line composes the notice into itself on its own cadence, so
|
||||||
|
// here it may still hold the previous one.
|
||||||
|
std::fprintf(stderr, "[drop-probe] refused feedback: notice='%s' warning=%d\n",
|
||||||
|
app->notice.c_str(),
|
||||||
|
gtk_widget_has_css_class(app->status_label, "warning") ? 1 : 0);
|
||||||
|
std::fflush(stderr);
|
||||||
|
// Again a second and a half later: if the label is still the previous notice then the status line
|
||||||
|
// never recomposes it, which would be the whole of the "no clear feedback" report.
|
||||||
|
g_timeout_add(1500, +[](gpointer u) -> gboolean {
|
||||||
|
App* app = static_cast<App*>(u);
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[drop-probe] the status line, composed: warning=%d status='%s'\n",
|
||||||
|
gtk_widget_has_css_class(app->status_label, "warning") ? 1 : 0,
|
||||||
|
gtk_label_get_text(GTK_LABEL(app->status_label)));
|
||||||
|
std::fflush(stderr);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}, app);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}, a);
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|||||||
+35
-4
@@ -17,7 +17,35 @@ void on_signal_switch(GObject* obj, GParamSpec*, gpointer user) {
|
|||||||
if (app->updating_panels) return;
|
if (app->updating_panels) return;
|
||||||
const std::size_t index = static_cast<std::size_t>(
|
const std::size_t index = static_cast<std::size_t>(
|
||||||
GPOINTER_TO_SIZE(g_object_get_data(obj, "udpscope-signal")));
|
GPOINTER_TO_SIZE(g_object_get_data(obj, "udpscope-signal")));
|
||||||
app->model->set_signal_enabled(index, gtk_check_button_get_active(GTK_CHECK_BUTTON(obj)));
|
const bool on = gtk_check_button_get_active(GTK_CHECK_BUTTON(obj)) != FALSE;
|
||||||
|
// The name first: the switch may be the last reference to a signal the model is about to drop.
|
||||||
|
std::string name;
|
||||||
|
{
|
||||||
|
const std::vector<SignalInfo>& signals = app->model->signals();
|
||||||
|
if (index < signals.size()) name = signals[index].name;
|
||||||
|
}
|
||||||
|
app->model->set_signal_enabled(index, on);
|
||||||
|
if (!on) {
|
||||||
|
// A trace of a signal nobody acquires draws nothing, and holds the slot a live one could take.
|
||||||
|
int gone = remove_signal_from_plots(*app, name);
|
||||||
|
// ... and the derived channels that read it cannot be computed at all, so they follow it off, and their
|
||||||
|
// traces leave with them.
|
||||||
|
const std::vector<std::string> off = disable_dependent_math_channels(*app, name);
|
||||||
|
for (const std::string& channel : off) gone += remove_signal_from_plots(*app, channel);
|
||||||
|
if (!off.empty()) apply_math_channels(*app); // the single writer: the pane's switches follow it
|
||||||
|
if (gone > 0 || !off.empty()) {
|
||||||
|
app->relayout_pending = true;
|
||||||
|
rebuild_signal_panels(*app); // the chips of the signals that are still plotted
|
||||||
|
std::string what = "'" + name + "' is off: " + std::to_string(gone) +
|
||||||
|
(gone == 1 ? " trace left the plots" : " traces left the plots");
|
||||||
|
if (!off.empty()) {
|
||||||
|
// The channel names already read as what they are ("math x2"), so the sentence does not prefix them.
|
||||||
|
what += "; math off: " + off.front() +
|
||||||
|
(off.size() == 1 ? "" : " and " + std::to_string(off.size() - 1) + " more");
|
||||||
|
}
|
||||||
|
set_notice(*app, what, 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
// The enable state is part of the session (a signal can be on only as a math channel's source).
|
// The enable state is part of the session (a signal can be on only as a math channel's source).
|
||||||
schedule_layout_save(*app);
|
schedule_layout_save(*app);
|
||||||
}
|
}
|
||||||
@@ -290,8 +318,7 @@ gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y
|
|||||||
if (signal >= app->model->signals().size()) return FALSE;
|
if (signal >= app->model->signals().size()) return FALSE;
|
||||||
|
|
||||||
const auto notice = [&](const std::string& what) {
|
const auto notice = [&](const std::string& what) {
|
||||||
app->notice = what;
|
set_notice(*app, what, 4);
|
||||||
app->notice_until_us = g_get_monotonic_time() + 4 * G_TIME_SPAN_SECOND;
|
|
||||||
std::fprintf(stderr, "[udpscope] drop: %s\n", what.c_str());
|
std::fprintf(stderr, "[udpscope] drop: %s\n", what.c_str());
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
};
|
};
|
||||||
@@ -334,7 +361,11 @@ gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (slot < 0) {
|
if (slot < 0) {
|
||||||
notice("plot " + std::to_string(row + 1) + " already holds four signals");
|
// Say what to do about it. A refusal a reader cannot act on is only half an answer: the two ways
|
||||||
|
// out are another plot or another row.
|
||||||
|
notice("plot " + std::to_string(row + 1) +
|
||||||
|
" holds four signals already: drop it on another plot, or add a row with the layout "
|
||||||
|
"editor (L)");
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
// The slot remembers the *signal* (and element), never the channel index: the
|
// The slot remembers the *signal* (and element), never the channel index: the
|
||||||
|
|||||||
Reference in new issue
Block a user