fix: the overlaid sidebar steps aside for a drag, and the compact view follows the trigger

Two reports, both real, and neither was about the lock itself.

**A drag out of an overlaid sidebar had nowhere to land.** In a window too narrow to show both columns,
`AdwOverlaySplitView` puts the sidebar *on* the plot — that is what `collapsed` means — so the pointer
stays over the list and never over a drop target: a signal could be dragged but not dropped, which reads
as "dragging does not work". `row_drag_sidebar()` now hides the sidebar on `drag-begin` and restores it on
`drag-end`, and only when collapsed: side by side the plot is already visible, and hiding the list the drag
started in would be startling. The reading begins a drag through the *source* with the state forced
collapsed, the way the sidebar probe forces it:

    pane drag: sidebar hidden for the drag=1 restored after it=1

**The compact view did not follow the trigger switch.** The locked line's first branch reads
`tc.enabled` — with trigger mode off it says `stream (trigger off)` and nothing else — and
`on_trigger_toggled()` never re-synced it, so the line kept saying whichever it had been. One call to
`sync_trigger_summary()` beside the switch sync. The dump's two `panes:` lines were already the test and
now pass it: with a session whose trigger is off, the line as found reads `stream (trigger off)` and the
live one reads the settings, because the exit probe turns the trigger on in between. Both reading `stream`
was the defect.

Verified: the two lines above; `panes: the same line as the probe found it: 'mode: stream (trigger off)'`
against the live `'signal: square | condition: window, rising | level: 0.3 .. 0.6 | mode: normal'`;
`make test-ui` 14 files; zero `Gtk-CRITICAL`. Docs: the `pane drag:` reading and the invariants bullet for
the overlay, and the `panes:` pair's second job — the refresh test — written down so the next reader knows
what both lines reading `stream` means.
This commit is contained in:
Martino Ferrari committed 2026-10-14 15:04:10 +02:00
1 parent a43d3830f0
commit 8b2dae7655
8 files changed
+96 -4

No files matched your search

+3 -1
View File
@@ -2405,7 +2405,9 @@ now — they are listed so they are not mistaken for anything else:
on, and neither offers a control that changes the configuration — each heading has an `[edit]` button on, and neither offers a control that changes the configuration — each heading has an `[edit]` button
to switch to the full pane, and back. Dragging a signal onto a plot still works while locked — filling 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, 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 the colour chip included. In a window too narrow to show both, where the sidebar is *overlaid on* the
plot, the sidebar steps aside for the length of the drag so there is something under the pointer to drop
onto. 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 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 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 feature is off is a pane whose controls you would otherwise have to leave to turn it back on. The
+7
View File
@@ -188,6 +188,13 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
**tooltip and accessible name** from the page's name, then hides the empty label that would otherwise **tooltip and accessible name** from the page's name, then hides the empty label that would otherwise
leave the icon off-centre. An icon-only pill is still a named control for a screen reader, and leave the icon off-centre. An icon-only pill is still a named control for a screen reader, and
`switcher: … pills=2 labels=0 names=2` is the reading for all three facts at once. `switcher: … pills=2 labels=0 names=2` is the reading for all three facts at once.
- **An *overlaid* sidebar covers the plot, so a drag out of it has nowhere to land.** `collapsed` in an
`AdwOverlaySplitView` means the sidebar sits *on* the plot rather than beside it: the pointer is over the
list, never over a drop target, and a signal can be dragged but not dropped. `row_drag_sidebar()` hides
the sidebar on `drag-begin` and restores it on `drag-end` — only when collapsed, because side by side the
plot is already visible and hiding the list the drag started in would be startling. `pane drag: sidebar
hidden for the drag=1 restored after it=1` is the reading; the state is forced collapsed the way the
sidebar probe forces it.
- **A stock icon name has to be one *Adwaita* has, and a blank button is the failure mode.** The two - **A stock icon name has to be one *Adwaita* has, and a blank button is the failure mode.** The two
panes' `[edit]` buttons are `document-edit-symbolic` (the pencil: edit) and `object-select-symbolic` panes' `[edit]` buttons are `document-edit-symbolic` (the pencil: edit) and `object-select-symbolic`
(the check: done). The done one was `emblem-ok-symbolic`, which breeze ships and **Adwaita does not** — (the check: done). The done one was `emblem-ok-symbolic`, which breeze ships and **Adwaita does not** —
+11 -1
View File
@@ -1142,12 +1142,19 @@ library measures and draws the rect in tests of its own.
## `panes:` — the locked and configuration modes ## `panes:` — the locked and configuration modes
``` ```
[udpscope] pane drag: sidebar hidden for the drag=1 restored after it=1
[udpscope] pane edits: locked=document-edit-symbolic(ok) config=object-select-symbolic(ok) [udpscope] pane edits: locked=document-edit-symbolic(ok) config=object-select-symbolic(ok)
[udpscope] panes: trigger=locked signals=locked body_visible=0 summary='signal: square | condition: window, falling | level: 0.3 .. 0.6 | mode: single | noise band: 0.2 | pre-trigger: 20 % | hold-off: 1000 ms' [udpscope] panes: trigger=locked signals=locked body_visible=0 summary='signal: square | condition: window, falling | level: 0.3 .. 0.6 | mode: single | noise band: 0.2 | pre-trigger: 20 % | hold-off: 1000 ms'
[udpscope] panes: trigger=config signals=config body_visible=1 summary='' [udpscope] panes: trigger=config signals=config body_visible=1 summary=''
[udpscope] panes: the same line as the probe found it: 'mode: stream (trigger off)' [udpscope] panes: the same line as the probe found it: 'mode: stream (trigger off)'
``` ```
**`pane drag:`** is a drag begun through the *source* with the split view forced into its **overlaid**
state, which is what a window with no room beside the plot produces. Both must read `1`: with the sidebar
covering the plot there is no drop target under the pointer, so a drag out of the signal list is
*impossible* rather than merely awkward, and `row_drag_sidebar()` steps the sidebar aside for the length of
the drag and puts it back after. `hidden=0` is an overlaid drag that can never land anywhere.
**`pane edits:`** is the two `[edit]` buttons in each mode, by name and verdict, and it exists because of **`pane edits:`** is the two `[edit]` buttons in each mode, by name and verdict, and it exists because of
a real defect: the "done" icon was `emblem-ok-symbolic`, which **Adwaita does not have** (breeze does), so a real defect: the "done" icon was `emblem-ok-symbolic`, which **Adwaita does not have** (breeze does), so
the button that leaves configuration mode was blank — and a blank icon button is invisible in every other the button that leaves configuration mode was blank — and a blank icon button is invisible in every other
@@ -1164,7 +1171,10 @@ its pane.
- **With trigger mode off the summary is one line**: `mode: stream (trigger off)`. The four lines below - **With trigger mode off the summary is one line**: `mode: stream (trigger off)`. The four lines below
describe a trigger that is not running, so quoting them would describe nothing. The dump carries the line describe a trigger that is not running, so quoting them would describe nothing. The dump carries the line
twice — once as the probe found it and once at the end — because only a run whose *session* has the twice — once as the probe found it and once at the end — because only a run whose *session* has the
trigger off shows the stream form, and a run whose session has it on shows the settings. trigger off shows the stream form, and a run whose session has it on shows the settings. **The pair is
also the refresh test**: the probe toggles the trigger, so a run whose session has it off must show
`stream (trigger off)` in the line it found and the settings in the live one. Both showing `stream` is a
compact view that did not refresh when the mode changed, which is a defect this pair caught.
- **`trigger panel sensitive …=1` is now a guard, not a state.** The trigger's body is never disabled — - **`trigger panel sensitive …=1` is now a guard, not a state.** The trigger's body is never disabled —
a trigger is configured before it is switched on — so this reads `1` always, and a `0` means the a trigger is configured before it is switched on — so this reads `1` always, and a `0` means the
settings have been made unreachable again. The *cursors'* body is disabled with the cursors, and that settings have been made unreachable again. The *cursors'* body is disabled with the cursors, and that
+22 -1
View File
@@ -4436,6 +4436,25 @@ void on_pane_edit_clicked(GtkButton* button, gpointer user) {
apply_pane_lock(*app); apply_pane_lock(*app);
} }
/// Hides the sidebar for the length of a drag, when it is *overlaid*, and restores it after.
///
/// With the split view collapsed the sidebar covers the plot -- that is what collapsed means -- so a drag
/// that starts in the signal list has nowhere to land: the pointer is over the sidebar and never over a
/// drop target, and the drag is impossible rather than merely awkward. Side by side nothing is done: the
/// plot is already visible, and hiding the list the drag started in would be startling.
void row_drag_sidebar(App& app, bool dragging) {
if (app.side_view == nullptr) return;
AdwOverlaySplitView* split = ADW_OVERLAY_SPLIT_VIEW(app.side_view);
if (dragging) {
if (!adw_overlay_split_view_get_collapsed(split)) return;
app.sidebar_hidden_for_drag = adw_overlay_split_view_get_show_sidebar(split) != FALSE;
if (app.sidebar_hidden_for_drag) adw_overlay_split_view_set_show_sidebar(split, FALSE);
} else if (app.sidebar_hidden_for_drag) {
app.sidebar_hidden_for_drag = false;
adw_overlay_split_view_set_show_sidebar(split, TRUE);
}
}
void run_app_action(App& app, AppAction action) { void run_app_action(App& app, AppAction action) {
const auto toggle = [](GtkWidget* button) { const auto toggle = [](GtkWidget* button) {
if (button == nullptr) return; if (button == nullptr) return;
@@ -5782,7 +5801,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
app.window_limit_s); app.window_limit_s);
{ {
GtkIconTheme* theme = gtk_icon_theme_get_for_display(gdk_display_get_default()); GtkIconTheme* theme = gtk_icon_theme_get_for_display(gdk_display_get_default());
std::printf("[udpscope] pane edits: locked=%s(%s) config=%s(%s)\n", std::printf("[udpscope] pane drag: sidebar hidden for the drag=%d restored after it=%d\n",
app.dbg_drag_hid_sidebar, app.dbg_drag_restored_sidebar);
std::printf("[udpscope] pane edits: locked=%s(%s) config=%s(%s)\n",
app.dbg_pane_edit_icon.c_str(), app.dbg_pane_edit_icon.c_str(),
gtk_icon_theme_has_icon(theme, app.dbg_pane_edit_icon.c_str()) ? "ok" : "MISSING", gtk_icon_theme_has_icon(theme, app.dbg_pane_edit_icon.c_str()) ? "ok" : "MISSING",
app.dbg_pane_view_icon.c_str(), app.dbg_pane_view_icon.c_str(),
+10
View File
@@ -261,6 +261,9 @@ struct App {
/// True while `sync_panel_switches()` is writing the switches: setting a `GtkSwitch` emits its own /// True while `sync_panel_switches()` is writing the switches: setting a `GtkSwitch` emits its own
/// `notify::active`, and without this the program's own write would come back as a click. /// `notify::active`, and without this the program's own write would come back as a click.
bool panel_switch_sync = false; bool panel_switch_sync = false;
/// True while a drag has the overlaid sidebar hidden: what it was showing before, so `drag-end` puts it
/// back rather than guessing.
bool sidebar_hidden_for_drag = false;
/// The channel a session's trigger names, until `rebuild_trigger_signals()` can resolve it: the /// The channel a session's trigger names, until `rebuild_trigger_signals()` can resolve it: the
/// file stores a *name* and the trigger needs an index into the acquired channels, which do not /// file stores a *name* and the trigger needs an index into the acquired channels, which do not
/// exist until the stream has been described. Empty means "nothing pending". /// exist until the stream has been described. Empty means "nothing pending".
@@ -822,6 +825,9 @@ struct App {
int dbg_signal_rows = 0; int dbg_signal_rows = 0;
/// How many rows of the list carry a `GtkDragSource`: `rows - 1`, the divider not being draggable. /// How many rows of the list carry a `GtkDragSource`: `rows - 1`, the divider not being draggable.
int dbg_signal_drag_sources = -1; int dbg_signal_drag_sources = -1;
/// The drag-with-an-overlaid-sidebar pair: the sidebar hid for the drag, and came back after it.
int dbg_drag_hid_sidebar = -1;
int dbg_drag_restored_sidebar = -1;
std::size_t dbg_signal_plain = 0; std::size_t dbg_signal_plain = 0;
int dbg_side_column_after_hide = 1; int dbg_side_column_after_hide = 1;
std::string dbg_mode_triggered; std::string dbg_mode_triggered;
@@ -1462,6 +1468,10 @@ const char* trigger_mode_text(TriggerMode m);
TriggerMode trigger_mode_from_text(const std::string& s); TriggerMode trigger_mode_from_text(const std::string& s);
/// The panel's controls from the model, and the session's trigger into both. /// The panel's controls from the model, and the session's trigger into both.
void write_trigger_widgets(App& app); void write_trigger_widgets(App& app);
/// Hides the sidebar for the length of a drag when it is *overlaid*, and puts it back after. With the
/// split view collapsed the sidebar covers the plot, so a drag that starts in the signal list has nowhere
/// to land: the pointer is over the sidebar, not over a drop target.
void row_drag_sidebar(App& app, bool dragging);
/// The compact line the trigger pane shows while it is locked, and the lock itself: the panes' /// The compact line the trigger pane shows while it is locked, and the lock itself: the panes'
/// bodies, their [edit] buttons and the signal list's rows all follow from `trigger_locked` and /// bodies, their [edit] buttons and the signal list's rows all follow from `trigger_locked` and
/// `signals_locked`. /// `signals_locked`.
+29
View File
@@ -103,6 +103,35 @@ gboolean exit_probe_step3(gpointer user) {
} }
a->dbg_signal_drag_sources = sources; a->dbg_signal_drag_sources = sources;
} }
// A drag out of an *overlaid* sidebar: the sidebar has to step aside or the drop has nowhere to land.
// The state is forced the way the sidebar probe forces it -- collapsed is what a window with no room
// beside the plot produces -- and the drag is begun through the *source*, so the handler is the one a
// real drag reaches.
if (a->side_view != nullptr && a->signal_list != nullptr) {
AdwOverlaySplitView* split = ADW_OVERLAY_SPLIT_VIEW(a->side_view);
GtkWidget* row = gtk_widget_get_first_child(a->signal_list);
adw_overlay_split_view_set_collapsed(split, TRUE);
adw_overlay_split_view_set_show_sidebar(split, TRUE);
if (row != nullptr) {
GListModel* controllers = gtk_widget_observe_controllers(row);
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)) {
g_signal_emit_by_name(c, "drag-begin", nullptr);
a->dbg_drag_hid_sidebar =
adw_overlay_split_view_get_show_sidebar(split) == FALSE ? 1 : 0;
row_drag_sidebar(*a, false);
a->dbg_drag_restored_sidebar =
adw_overlay_split_view_get_show_sidebar(split) != FALSE ? 1 : 0;
}
g_object_unref(c);
}
}
// Left as the window's own width decides, so the readings taken before this are the ones that stand.
adw_overlay_split_view_set_collapsed(split, FALSE);
adw_overlay_split_view_set_show_sidebar(split, TRUE);
}
// The panes' own switches, and *both* directions of the sync: the header bar's trigger button is // 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 // 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 // pane and the button is what has to follow. Both are read back after the fact, so the two pairs in
+11
View File
@@ -457,6 +457,17 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
g_signal_connect_data(source, "prepare", G_CALLBACK(on_drag_prepare), ctx, g_signal_connect_data(source, "prepare", G_CALLBACK(on_drag_prepare), ctx,
[](gpointer data, GClosure*) { delete static_cast<DragContext*>(data); }, [](gpointer data, GClosure*) { delete static_cast<DragContext*>(data); },
G_CONNECT_DEFAULT); G_CONNECT_DEFAULT);
// A drag out of an *overlaid* sidebar would have nowhere to land (see `row_drag_sidebar()`), so the
// sidebar steps out of the way for as long as the drag lasts.
g_signal_connect(source, "drag-begin", G_CALLBACK(+[](GtkDragSource*, GdkDrag*, gpointer user) {
row_drag_sidebar(*static_cast<App*>(user), true);
}),
&app);
g_signal_connect(source, "drag-end",
G_CALLBACK(+[](GtkDragSource*, GdkDrag*, gboolean, gpointer user) {
row_drag_sidebar(*static_cast<App*>(user), false);
}),
&app);
gtk_widget_add_controller(row, GTK_EVENT_CONTROLLER(source)); gtk_widget_add_controller(row, GTK_EVENT_CONTROLLER(source));
gtk_list_box_append(GTK_LIST_BOX(list), row); gtk_list_box_append(GTK_LIST_BOX(list), row);
+3 -1
View File
@@ -413,8 +413,10 @@ void on_trigger_toggled(GtkToggleButton* button, gpointer user) {
// Arming is a *runtime* control: with trigger mode off there is no engine to arm, and the button is the // Arming is a *runtime* control: with trigger mode off there is no engine to arm, and the button is the
// one part of this pane that is not a setting. (The body itself stays live -- see above.) // one part of this pane that is not a setting. (The body itself stays live -- see above.)
if (app->arm_button != nullptr) gtk_widget_set_sensitive(app->arm_button, on); if (app->arm_button != nullptr) gtk_widget_set_sensitive(app->arm_button, on);
// The pane's own switch shows the same state, whichever of the two was clicked. // The pane's own switch shows the same state, whichever of the two was clicked, and the compact line
// changes shape with it: with trigger mode off it says "stream" and nothing else.
sync_panel_switches(*app); sync_panel_switches(*app);
sync_trigger_summary(*app);
} }
/// The way out of trigger mode, from inside the panel that froze the view: it is /// The way out of trigger mode, from inside the panel that froze the view: it is