From a43d3830f0f9b8d6884acf747a78c5b8a942fede Mon Sep 17 00:00:00 2001 From: Martino Ferrari Date: Wed, 14 Oct 2026 14:57:29 +0200 Subject: [PATCH] test: the locked signal list keeps its drag, and the reading says so MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Asked for: dragging a signal onto a plot must still work in the compact (view) list. It does — the drag source is on the *row* and was never conditional on the lock — but nothing said so, and "the lock rebuilds this list and drops three of its controls" is exactly the change that could take the drag with it. Now it is a reading: **`drag_sources` must be `rows - 1`**, one per signal row, the `MATH` divider not being draggable. signals panel: … rows=3 signals=2 math=0 divider=1 drag_sources=2 (locked) signals panel: … rows=5 signals=4 math=0 divider=1 drag_sources=4 (configuration mode) Measured the direct way first, with a throwaway walk of the list's rows and `gtk_widget_observe_controllers()`: `locked=1 rows=3 rows_with_a_drag_source=2`, the third row being the divider. That is where the number in the reading comes from, and the walk is what found the shape of the answer rather than assuming it. One consequence worth recording, because it is an improvement rather than a cost: a locked row can be dragged from *anywhere* on it, the colour chip included. In configuration mode the chip carries a click gesture that claims the event sequence precisely so a drag cannot start there (it opens the colour chooser); the locked list does not build that gesture, so the chip is inert and the row's drag source sees the press. What the lock does *not* change is which signals are listed: only the ones that are on. A signal nobody acquires draws nothing on a plot, so a row for it would be a drag that ends in an empty slot. Verified: both lines above, from sessions differing only in whether one exists; `make test-ui` 14 files; zero `Gtk-CRITICAL`. Docs: the `drag_sources` field with what a `0` would mean, the invariants' lock bullet, and the README's note. --- README.md | 4 +++- docs/claude/invariants-session-and-export.md | 6 +++++- docs/claude/probe-readings.md | 6 +++++- src/ui.cpp | 5 +++-- src/ui_app.hpp | 2 ++ src/ui_probe.cpp | 18 ++++++++++++++++++ 6 files changed, 36 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index dc86b7b..6b94772 100644 --- a/README.md +++ b/README.md @@ -2403,7 +2403,9 @@ now — they are listed so they are not mistaken for anything else: accessible name. Both panes also have a **locked** mode, which is what a returning user gets: the trigger pane shows one line saying what it is set to, the signal list shows only the signals that are on, and neither offers a control that changes the configuration — each heading has an `[edit]` button - to switch to the full pane, and back. A **first run** (no session file for that source) comes up + to switch to the full pane, and back. Dragging a signal onto a plot still works while locked — filling + plots is what the list is for, not configuration — and a locked row can be dragged from anywhere on it, + the colour chip included. A **first run** (no session file for that source) comes up unlocked, because configuring the host is what a first run is for. Each pane's heading also carries its **own switch**, the same control as the header bar's button — clicking either moves both — because a pane that is *disabled* while its feature is off is a pane whose controls you would otherwise have to leave to turn it back on. The diff --git a/docs/claude/invariants-session-and-export.md b/docs/claude/invariants-session-and-export.md index f1ba76a..9d8b4e3 100644 --- a/docs/claude/invariants-session-and-export.md +++ b/docs/claude/invariants-session-and-export.md @@ -18,7 +18,11 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear a control that changes the configuration. A *first run* — no session file for that source — never reaches `apply_layout()` and stays in configuration mode, which is what a first run is for. The mode is a mode and not a setting: it is deliberately not persisted, so a scope reopened tomorrow starts - where a returning user expects to start. + where a returning user expects to start. **Dragging a row onto a plot is not configuration and survives + the lock**: filling plots is what the list is *for*, so a locked row keeps its `GtkDragSource` — from + anywhere on it, the colour chip included, which in configuration mode has a click gesture that claims + the sequence precisely so a drag cannot start there. `drag_sources` on the `signals panel:` line is the + reading, and it must be `rows - 1`. - **The session is the layout.** `LayoutState` persists what makes a reopened scope look like the one that was closed: the window, the rows and their slot signals, the calibration, **the enabled signals**, **the chosen trace colours** (`[[color]]`, `name` + `rgb = "#RRGGBB"` — only the diff --git a/docs/claude/probe-readings.md b/docs/claude/probe-readings.md index cbd7f43..3245bcd 100644 --- a/docs/claude/probe-readings.md +++ b/docs/claude/probe-readings.md @@ -1378,7 +1378,11 @@ the two lines together read what the file gave and what the code put in it. The signals panel's frame read from the window, and in the same line the *shape* of the one channel list: the heading and the hint the `.ui` gives it, the list the rows go in, how many rows that list holds (`rows`), how many of them are streamed signals (`signals`), how many math channels the app has (`math`), -and `divider`, which is 1 when `rows == signals + 1 + math` and 0 otherwise. `labels=1` is a `.ui` that lost +and `divider`, which is 1 when `rows == signals + 1 + math` and 0 otherwise. **`drag_sources` must be +`rows - 1`** — one per signal row, the `MATH` divider not being draggable. Dragging a signal onto a plot is +how a plot is filled, so it has to survive both pane modes: the locked list drops the tick, the menu and the +`+`, and this is the reading that says the drag did not go with them. (`0` is a list that has become a thing +you can read but not use.) `labels=1` is a `.ui` that lost one of the two labels — counted over the panel's subtree *above* the list, because the rows' own name and unit labels are the code's and the heading sits in a heading row beside the pane's `[edit]` button — `list=0` one that lost the list -- both of which otherwise show as a panel quietly missing a piece -- and **`divider=0` is the merged list that lost its `+`**, the one control that can create diff --git a/src/ui.cpp b/src/ui.cpp index c941889..1a9e4d3 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -5747,12 +5747,13 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit // ones, and the `MATH` divider plus the math channels account for the rest -- so // `rows == signals + 1 + math` is the invariant, and `divider=0` is a list that lost its `+`. std::printf("[udpscope] signals panel: labels=%d list=%d rows=%d signals=%zu math=%zu " - "divider=%d\n", + "divider=%d drag_sources=%d\n", labels, lists, app.dbg_signal_rows, app.dbg_signal_plain, app.math.size(), app.dbg_signal_rows == static_cast(app.dbg_signal_plain + 1 + app.math.size()) ? 1 - : 0); + : 0, + app.dbg_signal_drag_sources); } { // The events pane's frame, read from the window: the four widgets the `.ui` gives it, and the list diff --git a/src/ui_app.hpp b/src/ui_app.hpp index 5086adb..38ad53e 100644 --- a/src/ui_app.hpp +++ b/src/ui_app.hpp @@ -820,6 +820,8 @@ struct App { /// of them are streamed signals. The `MATH` divider and the math channels account for the rest, and /// `rows == signals + 1 + math` is the invariant the dump checks -- a list that lost its `+`. int dbg_signal_rows = 0; + /// How many rows of the list carry a `GtkDragSource`: `rows - 1`, the divider not being draggable. + int dbg_signal_drag_sources = -1; std::size_t dbg_signal_plain = 0; int dbg_side_column_after_hide = 1; std::string dbg_mode_triggered; diff --git a/src/ui_probe.cpp b/src/ui_probe.cpp index f4a7079..aa0f852 100644 --- a/src/ui_probe.cpp +++ b/src/ui_probe.cpp @@ -85,6 +85,24 @@ gboolean exit_probe_step3(gpointer user) { a->dbg_switcher_w = gtk_widget_get_width(a->side_switcher); a->dbg_switcher_h = gtk_widget_get_height(a->side_switcher); } + // How many rows of the signal list carry a drag source: dragging a signal onto a plot is how a plot is + // filled, so it has to survive both pane modes -- the locked list drops the tick, the menu and the `+`, + // and this is the reading that says it did not drop the drag with them. One row is the `MATH` divider, + // which is not draggable: the count must be `rows - 1`. + if (a->signal_list != nullptr) { + int sources = 0; + for (GtkWidget* r = gtk_widget_get_first_child(a->signal_list); r != nullptr; + r = gtk_widget_get_next_sibling(r)) { + GListModel* controllers = gtk_widget_observe_controllers(r); + const guint n = controllers != nullptr ? g_list_model_get_n_items(controllers) : 0; + for (guint i = 0; i < n; ++i) { + GObject* c = G_OBJECT(g_list_model_get_item(controllers, i)); + if (GTK_IS_DRAG_SOURCE(c)) ++sources; + g_object_unref(c); + } + } + a->dbg_signal_drag_sources = sources; + } // The panes' own switches, and *both* directions of the sync: the header bar's trigger button is // flipped here and the pane's switch has to follow, then the cursors' switch is driven from the // pane and the button is what has to follow. Both are read back after the fact, so the two pairs in