ui: the signal badge is the drag handle (to a plot row, or to reorder); the list rows stop dragging
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
f27925da59
commit
ca10dfb323
9 files changed
+230
-109
No files matched your search
@@ -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
|
||||||
|
|||||||
@@ -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** —
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user