Feature/source plugins #1

Merged
martino merged 130 commits from feature/source-plugins into main 2026-11-04 07:35:21 +01:00
9 changed files with 230 additions and 109 deletions
Showing only changes of commit ca10dfb323 - Show all commits

No files matched your search

+2 -2
View File
@@ -2252,7 +2252,7 @@ exists; it is there rather than on the chip because a second gesture on a one-gl
not something anybody would find. The chooser is opened by a `GtkGestureClick` on the existing not something anybody would find. The chooser is opened by a `GtkGestureClick` on the existing
label rather than by wrapping it in a `GtkButton`, because a button's padding is exactly what label rather than by wrapping it in a `GtkButton`, because a button's padding is exactly what
made this row twice as tall once (below), and the gesture claims its sequence so a click on the made this row twice as tall once (below), and the gesture claims its sequence so a click on the
chip cannot become the start of a row drag. chip cannot also activate the row (and open its page).
A `GtkSwitch` and two A `GtkSwitch` and two
always-visible buttons made a row about twice as tall (46 px against 26 px now), always-visible buttons made a row about twice as tall (46 px against 26 px now),
@@ -2261,7 +2261,7 @@ button and list-row minimums, not the widgets themselves, set that height, so th
row carries a `.udpscope-compact` style class that relaxes them. row carries a `.udpscope-compact` style class that relaxes them.
**Signals start disabled**, because enabling one is what starts acquiring it. **Signals start disabled**, because enabling one is what starts acquiring it.
Enable a signal with its checkbox, or drag it onto a plot: the drop enables the Enable a signal with its checkbox, or drag its badge from the badge bar onto a plot: the drop enables the
signal if needed, binds it to that row's first free trace slot, and rebuilds the signal if needed, binds it to that row's first free trace slot, and rebuilds the
figure so the legend name is right (there is one widget, so the row a drop lands figure so the legend name is right (there is one widget, so the row a drop lands
on is `PlotArea::row_at()`). The status line reports the outcome of a drop on is `PlotArea::row_at()`). The status line reports the outcome of a drop
+10 -7
View File
@@ -245,13 +245,16 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
alone (`sidebar_wanted`) and the dock's *sensitivity* from `have_signals`; the toolbar's Dock button stays alone (`sidebar_wanted`) and the dock's *sensitivity* from `have_signals`; the toolbar's Dock button stays
sensitive throughout. The wish is persisted (`dock = true|false` in the session file) and a change of it sensitive throughout. The wish is persisted (`dock = true|false` in the session file) and a change of it
schedules a save. `dock:` in the dump reads the result. schedules a save. `dock:` in the dump reads the result.
- **An *overlaid* sidebar covers the plot, so a drag out of it has nowhere to land.** `collapsed` in an - **The signal badge is the drag handle, and a drop handler only moves widgets.** Dragging a signal onto a plot
`AdwOverlaySplitView` means the sidebar sits *on* the plot rather than beside it: the pointer is over the starts on its badge in the badge bar (a `GtkDragSource` on the badge's button, payload the signal index as a
list, never over a drop target, and a signal can be dragged but not dropped. `row_drag_sidebar()` hides decimal string, taken by the plot area's existing drop target); the list's rows are not drag sources. A drag
the sidebar on `drag-begin` and restores it on `drag-end` — only when collapsed, because side by side the starting with the slide-over open would have part of the plot hidden under the panel, so the source's
plot is already visible and hiding the list the drag started in would be startling. `pane drag: sidebar `drag-begin` closes it (which only restyles the badges: nothing is destroyed mid-drag). A click without movement
hidden for the drag=1 restored after it=1` is the reading; the state is forced collapsed the way the never reaches the drag threshold and still opens the page; past it the source claims the sequence and the
sidebar probe forces it. button's click is cancelled. Each badge is also a `GtkDropTarget` for reordering: `on_badge_drop` calls
`gtk_box_reorder_child_after` and rewrites `App::badge_order` from the widgets, and **never rebuilds the badges** --
the dragged badge and the target whose `drop` emission is running must both survive it. A foreign drag whose string is not a
signal index is refused. `[sigbadges]` reads the controllers, the payload and the reorder; `pane drag:` reads the close.
- **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** —
+4 -5
View File
@@ -18,11 +18,10 @@ 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 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 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 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. **Dragging a row onto a plot is not configuration and survives where a returning user expects to start. **Dragging a signal 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 the lock**: it is the signal *badge's* drag (the list's rows are no longer drag sources, and the badges do
anywhere on it, the colour chip included, which in configuration mode has a click gesture that claims not follow the lock). `[sigbadges] drag_sources` is the reading, and `drag_sources` on the `signals panel:`
the sequence precisely so a drag cannot start there. `drag_sources` on the `signals panel:` line is the line must be 0.
reading, and it must be `rows - 1`.
- **The session is the layout.** `LayoutState` persists what makes a reopened scope look like the - **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 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 signals**, **the chosen trace colours** (`[[color]]`, `name` + `rgb = "#RRGGBB"` — only the
+25 -10
View File
@@ -1703,18 +1703,21 @@ 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 drag: slide-over closed for the drag=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** **`pane drag:`** is a drag begun through the *source* of the first signal badge (`drag-begin` emitted on its
state, which is what a window with no room beside the plot produces. Both must read `1`: with the sidebar `GtkDragSource`, so the handler is the one a real drag reaches) with the `signals` slide-over page open. It
covering the plot there is no drop target under the pointer, so a drag out of the signal list is must read `1`: the slide-over covers the plot's right edge, so a drag that started with it open would have part
*impossible* rather than merely awkward, and `row_drag_sidebar()` steps the sidebar aside for the length of of the plot that is not a place to drop, and the badge's `drag-begin` closes it. (The page that was open is put
the drag and puts it back after. `hidden=0` is an overlaid drag that can never land anywhere. back afterwards.) `0` is a drag that leaves the panel over the plot; `-1` is no badge to drag (no signal enabled,
or more than six, where the badges are replaced by the "N signals" overflow). It replaced the old
`sidebar hidden for the drag=1 restored after it=1` pair, which tested `row_drag_sidebar()` -- the list rows were
the drag handle and the overlaid sidebar had to step aside; both are gone.
**`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
@@ -2309,10 +2312,9 @@ The signals panel's frame read from the window, and in the same line the *shape*
the heading and the hint the `.ui` gives it, the list the rows go in, how many rows that list holds 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`), (`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. **`drag_sources` must be 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 `0`**: dragging a signal onto a plot is the signal *badge's* job now (see `[sigbadges]`, which counts the
how a plot is filled, so it has to survive both pane modes: the locked list drops the tick, the menu and the badges' own sources), and the list's rows only open the signal's page; a non-zero reading is a second, competing
`+`, and this is the reading that says the drag did not go with them. (`0` is a list that has become a thing drag handle on the list. `labels=1` is a `.ui` that lost
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 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 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 missing a piece -- and **`divider=0` is the merged list that lost its `+`**, the one control that can create
@@ -2666,6 +2668,19 @@ height moved while one signal was switched off and on again (`rebuild_signal_pan
first signal badge opens `signal:<name>` with the badge marked open and a second click closes it (`-1` when overflowed, there being no signal badges); `plus_opens=1` the same for the "+" first signal badge opens `signal:<name>` with the badge marked open and a second click closes it (`-1` when overflowed, there being no signal badges); `plus_opens=1` the same for the "+"
(the overflow badge when overflowed) and the `signals` page. (the overflow badge when overflowed) and the `signals` page.
```
[sigbadges] drag_sources=4 drop_targets=4 prepare_ok=1 reorder_before=sine,square,ramp,noise reorder_after=noise,sine,square,ramp reorder_ok=1 payload_drop=1 list_drag=0
```
The second line is the drag handles. `drag_sources` and `drop_targets` count the `GtkDragSource` / `GtkDropTarget` controllers over all the
signal badges (each badge carries one of each, so both must equal `badges`; beyond six signals the badges are gone and both read 0). `prepare_ok=1`: the first badge's
`prepare` returns a provider whose string is its signal index (the payload the plot area's drop target reads; `0` is a drag that carries nothing or the wrong
signal, `-1` no badge). `reorder_before` / `reorder_after` are the badge names in widget order around a call to the badges' own `drop` handler with the last badge dropped on
the first; `reorder_ok=1` means the last badge is now first, `badge_order` matches the widgets, and the original order was restored afterwards (so later probes see it;
`-1` with fewer than two badges). `payload_drop=1`: the signal is taken off the plots and `assign_signal_to_row(signal, 0)`, the call the plot's drop makes, leaves it plotted
(`-1` no badge). `list_drag` is the number of drag sources on the signal list's first row, and **must be 0**. **A real pointer drag cannot be simulated headless**: the
gesture's threshold, the slide-over closing under it and the pointer-driven drop are untested here; the line reads the controllers, the payload, the reorder and the assignment.
## `[badges]` ## `[badges]`
``` ```
+2 -21
View File
@@ -4634,25 +4634,6 @@ 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;
@@ -6223,8 +6204,8 @@ 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 drag: sidebar hidden for the drag=%d restored after it=%d\n", std::printf("[udpscope] pane drag: slide-over closed for the drag=%d\n",
app.dbg_drag_hid_sidebar, app.dbg_drag_restored_sidebar); app.dbg_drag_closed_slideover);
std::printf("[udpscope] pane edits: locked=%s(%s) config=%s(%s)\n", 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",
+4 -11
View File
@@ -531,9 +531,6 @@ 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".
@@ -1166,11 +1163,11 @@ struct App {
/// of them are streamed signals. The `MATH` divider and the math channels account for the rest, and /// 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 `+`. /// `rows == signals + 1 + math` is the invariant the dump checks -- a list that lost its `+`.
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`: 0 -- the signal badges are the drag handles now.
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. /// A drag begun through the first signal badge's source closed the open slide-over (1), or did not (0);
int dbg_drag_hid_sidebar = -1; /// -1 when there is no badge to drag.
int dbg_drag_restored_sidebar = -1; int dbg_drag_closed_slideover = -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;
@@ -1852,10 +1849,6 @@ 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`.
+161 -1
View File
@@ -10,6 +10,7 @@
#include <algorithm> #include <algorithm>
#include <cstdio> #include <cstdio>
#include <cstdlib>
#include <cstring> #include <cstring>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
@@ -78,6 +79,40 @@ void make_badge(App& app, Badge& b, GtkWidget* parent, const char* name, const s
gtk_box_append(GTK_BOX(parent), b.button); gtk_box_append(GTK_BOX(parent), b.button);
} }
/// The reorder: widgets are moved, never rebuilt, so neither the dragged badge nor this target (whose "drop"
/// emission is running) is destroyed under the drag. The payload is the signal index as a decimal string (the
/// plot area's drop target reads the same one); anything else a foreign drag might carry is refused.
gboolean on_badge_drop(GtkDropTarget*, const GValue* value, double, double, gpointer user) {
Badge* target = static_cast<Badge*>(user);
App* app = static_cast<App*>(g_object_get_data(G_OBJECT(target->button), "udpscope-app"));
if (app == nullptr || value == nullptr || !G_VALUE_HOLDS_STRING(value)) return FALSE;
const char* text = g_value_get_string(value);
if (text == nullptr || *text == '\0') return FALSE;
char* end = nullptr;
const unsigned long long parsed = std::strtoull(text, &end, 10);
if (end == nullptr || *end != '\0') return FALSE;
const std::size_t signal = static_cast<std::size_t>(parsed);
Badge* moved = nullptr;
for (auto& b : app->badges.signal_badges) {
if (b->signal == signal) moved = b.get();
}
if (moved == nullptr) return FALSE;
if (moved == target) return TRUE;
GtkWidget* prev = gtk_widget_get_prev_sibling(target->button);
if (prev != moved->button) { // already directly in front: nothing to move
gtk_box_reorder_child_after(GTK_BOX(app->badges.signals_row), moved->button, prev);
}
// The order, as the widgets now stand.
app->badge_order.clear();
for (GtkWidget* c = gtk_widget_get_first_child(app->badges.signals_row); c != nullptr;
c = gtk_widget_get_next_sibling(c)) {
for (auto& b : app->badges.signal_badges) {
if (b->button == c) app->badge_order.push_back(b->signal_name);
}
}
return TRUE;
}
/// A signal's badge: its colour dot (grey when it has no trace) and its name. Words and a dot, no icon. /// A signal's badge: its colour dot (grey when it has no trace) and its name. Words and a dot, no icon.
void make_signal_badge(App& app, Badge& b, const std::vector<SignalInfo>& signals, std::size_t index) { void make_signal_badge(App& app, Badge& b, const std::vector<SignalInfo>& signals, std::size_t index) {
b.signal = index; b.signal = index;
@@ -98,6 +133,27 @@ void make_signal_badge(App& app, Badge& b, const std::vector<SignalInfo>& signal
gtk_widget_set_tooltip_text(b.button, b.signal_name.c_str()); gtk_widget_set_tooltip_text(b.button, b.signal_name.c_str());
g_object_set_data(G_OBJECT(b.button), "udpscope-app", &app); g_object_set_data(G_OBJECT(b.button), "udpscope-app", &app);
g_signal_connect(b.button, "clicked", G_CALLBACK(on_badge_clicked), &b); g_signal_connect(b.button, "clicked", G_CALLBACK(on_badge_clicked), &b);
// A drag handle: the same payload the list's rows used to carry, so the plot area's drop target takes it
// unchanged. The slide-over steps aside for the drag, since it covers the plot's right edge. (A click
// without movement never reaches the drag threshold, so it still opens the page; past the threshold the
// source claims the sequence and the button's click is cancelled.)
DragContext* ctx = new DragContext{&app, index};
GtkDragSource* source = gtk_drag_source_new();
gtk_drag_source_set_actions(source, GDK_ACTION_COPY);
g_signal_connect_data(source, "prepare", G_CALLBACK(on_drag_prepare), ctx,
[](gpointer data, GClosure*) { delete static_cast<DragContext*>(data); },
G_CONNECT_DEFAULT);
g_signal_connect(source, "drag-begin", G_CALLBACK(+[](GtkDragSource*, GdkDrag*, gpointer user) {
slideover_close(*static_cast<App*>(user));
}),
&app);
gtk_widget_add_controller(b.button, GTK_EVENT_CONTROLLER(source));
// Dropping another signal's badge on this one moves it in front of this one.
GtkDropTarget* drop = gtk_drop_target_new(G_TYPE_STRING, GDK_ACTION_COPY);
g_signal_connect(drop, "drop", G_CALLBACK(on_badge_drop), &b);
gtk_widget_add_controller(b.button, GTK_EVENT_CONTROLLER(drop));
} }
} // namespace } // namespace
@@ -197,7 +253,9 @@ void sync_badge_bar(App& app) {
} }
if (bb.overflow.button != nullptr) { if (bb.overflow.button != nullptr) {
gtk_widget_set_sensitive(bb.overflow.button, !app.model->signals().empty()); gtk_widget_set_sensitive(bb.overflow.button, !app.model->signals().empty());
set_open(bb.overflow.button, slideover_page(app) == bb.overflow.page); // The overflow stands for every signal's badge too: a signal page opened from a list row marks it.
set_open(bb.overflow.button,
slideover_page(app) == bb.overflow.page || slideover_page(app).rfind("signal:", 0) == 0);
} }
for (const std::unique_ptr<Badge>& sb : bb.signal_badges) { for (const std::unique_ptr<Badge>& sb : bb.signal_badges) {
set_open(sb->button, slideover_page(app) == sb->page); set_open(sb->button, slideover_page(app) == sb->page);
@@ -360,6 +418,108 @@ gboolean signal_badges_probe(gpointer user) {
"bar_h=%d bar_h_fixed=%d bar_min_w=%d opens=%d plus_opens=%d\n", "bar_h=%d bar_h_fixed=%d bar_min_w=%d opens=%d plus_opens=%d\n",
enabled, badges, overflowed ? 1 : 0, overflow_text.c_str(), plus_visible, dot_plotted, before.first, enabled, badges, overflowed ? 1 : 0, overflow_text.c_str(), plus_visible, dot_plotted, before.first,
bar_h_fixed, bar_min_w, opens, plus_opens); bar_h_fixed, bar_min_w, opens, plus_opens);
// The drag handles. Controllers are enumerated through the widget's own model.
const auto count_controllers = [](GtkWidget* w, bool (*is)(GObject*)) {
int n = 0;
GListModel* controllers = gtk_widget_observe_controllers(w);
const guint items = controllers != nullptr ? g_list_model_get_n_items(controllers) : 0;
for (guint i = 0; i < items; ++i) {
GObject* c = G_OBJECT(g_list_model_get_item(controllers, i));
if (is(c)) ++n;
g_object_unref(c);
}
return n;
};
const auto is_source = [](GObject* c) { return GTK_IS_DRAG_SOURCE(c) != FALSE; };
const auto is_target = [](GObject* c) { return GTK_IS_DROP_TARGET(c) != FALSE; };
int drag_sources = 0;
int drop_targets = 0;
for (const std::unique_ptr<Badge>& sb : bb.signal_badges) {
drag_sources += count_controllers(sb->button, is_source);
drop_targets += count_controllers(sb->button, is_target);
}
// The payload: what a drag from the first badge carries, read back from the provider `prepare` returns.
int prepare_ok = -1;
if (!bb.signal_badges.empty()) {
prepare_ok = 0;
GListModel* controllers = gtk_widget_observe_controllers(bb.signal_badges.front()->button);
const guint items = controllers != nullptr ? g_list_model_get_n_items(controllers) : 0;
for (guint i = 0; i < items; ++i) {
GObject* c = G_OBJECT(g_list_model_get_item(controllers, i));
if (GTK_IS_DRAG_SOURCE(c)) {
GdkContentProvider* provider = nullptr;
g_signal_emit_by_name(c, "prepare", 0.0, 0.0, &provider);
if (provider != nullptr) {
GValue v = G_VALUE_INIT;
g_value_init(&v, G_TYPE_STRING);
if (gdk_content_provider_get_value(provider, &v, nullptr) && g_value_get_string(&v) != nullptr &&
std::to_string(bb.signal_badges.front()->signal) == g_value_get_string(&v)) {
prepare_ok = 1;
}
g_value_unset(&v);
g_object_unref(provider);
}
}
g_object_unref(c);
}
}
// The reorder, by the drop handler the targets run: the last badge dropped on the first goes in front of it,
// and the remembered order follows the widgets. Then the order is put back as it was.
const auto widget_names = [&] {
std::string names;
for (GtkWidget* c = gtk_widget_get_first_child(bb.signals_row); c != nullptr; c = gtk_widget_get_next_sibling(c)) {
for (const std::unique_ptr<Badge>& sb : bb.signal_badges) {
if (sb->button == c) names += (names.empty() ? "" : ",") + sb->signal_name;
}
}
return names;
};
std::string reorder_before = "-";
std::string reorder_after = "-";
int reorder_ok = -1;
if (bb.signal_badges.size() >= 2) {
const std::vector<std::string> saved_order = a->badge_order;
reorder_before = widget_names();
Badge& first = *bb.signal_badges.front();
Badge& last = *bb.signal_badges.back();
GValue v = G_VALUE_INIT;
g_value_init(&v, G_TYPE_STRING);
g_value_set_string(&v, std::to_string(last.signal).c_str());
const gboolean handled = on_badge_drop(nullptr, &v, 0.0, 0.0, &first);
g_value_unset(&v);
reorder_after = widget_names();
std::string order;
for (const std::string& n : a->badge_order) order += (order.empty() ? "" : ",") + n;
GtkWidget* head = gtk_widget_get_first_child(bb.signals_row);
reorder_ok = handled && head == last.button && order == reorder_after && reorder_after != reorder_before ? 1 : 0;
// Back to the original place: the moved badge goes after the one that was last, and the order is restored.
gtk_box_reorder_child_after(GTK_BOX(bb.signals_row), last.button, bb.signal_badges[bb.signal_badges.size() - 2]->button);
a->badge_order = saved_order;
if (widget_names() != reorder_before) reorder_ok = 0;
}
// The drop path itself: a badge's payload lands through `assign_signal_to_row`, as the plot area's drop does.
// (The assignment rebuilds the panels, and the badges with them, so the first one is named beforehand.)
int payload_drop = -1;
int list_drag = -1;
if (!bb.signal_badges.empty()) {
const std::size_t signal = bb.signal_badges.front()->signal;
const std::string name = bb.signal_badges.front()->signal_name;
remove_signal_from_plots(*a, name);
const bool assigned = assign_signal_to_row(*a, signal, 0);
payload_drop = assigned && signal_plotted(*a, name) ? 1 : 0;
}
if (a->signal_list != nullptr) {
GtkWidget* row = gtk_widget_get_first_child(a->signal_list);
list_drag = row != nullptr ? count_controllers(row, is_source) : -1;
}
std::fprintf(stderr,
"[sigbadges] drag_sources=%d drop_targets=%d prepare_ok=%d reorder_before=%s reorder_after=%s "
"reorder_ok=%d payload_drop=%d list_drag=%d\n",
drag_sources, drop_targets, prepare_ok, reorder_before.c_str(), reorder_after.c_str(), reorder_ok,
payload_drop, list_drag);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
+18 -30
View File
@@ -101,10 +101,9 @@ gboolean exit_probe_step3b(gpointer user) {
// the page is open and the reveal has run. // the page is open and the reveal has run.
a->dbg_signal_w = gtk_widget_get_width(a->signal_panel); a->dbg_signal_w = gtk_widget_get_width(a->signal_panel);
a->dbg_signal_h = gtk_widget_get_height(a->signal_panel); a->dbg_signal_h = gtk_widget_get_height(a->signal_panel);
// How many rows of the signal list carry a drag source: dragging a signal onto a plot is how a plot is // How many rows of the signal list carry a drag source: none. Dragging a signal onto a plot is the signal
// filled, so it has to survive both pane modes -- the locked list drops the tick, the menu and the `+`, // badge's job (and survives the pane lock, since the badges do not follow it); the list's rows only open
// and this is the reading that says it did not drop the drag with them. One row is the `MATH` divider, // the signal's page. The reading says the list did not grow a second, competing handle.
// which is not draggable: the count must be `rows - 1`.
if (a->signal_list != nullptr) { if (a->signal_list != nullptr) {
int sources = 0; int sources = 0;
for (GtkWidget* r = gtk_widget_get_first_child(a->signal_list); r != nullptr; for (GtkWidget* r = gtk_widget_get_first_child(a->signal_list); r != nullptr;
@@ -119,34 +118,23 @@ gboolean exit_probe_step3b(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. // A drag out of a badge with a slide-over open: the panel covers the plot's right edge, so it has to step
// The state is forced the way the sidebar probe forces it -- collapsed is what a window with no room // aside or part of the plot is not a place to drop. The drag is begun through the *source*, so the handler
// beside the plot produces -- and the drag is begun through the *source*, so the handler is the one a // is the one a real drag reaches; the page that was open is put back so the readings after it stand.
// real drag reaches. if (!a->badges.signal_badges.empty() && slideover_has_page(*a, "signals")) {
if (a->side_view != nullptr && a->signal_list != nullptr) { const std::string was = slideover_page(*a);
AdwOverlaySplitView* split = ADW_OVERLAY_SPLIT_VIEW(a->side_view); slideover_open(*a, "signals");
GtkWidget* row = gtk_widget_get_first_child(a->signal_list); GListModel* controllers = gtk_widget_observe_controllers(a->badges.signal_badges.front()->button);
adw_overlay_split_view_set_collapsed(split, TRUE); const guint n = controllers != nullptr ? g_list_model_get_n_items(controllers) : 0;
adw_overlay_split_view_set_show_sidebar(split, TRUE); for (guint i = 0; i < n; ++i) {
if (row != nullptr) { GObject* c = G_OBJECT(g_list_model_get_item(controllers, i));
GListModel* controllers = gtk_widget_observe_controllers(row); if (GTK_IS_DRAG_SOURCE(c)) {
const guint n = controllers != nullptr ? g_list_model_get_n_items(controllers) : 0; g_signal_emit_by_name(c, "drag-begin", nullptr);
for (guint i = 0; i < n; ++i) { a->dbg_drag_closed_slideover = slideover_page(*a).empty() ? 1 : 0;
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);
} }
g_object_unref(c);
} }
// Left as the window's own width decides, so the readings taken before this are the ones that stand. if (!was.empty()) slideover_open(*a, was);
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
+4 -22
View File
@@ -302,7 +302,7 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
// One line per *signal*, not per channel. A group's model holds each signal once per event; a list // One line per *signal*, not per channel. A group's model holds each signal once per event; a list
// that showed both would name one "sine (A)" and the other "sine (B)" as if they were different // that showed both would name one "sine (A)" and the other "sine (B)" as if they were different
// things, and give them different colours. What the row toggles, colours and drags is the signal. // things, and give them different colours. What the row toggles and colours is the signal.
std::vector<std::size_t> rows; std::vector<std::size_t> rows;
// The `MATH` divider goes in before the first math row, and once only: `append_math_signals_locked()` // The `MATH` divider goes in before the first math row, and once only: `append_math_signals_locked()`
// has already put every math channel in `signals()`, so the boundary is the one thing the deleted // has already put every math channel in `signals()`, so the boundary is the one thing the deleted
@@ -350,8 +350,8 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
g_signal_connect(pick, "pressed", g_signal_connect(pick, "pressed",
G_CALLBACK(+[](GtkGestureClick* g, int, double, double, gpointer user) { G_CALLBACK(+[](GtkGestureClick* g, int, double, double, gpointer user) {
App* a = static_cast<App*>(user); App* a = static_cast<App*>(user);
// Claimed, so the row's `GtkDragSource` cannot take the same sequence // Claimed, so the same click does not also activate the row (which would open
// and turn a click on the chip into the start of a drag. // the signal's page).
gtk_gesture_set_state(GTK_GESTURE(g), GTK_EVENT_SEQUENCE_CLAIMED); gtk_gesture_set_state(GTK_GESTURE(g), GTK_EVENT_SEQUENCE_CLAIMED);
open_signal_color(*a, static_cast<std::size_t>(GPOINTER_TO_SIZE( open_signal_color(*a, static_cast<std::size_t>(GPOINTER_TO_SIZE(
g_object_get_data(G_OBJECT(g), g_object_get_data(G_OBJECT(g),
@@ -466,25 +466,7 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), box); gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), box);
// Activating the row opens the signal's page; the row resolves its identity the way its widgets do. // Activating the row opens the signal's page; the row resolves its identity the way its widgets do.
g_object_set_data(G_OBJECT(row), "udpscope-signal", GSIZE_TO_POINTER(i)); g_object_set_data(G_OBJECT(row), "udpscope-signal", GSIZE_TO_POINTER(i));
// Dragging a signal onto a plot is the signal badge's job (`make_signal_badge`): the row opens the page.
DragContext* ctx = new DragContext{&app, i};
GtkDragSource* source = gtk_drag_source_new();
gtk_drag_source_set_actions(source, GDK_ACTION_COPY);
g_signal_connect_data(source, "prepare", G_CALLBACK(on_drag_prepare), ctx,
[](gpointer data, GClosure*) { delete static_cast<DragContext*>(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<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_list_box_append(GTK_LIST_BOX(list), row); gtk_list_box_append(GTK_LIST_BOX(list), row);
} }