scope windows take drops, and the signal page lists them
The second windows were read-only by accident: nothing accepted a drop on them, and the
signal page's *Plot on* buttons knew only the main window's rows.
- **Drop target per scope window**: the same string payload a badge drags (the signal index),
handled by `on_scope_drop_channel()` -- the target carries its own window, the row is that
window's cell under the pointer, and `assign_signal_to_scope()` lands the signal in the
window's own row set (enabling an unacquired signal first, exactly as the main drop does).
The rebuild and the area's re-point are the window's next frame in the tick: a drop arrives
from inside the very widget it rearranges, and a synchronous rebuild would reset the drag
controller under it.
- **Plot on, per window**: the signal page carries one named group per scope window ("In
\"plot 2\""), one button per plot of that window, sensitive on that window's own rows. A
window closing refreshes the pages in its destroy handler, before the deferred delete could
dangle a button's scope pointer.
- Probe: `[scope-probe]` drops through the wired handler (`drop=1 slot1='square'`) and counts
the page's groups (`page_groups=2`); the section documents that it must not run with
`UDPSCOPE_ENABLE_ALL_MS` -- the enable-all probe's second leg clears the front plot, which
is the slot this probe copies. README, help and the layout invariant follow.
This commit is contained in:
1 parent
706d247b58
commit
51b4fad386
9 files changed
+231
-21
No files matched your search
@@ -138,6 +138,10 @@ and `PlotArea`; the overflow menu's **New scope window** opens one with an empty
|
|||||||
window is another *view* of the same acquisition: the main window's tick binds and acquires its
|
window is another *view* of the same acquisition: the main window's tick binds and acquires its
|
||||||
rowset under the same model mutex, mirrors the main window's time window (or its pin) onto it
|
rowset under the same model mutex, mirrors the main window's time window (or its pin) onto it
|
||||||
every frame, and pumps its render — so the two windows never disagree about the data, the display
|
every frame, and pumps its render — so the two windows never disagree about the data, the display
|
||||||
|
mode or the span. **A signal badge dragged onto a scope window's plot lands there** — the same
|
||||||
|
drop the main plot takes, into that window's own row set, on the cell the pointer is over — and
|
||||||
|
the signal page's *Plot on* buttons list **every window**: one group per window (named, `In
|
||||||
|
"plot 2"`), one button per plot of it.
|
||||||
mode or the span.
|
mode or the span.
|
||||||
|
|
||||||
**The window.** The plot is the window. Under it sits a fixed-height **badge bar** (36 px). Its left
|
**The window.** The plot is the window. Under it sits a fixed-height **badge bar** (36 px). Its left
|
||||||
|
|||||||
@@ -357,7 +357,14 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
|||||||
(same display mode, same epoch), and its span follows the main window's window or pin every frame. It is
|
(same display mode, same epoch), and its span follows the main window's window or pin every frame. It is
|
||||||
therefore a second *view* -- the settle rule is that a scope must never acquire, plan or epoch anything of
|
therefore a second *view* -- the settle rule is that a scope must never acquire, plan or epoch anything of
|
||||||
its own. Closing the main window destroys its scopes first: a leftover toplevel keeps the GApplication's
|
its own. Closing the main window destroys its scopes first: a leftover toplevel keeps the GApplication's
|
||||||
loop alive around a window that is gone (the exit run's hang).
|
loop alive around a window that is gone (the exit run's hang). What a user *arranges* in a scope is the
|
||||||
|
scope's own arrangement, and it takes the same two doors the main window's rows take: a badge **dropped on
|
||||||
|
a scope window's plot** lands in that window's row set on the cell under the pointer (its own drop target,
|
||||||
|
its own `assign_signal_to_scope()`), and the signal page's *Plot on* section carries **one named group per
|
||||||
|
window** -- rebuilt out of `App::scopes` on every fill, and again when a window closes (the destroy handler
|
||||||
|
refreshes the pages before the deferred delete can dangle a button's scope pointer). The change itself is
|
||||||
|
never applied synchronously: a drop arrives from inside the very widget it rearranges, so the rebuild and
|
||||||
|
the area's re-point are the window's next frame in the tick.
|
||||||
|
|
||||||
- **A figure row's full-screen is one click from two places, and never from one figure.** The
|
- **A figure row's full-screen is one click from two places, and never from one figure.** The
|
||||||
toggle sits on the row's top-right corner, **out of hiding only while the pointer is near that
|
toggle sits on the row's top-right corner, **out of hiding only while the pointer is near that
|
||||||
|
|||||||
@@ -1453,10 +1453,14 @@ places), arranged as two single-cell rows with one signal each. The two reads af
|
|||||||
|
|
||||||
`UDPSCOPE_PROBE_SCOPE_MS`. The **second window**: `open_scope_window()` twice — once copying
|
`UDPSCOPE_PROBE_SCOPE_MS`. The **second window**: `open_scope_window()` twice — once copying
|
||||||
the main window's figure 0, once empty — and read back a few frames later (the main tick is the
|
the main window's figure 0, once empty — and read back a few frames later (the main tick is the
|
||||||
scope's bind/acquire/repaint pump). Measured output:
|
scope's bind/acquire/repaint pump). **Do not run it with `UDPSCOPE_ENABLE_ALL_MS`**: the
|
||||||
|
enable-all probe's second leg, two seconds in, clears the front plot through the signal page --
|
||||||
|
exactly the slot this probe copies -- so `copied=''` under that combination is that race and not
|
||||||
|
a defect in the copy. Measured output:
|
||||||
|
|
||||||
```
|
```
|
||||||
[scope-probe] count=2 copied='sine' bound=1 empty_ok=1 windows=1
|
[scope-probe] count=2 copied='sine' bound=1 empty_ok=1 windows=1
|
||||||
|
[scope-probe] drop=1 slot1='square' page_groups=2
|
||||||
```
|
```
|
||||||
|
|
||||||
- `count=2` is both windows having been created and retained; `windows=1` each being a visible
|
- `count=2` is both windows having been created and retained; `windows=1` each being a visible
|
||||||
@@ -1466,6 +1470,14 @@ scope's bind/acquire/repaint pump). Measured output:
|
|||||||
model, not a snapshot taken at creation.
|
model, not a snapshot taken at creation.
|
||||||
- `empty_ok=1` is the `-1` call producing an empty row, which is the other entry point the
|
- `empty_ok=1` is the `-1` call producing an empty row, which is the other entry point the
|
||||||
overflow menu uses.
|
overflow menu uses.
|
||||||
|
- The second line is the **window as a drop target and a *Plot on* group**: `drop=1` is
|
||||||
|
`on_scope_drop_channel()` — the handler the window's own target wires — putting `square` in
|
||||||
|
the copied window's second slot (`slot1=`), the rebuild and the area's re-point then left to
|
||||||
|
the window's next frame; `page_groups=2` is the signal page of an unplotted signal carrying
|
||||||
|
**one *Plot on* group per window** (`udpscope-scope-plot-on`), so a signal can be plotted
|
||||||
|
into a scope window without dragging it there. `drop=0` names a refused drop (a full plot or
|
||||||
|
an unacquirable signal); `page_groups` less than the window count is a page rebuilt before
|
||||||
|
the windows were read or a group that lost its window.
|
||||||
|
|
||||||
## `[search-probe]`
|
## `[search-probe]`
|
||||||
|
|
||||||
|
|||||||
@@ -81,8 +81,11 @@
|
|||||||
<item><p>A plot's right-click menu has <gui>Open this plot in a new window</gui>: a second
|
<item><p>A plot's right-click menu has <gui>Open this plot in a new window</gui>: a second
|
||||||
window showing the same stream through a copy of that figure (its cells, their widths and
|
window showing the same stream through a copy of that figure (its cells, their widths and
|
||||||
every trace), with its own axes following the main window's time window. The overflow menu's
|
every trace), with its own axes following the main window's time window. The overflow menu's
|
||||||
<gui>New scope window</gui> opens the same kind of window with an empty figure. Closing one
|
<gui>New scope window</gui> opens the same kind of window with an empty figure. A signal
|
||||||
closes that view only; the main window keeps acquiring.</p></item>
|
badge dragged onto a window's plot lands in that window (on the plot under the pointer), and
|
||||||
|
the signal page's <gui>Plot on</gui> section lists every window — one named group per
|
||||||
|
window, one button per plot of it. Closing one closes that view only; the main window keeps
|
||||||
|
acquiring.</p></item>
|
||||||
</list>
|
</list>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
+20
@@ -611,6 +611,16 @@ void open_scope_window(App& app, int figure_row) {
|
|||||||
gtk_widget_set_hexpand(scope->widget, true);
|
gtk_widget_set_hexpand(scope->widget, true);
|
||||||
gtk_widget_set_vexpand(scope->widget, true);
|
gtk_widget_set_vexpand(scope->widget, true);
|
||||||
gtk_widget_set_size_request(scope->widget, 320, 160);
|
gtk_widget_set_size_request(scope->widget, 320, 160);
|
||||||
|
// The window's short name, what the signal page's *Plot on* groups carry: "plot N" for a copy
|
||||||
|
// (of the main window's figure N), "scope N" -- numbered app-wide -- for an empty one.
|
||||||
|
scope->name = figure_row >= 0 ? "plot " + std::to_string(figure_row + 1)
|
||||||
|
: "scope " + std::to_string(++app.scope_seq);
|
||||||
|
// A drop target like the main area's: a signal badge dragged onto this window's plot lands in
|
||||||
|
// that window's own row set, on the cell the pointer is over.
|
||||||
|
scope->drop = gtk_drop_target_new(G_TYPE_STRING, GDK_ACTION_COPY);
|
||||||
|
g_object_set_data(G_OBJECT(scope->drop), "udpscope-scope", scope.get());
|
||||||
|
g_signal_connect(scope->drop, "drop", G_CALLBACK(on_scope_drop_channel), &app);
|
||||||
|
gtk_widget_add_controller(scope->widget, GTK_EVENT_CONTROLLER(scope->drop));
|
||||||
scope->window = GTK_WINDOW(gtk_window_new());
|
scope->window = GTK_WINDOW(gtk_window_new());
|
||||||
const std::string title = figure_row >= 0
|
const std::string title = figure_row >= 0
|
||||||
? "udpscope \u2014 plot " + std::to_string(figure_row + 1)
|
? "udpscope \u2014 plot " + std::to_string(figure_row + 1)
|
||||||
@@ -640,6 +650,10 @@ void open_scope_window(App& app, int figure_row) {
|
|||||||
victim, nullptr);
|
victim, nullptr);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// The signal page's *Plot on* groups carry a button per window: with one gone,
|
||||||
|
// the pages are rebuilt before anything can read a dangling scope pointer
|
||||||
|
// (the struct itself is deleted on the idle above).
|
||||||
|
signal_pages_sync(*a);
|
||||||
}),
|
}),
|
||||||
&app);
|
&app);
|
||||||
gtk_window_present(scope->window);
|
gtk_window_present(scope->window);
|
||||||
@@ -4235,6 +4249,12 @@ gboolean tick(gpointer user) {
|
|||||||
} else if (std::fabs(scope->rows->time_window() - window_s) > 1.0e-12) {
|
} else if (std::fabs(scope->rows->time_window() - window_s) > 1.0e-12) {
|
||||||
scope->rows->set_time_window(window_s);
|
scope->rows->set_time_window(window_s);
|
||||||
}
|
}
|
||||||
|
// An arrangement change (a drop, a *Plot on* button) rebuilt the row set: point the area at
|
||||||
|
// the new figure here, in the tick -- the change arrives from inside this very widget (a drop
|
||||||
|
// signal), and a synchronous rebuild would reset the drag controller under it.
|
||||||
|
if (scope->rows->rebuild_if_dirty() && scope->area != nullptr) {
|
||||||
|
scope->area->set_figure(&scope->rows->figure());
|
||||||
|
}
|
||||||
if (scope->widget != nullptr) gtk_gl_area_queue_render(GTK_GL_AREA(scope->widget));
|
if (scope->widget != nullptr) gtk_gl_area_queue_render(GTK_GL_AREA(scope->widget));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -215,6 +215,12 @@ struct App {
|
|||||||
GtkWindow* window = nullptr;
|
GtkWindow* window = nullptr;
|
||||||
plot::gtk4::PlotArea* area = nullptr;
|
plot::gtk4::PlotArea* area = nullptr;
|
||||||
GtkWidget* widget = nullptr;
|
GtkWidget* widget = nullptr;
|
||||||
|
/// The window's short name, the one the signal page's *Plot on* groups carry ("plot 2" for a
|
||||||
|
/// copy, "scope 3" for an empty one): a button must say which window it plots into.
|
||||||
|
std::string name;
|
||||||
|
/// The drop target wired onto `widget`: the handler reads the *window* off it (each window's
|
||||||
|
/// target names its own scope), and the probe calls the handler with it directly.
|
||||||
|
GtkDropTarget* drop = nullptr;
|
||||||
/// The area's initial plot, owned here: `PlotArea::create()` wants a `Plot&` that
|
/// The area's initial plot, owned here: `PlotArea::create()` wants a `Plot&` that
|
||||||
/// outlives the widget, and sharing the main window's placeholder would hand two areas
|
/// outlives the widget, and sharing the main window's placeholder would hand two areas
|
||||||
/// one plot the moment the figure mode is ever left.
|
/// one plot the moment the figure mode is ever left.
|
||||||
@@ -222,6 +228,8 @@ struct App {
|
|||||||
std::unique_ptr<RowSet> rows;
|
std::unique_ptr<RowSet> rows;
|
||||||
};
|
};
|
||||||
std::vector<std::unique_ptr<ScopeWindow>> scopes;
|
std::vector<std::unique_ptr<ScopeWindow>> scopes;
|
||||||
|
/// Every empty scope window is numbered from this, so two of them never share a name.
|
||||||
|
int scope_seq = 0;
|
||||||
|
|
||||||
/// On while the layout editor is up. Nothing else about the plot changes: the editor is
|
/// On while the layout editor is up. Nothing else about the plot changes: the editor is
|
||||||
/// a set of overlay buttons, not a mode the figure knows about.
|
/// a set of overlay buttons, not a mode the figure knows about.
|
||||||
@@ -2038,6 +2046,13 @@ void row_menu_fill(RowMenuState& st);
|
|||||||
GtkWidget* open_row_menu(App& app, int row, double x, double y, GtkWidget* parent, bool autohide = true);
|
GtkWidget* open_row_menu(App& app, int row, double x, double y, GtkWidget* parent, bool autohide = true);
|
||||||
void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer user);
|
void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer user);
|
||||||
gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y, gpointer user);
|
gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y, gpointer user);
|
||||||
|
/// The same drop, onto a **scope window's** plot: the target carries the window, the row is the
|
||||||
|
/// window's own cell under the pointer, and the assignment lands in that window's row set.
|
||||||
|
gboolean on_scope_drop_channel(GtkDropTarget*, const GValue* value, double x, double y, gpointer user);
|
||||||
|
/// Plots `signal` on `row` of a scope window's row set (the signal page's *Plot on* buttons and the
|
||||||
|
/// drop target both end here). False, with a notice, when the signal cannot be acquired or the plot
|
||||||
|
/// holds four signals already.
|
||||||
|
bool assign_signal_to_scope(App& app, App::ScopeWindow& scope, std::size_t signal, int row);
|
||||||
/// Reads each pane's own switch back off the header bar's button, and the other way round: the two are
|
/// Reads each pane's own switch back off the header bar's button, and the other way round: the two are
|
||||||
/// one control in two places, and `panel_switch_sync` keeps the write from looking like a click.
|
/// one control in two places, and `panel_switch_sync` keeps the write from looking like a click.
|
||||||
void sync_panel_switches(App& app);
|
void sync_panel_switches(App& app);
|
||||||
|
|||||||
@@ -1736,6 +1736,19 @@ gboolean maximize_probe_step(gpointer user) {
|
|||||||
/// bound to the same stream a few frames later, while the empty one stays empty. The window
|
/// bound to the same stream a few frames later, while the empty one stays empty. The window
|
||||||
/// creation itself (a real `GtkWindow`, an own `PlotArea`, its own `RowSet`) is exercised by
|
/// creation itself (a real `GtkWindow`, an own `PlotArea`, its own `RowSet`) is exercised by
|
||||||
/// the same call the two menu entries make.
|
/// the same call the two menu entries make.
|
||||||
|
/// How many descendants carry `name` -- the scope window's *Plot on* groups are counted, one per
|
||||||
|
/// window, where a single `find_named` would stop at the first.
|
||||||
|
int count_named(GtkWidget* root, const char* name) {
|
||||||
|
int n = 0;
|
||||||
|
if (root == nullptr) return 0;
|
||||||
|
if (strcmp(gtk_widget_get_name(root), name) == 0) ++n;
|
||||||
|
for (GtkWidget* c = gtk_widget_get_first_child(root); c != nullptr;
|
||||||
|
c = gtk_widget_get_next_sibling(c)) {
|
||||||
|
n += count_named(c, name);
|
||||||
|
}
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
gboolean scope_probe_step(gpointer user) {
|
gboolean scope_probe_step(gpointer user) {
|
||||||
App* a = static_cast<App*>(user);
|
App* a = static_cast<App*>(user);
|
||||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||||
@@ -1786,6 +1799,31 @@ gboolean scope_probe_step(gpointer user) {
|
|||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
"[scope-probe] count=2 copied='%s' bound=%d empty_ok=%d windows=%d\n",
|
"[scope-probe] count=2 copied='%s' bound=%d empty_ok=%d windows=%d\n",
|
||||||
copied.c_str(), copied_bound ? 1 : 0, empty_ok ? 1 : 0, windows ? 1 : 0);
|
copied.c_str(), copied_bound ? 1 : 0, empty_ok ? 1 : 0, windows ? 1 : 0);
|
||||||
|
// **A drop onto a window**: the handler the drop target wires, called directly with a
|
||||||
|
// cell-0 coordinate. The copied window holds 'sine' in slot 0, so the dropped signal must
|
||||||
|
// land in its second slot (the rebuild and the area's re-point are the window's next frame).
|
||||||
|
std::size_t square = std::size_t(-1);
|
||||||
|
for (std::size_t i = 0; i < a->model->signals().size(); ++i) {
|
||||||
|
if (a->model->signals()[i].name == "square") square = i;
|
||||||
|
}
|
||||||
|
GValue payload = G_VALUE_INIT;
|
||||||
|
g_value_init(&payload, G_TYPE_STRING);
|
||||||
|
g_value_set_string(&payload, std::to_string(square).c_str());
|
||||||
|
const gboolean dropped = square != std::size_t(-1) && copy.drop != nullptr
|
||||||
|
? on_scope_drop_channel(copy.drop, &payload, 40.0, 20.0, a)
|
||||||
|
: FALSE;
|
||||||
|
g_value_unset(&payload);
|
||||||
|
const std::string slot1 = copy.rows->row(0).signal_name[1];
|
||||||
|
// **The signal page lists the windows**: one *Plot on* group per window, in the page of a
|
||||||
|
// signal that is plotted nowhere (so every button is live).
|
||||||
|
signal_page_ensure(*a, "ramp");
|
||||||
|
signal_pages_sync(*a);
|
||||||
|
int groups = 0;
|
||||||
|
const auto page_it = a->signal_pages.find("ramp");
|
||||||
|
if (page_it != a->signal_pages.end()) groups = count_named(page_it->second, "udpscope-scope-plot-on");
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[scope-probe] drop=%d slot1='%s' page_groups=%d\n",
|
||||||
|
dropped ? 1 : 0, slot1.c_str(), groups);
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -288,6 +288,48 @@ void fill(App& app, const std::string& name, GtkWidget* page) {
|
|||||||
&app);
|
&app);
|
||||||
}
|
}
|
||||||
gtk_box_append(GTK_BOX(page), rows_box);
|
gtk_box_append(GTK_BOX(page), rows_box);
|
||||||
|
|
||||||
|
// The scope windows: each is a view with its own arrangement, and a signal is plotted there
|
||||||
|
// exactly as it is here -- one group per window (named, so two windows never blur into one),
|
||||||
|
// one button per plot of that window's figure. The buttons read *that* window's rows for
|
||||||
|
// "already on this plot", and a click lands in the window's own row set.
|
||||||
|
for (const std::unique_ptr<App::ScopeWindow>& scope : app.scopes) {
|
||||||
|
if (scope->rows == nullptr || scope->rows->row_count() <= 0) continue;
|
||||||
|
GtkWidget* scope_head = gtk_label_new(("In \"" + scope->name + "\"").c_str());
|
||||||
|
gtk_widget_add_css_class(scope_head, "heading");
|
||||||
|
gtk_widget_set_halign(scope_head, GTK_ALIGN_START);
|
||||||
|
gtk_box_append(GTK_BOX(page), scope_head);
|
||||||
|
GtkWidget* scope_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||||
|
gtk_widget_set_name(scope_box, "udpscope-scope-plot-on");
|
||||||
|
for (int r = 0; r < scope->rows->row_count(); ++r) {
|
||||||
|
GtkWidget* b = text_button(scope->rows->plot_label(r).c_str(), scope_box);
|
||||||
|
tag(b, index);
|
||||||
|
g_object_set_data(G_OBJECT(b), "udpscope-row", GINT_TO_POINTER(r));
|
||||||
|
g_object_set_data(G_OBJECT(b), "udpscope-scope", scope.get());
|
||||||
|
bool on_this_row = false;
|
||||||
|
for (int slot = 0; slot < RowSet::kSlotsPerRow; ++slot) {
|
||||||
|
if (scope->rows->row(r).signal_name[static_cast<std::size_t>(slot)] == sig.name) {
|
||||||
|
on_this_row = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
gtk_widget_set_sensitive(b, !on_this_row);
|
||||||
|
gtk_widget_set_tooltip_text(b, on_this_row ? "already on this plot"
|
||||||
|
: "draw this signal in this window's plot");
|
||||||
|
g_signal_connect(b, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
|
||||||
|
App* a = static_cast<App*>(user);
|
||||||
|
auto* scope = static_cast<App::ScopeWindow*>(
|
||||||
|
g_object_get_data(G_OBJECT(b), "udpscope-scope"));
|
||||||
|
if (scope == nullptr) return;
|
||||||
|
outlive_the_rebuild(GTK_WIDGET(b));
|
||||||
|
assign_signal_to_scope(*a, *scope, tagged(b),
|
||||||
|
GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b),
|
||||||
|
"udpscope-row")));
|
||||||
|
}),
|
||||||
|
&app);
|
||||||
|
}
|
||||||
|
gtk_box_append(GTK_BOX(page), scope_box);
|
||||||
|
}
|
||||||
|
|
||||||
GtkWidget* off = text_button("Remove from plots", page);
|
GtkWidget* off = text_button("Remove from plots", page);
|
||||||
gtk_widget_set_halign(off, GTK_ALIGN_START);
|
gtk_widget_set_halign(off, GTK_ALIGN_START);
|
||||||
gtk_widget_set_sensitive(off, plotted);
|
gtk_widget_set_sensitive(off, plotted);
|
||||||
|
|||||||
+86
-17
@@ -505,24 +505,26 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
|
|||||||
app.updating_panels = false;
|
app.updating_panels = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool assign_signal_to_row(App& app, std::size_t signal, int row) {
|
/// The drop's one-line report, wherever the drop landed: the notice bar and the log, because a
|
||||||
if (signal >= app.model->signals().size()) return false;
|
/// refusal a reader cannot act on is only half an answer.
|
||||||
|
void drop_notice(App& app, const std::string& what) {
|
||||||
const auto notice = [&](const std::string& what) {
|
set_notice(app, what, 4);
|
||||||
set_notice(app, what, 4);
|
std::fprintf(stderr, "[udpscope] drop: %s\n", what.c_str());
|
||||||
std::fprintf(stderr, "[udpscope] drop: %s\n", what.c_str());
|
std::fflush(stderr);
|
||||||
std::fflush(stderr);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
|
/// Resolves a dropped signal to an acquired channel, enabling the signal when it has to be: the
|
||||||
|
/// channel index and its display name, or -1 when the signal cannot be acquired (a multi-element
|
||||||
|
/// signal has no channel to bind). Shared by the main window's drop and the scope windows'.
|
||||||
|
int resolve_drop_channel(App& app, std::size_t signal, std::string* name) {
|
||||||
// The dropped signal's channels are the ones the model built for it; take the
|
// The dropped signal's channels are the ones the model built for it; take the
|
||||||
// first, or the first element if the signal is a vector.
|
// first, or the first element if the signal is a vector.
|
||||||
const std::vector<ChannelInfo>& channels = app.model->channels();
|
const std::vector<ChannelInfo>& channels = app.model->channels();
|
||||||
int channel = -1;
|
int channel = -1;
|
||||||
std::string name;
|
|
||||||
for (std::size_t i = 0; i < channels.size(); ++i) {
|
for (std::size_t i = 0; i < channels.size(); ++i) {
|
||||||
if (channels[i].signal == signal) {
|
if (channels[i].signal == signal) {
|
||||||
channel = static_cast<int>(i);
|
channel = static_cast<int>(i);
|
||||||
name = channels[i].name;
|
*name = channels[i].name;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,14 +535,22 @@ bool assign_signal_to_row(App& app, std::size_t signal, int row) {
|
|||||||
for (std::size_t i = 0; i < again.size(); ++i) {
|
for (std::size_t i = 0; i < again.size(); ++i) {
|
||||||
if (again[i].signal == signal) {
|
if (again[i].signal == signal) {
|
||||||
channel = static_cast<int>(i);
|
channel = static_cast<int>(i);
|
||||||
name = again[i].name;
|
*name = again[i].name;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool assign_signal_to_row(App& app, std::size_t signal, int row) {
|
||||||
|
if (signal >= app.model->signals().size()) return false;
|
||||||
|
|
||||||
|
std::string name;
|
||||||
|
const int channel = resolve_drop_channel(app, signal, &name);
|
||||||
if (channel < 0) {
|
if (channel < 0) {
|
||||||
notice("cannot acquire '" + app.model->signals()[signal].name +
|
drop_notice(app, "cannot acquire '" + app.model->signals()[signal].name +
|
||||||
"' (multi-element signals are not acquired)");
|
"' (multi-element signals are not acquired)");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -554,9 +564,9 @@ bool assign_signal_to_row(App& app, std::size_t signal, int row) {
|
|||||||
if (slot < 0) {
|
if (slot < 0) {
|
||||||
// Say what to do about it. A refusal a reader cannot act on is only half an answer: the two ways
|
// Say what to do about it. A refusal a reader cannot act on is only half an answer: the two ways
|
||||||
// out are another plot or another row.
|
// out are another plot or another row.
|
||||||
notice("plot " + app.rows->plot_label(row) +
|
drop_notice(app, "plot " + app.rows->plot_label(row) +
|
||||||
" holds four signals already: drop it on another plot, or add a row with the layout "
|
" holds four signals already: drop it on another plot, or add a row with "
|
||||||
"editor (L)");
|
"the layout editor (L)");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// The slot remembers the *signal* (and element), never the channel index: the
|
// The slot remembers the *signal* (and element), never the channel index: the
|
||||||
@@ -567,10 +577,69 @@ bool assign_signal_to_row(App& app, std::size_t signal, int row) {
|
|||||||
schedule_layout_save(app);
|
schedule_layout_save(app);
|
||||||
app.relayout_pending = true;
|
app.relayout_pending = true;
|
||||||
rebuild_signal_panels(app); // the row's colour chip now has a trace to name
|
rebuild_signal_panels(app); // the row's colour chip now has a trace to name
|
||||||
notice("plotting '" + name + "' on plot " + app.rows->plot_label(row));
|
drop_notice(app, "plotting '" + name + "' on plot " + app.rows->plot_label(row));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool assign_signal_to_scope(App& app, App::ScopeWindow& scope, std::size_t signal, int row) {
|
||||||
|
if (scope.rows == nullptr || signal >= app.model->signals().size()) return false;
|
||||||
|
if (row < 0 || row >= scope.rows->row_count()) return false;
|
||||||
|
std::string name;
|
||||||
|
const int channel = resolve_drop_channel(app, signal, &name);
|
||||||
|
if (channel < 0) {
|
||||||
|
drop_notice(app, "cannot acquire '" + app.model->signals()[signal].name +
|
||||||
|
"' (multi-element signals are not acquired)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
int slot = -1;
|
||||||
|
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
|
||||||
|
if (!scope.rows->slot_bound(row, s)) {
|
||||||
|
slot = s;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (slot < 0) {
|
||||||
|
drop_notice(app, scope.name + " plot " + scope.rows->plot_label(row) + " holds four signals "
|
||||||
|
"already: drop it on another plot of that window");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// The slot remembers the *signal* (and element), never the channel index -- the same rule the
|
||||||
|
// main window's rows follow, for the same renumbering reason.
|
||||||
|
const std::vector<SignalInfo>& signals = app.model->signals();
|
||||||
|
const SignalInfo& sig = signals[signal];
|
||||||
|
scope.rows->assign(row, slot, sig.name, name, signal_color_index(signals, signal));
|
||||||
|
// The rebuild and the area's re-point are the window's own frame tick's (a drop arrives from
|
||||||
|
// inside this widget, and a synchronous rebuild would reset the drag controller under it). The
|
||||||
|
// signal page is refreshed now: its *Plot on* buttons read the scope's rows for "already on".
|
||||||
|
signal_pages_sync(app);
|
||||||
|
drop_notice(app, "plotting '" + name + "' on " + scope.name + " plot " +
|
||||||
|
scope.rows->plot_label(row));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean on_scope_drop_channel(GtkDropTarget* target, const GValue* value, double x, double y,
|
||||||
|
gpointer user) {
|
||||||
|
App* app = static_cast<App*>(user);
|
||||||
|
auto* scope =
|
||||||
|
static_cast<App::ScopeWindow*>(g_object_get_data(G_OBJECT(target), "udpscope-scope"));
|
||||||
|
if (scope == nullptr || scope->area == nullptr || scope->rows == nullptr) return FALSE;
|
||||||
|
if (!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);
|
||||||
|
// Where the drop landed decides the row *of this window*: one area covers its figure's cells.
|
||||||
|
const int row = std::clamp(static_cast<int>(scope->area->row_at(x, y)), 0,
|
||||||
|
scope->rows->row_count() - 1);
|
||||||
|
if (!assign_signal_to_scope(*app, *scope, signal, row)) return FALSE;
|
||||||
|
if (plot::input::Controller* c = scope->area->row_controller(static_cast<plot::u32>(row))) {
|
||||||
|
c->on_key(plot::input::KeyEvent{plot::input::kKeyEscape, 0, true});
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y,
|
gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y,
|
||||||
gpointer user) {
|
gpointer user) {
|
||||||
App* app = static_cast<App*>(user);
|
App* app = static_cast<App*>(user);
|
||||||
|
|||||||
Reference in new issue
Block a user