fix: a repaired source is reconciled above the validation line, not below it
`math_dialog_sync_form()` wrote the label and then moved a node whose source names an unacquired channel -- a mutation that changes what `math_dialog_problem()` reports. With the sync re-entry now guarded, the nested call that used to repair the label is suppressed, so the line kept describing a source that had just been fixed and left `Apply` insensitive until the next mutation. The reconcile moves above the write and the dropdown reuses its list.
This commit is contained in:
1 parent
f238e717b2
commit
8bc1fe8b5c
2 files changed
+23
-15
No files matched your search
@@ -25,7 +25,8 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
|||||||
two-click protocol is gone), drag a box to move it; **wheel zooms about the pointer, background
|
two-click protocol is gone), drag a box to move it; **wheel zooms about the pointer, background
|
||||||
drag pans, `f` fits, `Ctrl+0` resets**.
|
drag pans, `f` fits, `Ctrl+0` resets**.
|
||||||
Its widgets are created once and only shown, hidden and set by `math_dialog_sync()`, so no rebuild
|
Its widgets are created once and only shown, hidden and set by `math_dialog_sync()`, so no rebuild
|
||||||
can invalidate a live callback handle -- the same shape the measurements dialog uses. `UDPSCOPE_PROBE_MATH_MS` defines two graphs and opens the editor (a construction crash test
|
can invalidate a live callback handle -- the same shape the measurements dialog uses.
|
||||||
|
`UDPSCOPE_PROBE_MATH_MS` defines two graphs and opens the editor (a construction crash test
|
||||||
as much as a state check), and the dump prints each derived channel's extremes beside the
|
as much as a state check), and the dump prints each derived channel's extremes beside the
|
||||||
source's. Read it on the fast mock, where the two assertions that matter are **`gaps=0`** (the
|
source's. Read it on the fast mock, where the two assertions that matter are **`gaps=0`** (the
|
||||||
math is on the producer thread, so what it can break is packet capture, never the frame rate)
|
math is on the producer thread, so what it can break is packet capture, never the frame rate)
|
||||||
|
|||||||
+21
-14
@@ -4085,9 +4085,9 @@ gboolean math_canvas_key(GtkEventControllerKey*, guint keyval, guint, GdkModifie
|
|||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Adds a node of `op` and selects it. The grid placement is the same one the auto-place uses, so a
|
/// Adds a node of `op` and selects it, at the first grid cell whose box overlaps nothing, so a node
|
||||||
/// node added here lands clear of the others -- 160 is narrower than `kNodeW` plus a wire's tread,
|
/// added here lands clear of the others -- 160 is narrower than `kNodeW` plus a wire's tread, which
|
||||||
/// which is why the tidy layout uses a wider pitch; this is only a first guess at a free cell.
|
/// is why the tidy layout uses a wider pitch.
|
||||||
void math_add_node(MathDialog* d, MathOp op) {
|
void math_add_node(MathDialog* d, MathOp op) {
|
||||||
// A node added to a graph that was *linear* -- its output was the last node -- becomes the output,
|
// A node added to a graph that was *linear* -- its output was the last node -- becomes the output,
|
||||||
// which is what "add a gain and it applies" means. A graph whose output is an earlier node is a
|
// which is what "add a gain and it applies" means. A graph whose output is an earlier node is a
|
||||||
@@ -4233,6 +4233,20 @@ void on_math_op_changed(GObject* object, GParamSpec*, gpointer user) {
|
|||||||
/// nodes its inputs are wired to. Called only through `math_dialog_sync()`, which guards it against
|
/// nodes its inputs are wired to. Called only through `math_dialog_sync()`, which guards it against
|
||||||
/// the re-entry the dropdowns' `notify::selected` would otherwise cause.
|
/// the re-entry the dropdowns' `notify::selected` would otherwise cause.
|
||||||
void math_dialog_sync_form(MathDialog* d) {
|
void math_dialog_sync_form(MathDialog* d) {
|
||||||
|
// A source naming a channel nobody acquires is moved to the first acquired one -- see the dropdown
|
||||||
|
// below for why. It happens *here*, above the validation line, because the move is a mutation that
|
||||||
|
// changes what `math_dialog_problem()` reports: reconciled after the label is written, a repaired
|
||||||
|
// source would be described as broken, with `Apply` insensitive, until some later sync.
|
||||||
|
std::vector<std::string> names;
|
||||||
|
const bool have_form =
|
||||||
|
d->selected >= 0 && d->selected < static_cast<int>(d->nodes.size());
|
||||||
|
if (d->source_drop != nullptr && have_form) {
|
||||||
|
names = math_source_names(*d->app);
|
||||||
|
MathNode& sel = d->nodes[static_cast<std::size_t>(d->selected)];
|
||||||
|
if (!names.empty() && std::find(names.begin(), names.end(), sel.source) == names.end()) {
|
||||||
|
sel.source = names.front();
|
||||||
|
}
|
||||||
|
}
|
||||||
// Before the early return below: an out-of-range selection is exactly the state in which a stale
|
// Before the early return below: an out-of-range selection is exactly the state in which a stale
|
||||||
// validation line is most misleading -- a graph with no node selected can still be broken, and the
|
// validation line is most misleading -- a graph with no node selected can still be broken, and the
|
||||||
// user has no form to read instead.
|
// user has no form to read instead.
|
||||||
@@ -4275,22 +4289,15 @@ void math_dialog_sync_form(MathDialog* d) {
|
|||||||
if (d->source_drop != nullptr) {
|
if (d->source_drop != nullptr) {
|
||||||
// The *acquired* channels, which is what Apply validates against. Listing every signal let a
|
// The *acquired* channels, which is what Apply validates against. Listing every signal let a
|
||||||
// user pick a source that could never be made valid -- and put the stream's timestamp signal,
|
// user pick a source that could never be made valid -- and put the stream's timestamp signal,
|
||||||
// which has no samples at all, at the top of the list.
|
// which has no samples at all, at the top of the list. A source that is not in this list would
|
||||||
const std::vector<std::string> names = math_source_names(*d->app);
|
// read NaN for ever while the dropdown showed the first entry as if it were selected, so the
|
||||||
|
// node was already moved to the first entry at the top of this function.
|
||||||
GtkStringList* list = gtk_string_list_new(nullptr);
|
GtkStringList* list = gtk_string_list_new(nullptr);
|
||||||
guint pick = 0;
|
guint pick = 0;
|
||||||
bool found = false;
|
|
||||||
for (std::size_t i = 0; i < names.size(); ++i) {
|
for (std::size_t i = 0; i < names.size(); ++i) {
|
||||||
gtk_string_list_append(list, names[i].c_str());
|
gtk_string_list_append(list, names[i].c_str());
|
||||||
if (names[i] == n.source) {
|
if (names[i] == n.source) pick = static_cast<guint>(i);
|
||||||
pick = static_cast<guint>(i);
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// A source that is not in the list is a graph naming a channel nobody acquires -- it would read
|
|
||||||
// NaN for ever while the dropdown showed the first entry as if it were selected. The node is
|
|
||||||
// moved to what the form is actually showing, so what is on screen is what the graph says.
|
|
||||||
if (!found && !names.empty()) n.source = names.front();
|
|
||||||
gtk_drop_down_set_model(GTK_DROP_DOWN(d->source_drop), G_LIST_MODEL(list));
|
gtk_drop_down_set_model(GTK_DROP_DOWN(d->source_drop), G_LIST_MODEL(list));
|
||||||
gtk_drop_down_set_selected(GTK_DROP_DOWN(d->source_drop), pick);
|
gtk_drop_down_set_selected(GTK_DROP_DOWN(d->source_drop), pick);
|
||||||
g_object_unref(list);
|
g_object_unref(list);
|
||||||
|
|||||||
Reference in new issue
Block a user