diff --git a/README.md b/README.md index 6b94772..81b2237 100644 --- a/README.md +++ b/README.md @@ -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 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 + 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 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-gtk-and-chrome.md b/docs/claude/invariants-gtk-and-chrome.md index b2340bb..2385afc 100644 --- a/docs/claude/invariants-gtk-and-chrome.md +++ b/docs/claude/invariants-gtk-and-chrome.md @@ -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 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. +- **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 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** — diff --git a/docs/claude/probe-readings.md b/docs/claude/probe-readings.md index 3245bcd..50460a6 100644 --- a/docs/claude/probe-readings.md +++ b/docs/claude/probe-readings.md @@ -1142,12 +1142,19 @@ library measures and draws the rect in tests of its own. ## `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] 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: 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 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 @@ -1164,7 +1171,10 @@ its pane. - **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 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 — 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 diff --git a/src/ui.cpp b/src/ui.cpp index 1a9e4d3..eb9a06b 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -4436,6 +4436,25 @@ void on_pane_edit_clicked(GtkButton* button, gpointer user) { 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) { const auto toggle = [](GtkWidget* button) { 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); { 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(), gtk_icon_theme_has_icon(theme, app.dbg_pane_edit_icon.c_str()) ? "ok" : "MISSING", app.dbg_pane_view_icon.c_str(), diff --git a/src/ui_app.hpp b/src/ui_app.hpp index 38ad53e..a794b57 100644 --- a/src/ui_app.hpp +++ b/src/ui_app.hpp @@ -261,6 +261,9 @@ struct App { /// 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. 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 /// 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". @@ -822,6 +825,9 @@ struct App { 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; + /// 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; int dbg_side_column_after_hide = 1; 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); /// The panel's controls from the model, and the session's trigger into both. 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' /// bodies, their [edit] buttons and the signal list's rows all follow from `trigger_locked` and /// `signals_locked`. diff --git a/src/ui_probe.cpp b/src/ui_probe.cpp index aa0f852..ef79670 100644 --- a/src/ui_probe.cpp +++ b/src/ui_probe.cpp @@ -103,6 +103,35 @@ gboolean exit_probe_step3(gpointer user) { } 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 // 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 diff --git a/src/ui_signals.cpp b/src/ui_signals.cpp index de18219..1778aef 100644 --- a/src/ui_signals.cpp +++ b/src/ui_signals.cpp @@ -457,6 +457,17 @@ void rebuild_signal_list(App& app, const std::vector& signals) { g_signal_connect_data(source, "prepare", G_CALLBACK(on_drag_prepare), ctx, [](gpointer data, GClosure*) { delete static_cast(data); }, 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(user), true); + }), + &app); + g_signal_connect(source, "drag-end", + G_CALLBACK(+[](GtkDragSource*, GdkDrag*, gboolean, gpointer user) { + row_drag_sidebar(*static_cast(user), false); + }), + &app); gtk_widget_add_controller(row, GTK_EVENT_CONTROLLER(source)); gtk_list_box_append(GTK_LIST_BOX(list), row); diff --git a/src/ui_trigger.cpp b/src/ui_trigger.cpp index e6222b5..6ed5e92 100644 --- a/src/ui_trigger.cpp +++ b/src/ui_trigger.cpp @@ -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 // 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); - // 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_trigger_summary(*app); } /// The way out of trigger mode, from inside the panel that froze the view: it is