Gated measurements #2

Merged
martino merged 12 commits from gated-measurements into main 2026-11-06 12:30:43 +01:00
5 changed files with 205 additions and 10 deletions
Showing only changes of commit 2924f7627d - Show all commits

No files matched your search

+3 -1
View File
@@ -145,7 +145,9 @@ every frame, and pumps its render — so the two windows never disagree about th
mode or the span. **A signal badge dragged onto a scope window's plot lands there** — the same 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 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 the signal page's *Plot on* buttons list **every window**: one group per window (named, `In
"plot 2"`), one button per plot of it. "plot 2"`), one button per plot of it. A right-click on a scope window's plot opens **its own
menu** — y axis (auto or a fixed range), fit, remove a trace — acting on that window's rows; a
detached window's menu carries **Attach back to the main window** beside them.
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
+4
View File
@@ -1473,6 +1473,7 @@ a defect in the copy. Measured output:
[scope-probe] detach=1 scopes=3 frows=1 attach=1 scopes_back=2 frows_back=2 slot='ramp' [scope-probe] detach=1 scopes=3 frows=1 attach=1 scopes_back=2 frows_back=2 slot='ramp'
[scope-probe] gl copy=ok(67f) empty=ok(39f) empty_bound=1 [scope-probe] gl copy=ok(67f) empty=ok(39f) empty_bound=1
[scope-probe] gl detached ok=1 frames=42 rows=1 slot0='sine' [scope-probe] gl detached ok=1 frames=42 rows=1 slot0='sine'
[scope-probe] rclick=1
``` ```
- `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
@@ -1492,6 +1493,9 @@ a defect in the copy. Measured output:
signal into it" — landing in its row set. `drop=0` names a refused drop (a full plot or an signal into it" — landing in its row set. `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 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. windows were read or a group that lost its window.
- `rclick=1` is the scope window's **own right-click menu** being wired (one click gesture with
button 3 on its GL area, beside the drop target): the menu's actions act on that window's row
set.
- The fourth line is the windows' **GL account** (`gl copy=`/`gl empty=`): `ok(Nf)` is a context - The fourth line is the windows' **GL account** (`gl copy=`/`gl empty=`): `ok(Nf)` is a context
that staged N frames, `err:` carries the adapter's failure message. A scope whose context that staged N frames, `err:` carries the adapter's failure message. A scope whose context
failed draws *nothing* — no axes, no traces, exactly the "blank plot widget" report — so failed draws *nothing* — no axes, no traces, exactly the "blank plot widget" report — so
+173 -9
View File
@@ -621,6 +621,12 @@ void open_scope_window(App& app, int figure_row, bool detached) {
g_object_set_data(G_OBJECT(scope->drop), "udpscope-scope", scope.get()); 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); g_signal_connect(scope->drop, "drop", G_CALLBACK(on_scope_drop_channel), &app);
gtk_widget_add_controller(scope->widget, GTK_EVENT_CONTROLLER(scope->drop)); gtk_widget_add_controller(scope->widget, GTK_EVENT_CONTROLLER(scope->drop));
// The window's own right-click menu: its y scales, its traces, its removals -- the same
// actions the main window's plot menu carries, aimed at **this window's** row set.
GtkGesture* right = gtk_gesture_click_new();
gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(right), 3);
g_signal_connect(right, "pressed", G_CALLBACK(on_scope_right_click), &app);
gtk_widget_add_controller(scope->widget, GTK_EVENT_CONTROLLER(right));
scope->detached = detached; scope->detached = detached;
scope->window = GTK_WINDOW(gtk_window_new()); scope->window = GTK_WINDOW(gtk_window_new());
std::string title = figure_row >= 0 std::string title = figure_row >= 0
@@ -3338,8 +3344,9 @@ void on_y_auto_clicked(GtkButton* button, gpointer user) {
void on_fixed_range_apply(GtkButton*, gpointer user) { void on_fixed_range_apply(GtkButton*, gpointer user) {
FixedRangeContext* ctx = static_cast<FixedRangeContext*>(user); FixedRangeContext* ctx = static_cast<FixedRangeContext*>(user);
ctx->app->rows->set_y_range(ctx->row, gtk_spin_button_get_value(GTK_SPIN_BUTTON(ctx->lo)), RowSet& rows = ctx->rows != nullptr ? *ctx->rows : *ctx->app->rows;
gtk_spin_button_get_value(GTK_SPIN_BUTTON(ctx->hi))); rows.set_y_range(ctx->row, gtk_spin_button_get_value(GTK_SPIN_BUTTON(ctx->lo)),
gtk_spin_button_get_value(GTK_SPIN_BUTTON(ctx->hi)));
gtk_window_destroy(GTK_WINDOW(ctx->window)); gtk_window_destroy(GTK_WINDOW(ctx->window));
} }
@@ -3354,7 +3361,7 @@ void on_y_fixed_clicked(GtkButton* button, gpointer user) {
gtk_box_append(GTK_BOX(box), labeled_box("min", lo)); gtk_box_append(GTK_BOX(box), labeled_box("min", lo));
gtk_box_append(GTK_BOX(box), labeled_box("max", hi)); gtk_box_append(GTK_BOX(box), labeled_box("max", hi));
auto* ctx = new FixedRangeContext{app, row, lo, hi, window}; auto* ctx = new FixedRangeContext{app, nullptr, row, lo, hi, window};
GtkWidget* cancel = gtk_button_new_with_label("Cancel"); GtkWidget* cancel = gtk_button_new_with_label("Cancel");
g_signal_connect_swapped(cancel, "clicked", G_CALLBACK(gtk_window_destroy), window); g_signal_connect_swapped(cancel, "clicked", G_CALLBACK(gtk_window_destroy), window);
GtkWidget* apply = gtk_button_new_with_label("Apply"); GtkWidget* apply = gtk_button_new_with_label("Apply");
@@ -3374,9 +3381,9 @@ void on_y_fixed_clicked(GtkButton* button, gpointer user) {
/// Frees one trace slot of a row. All it takes is the free: the row is rebuilt with a series that is /// Frees one trace slot of a row. All it takes is the free: the row is rebuilt with a series that is
/// nothing, so it draws nothing and leaves the legend. /// nothing, so it draws nothing and leaves the legend.
void remove_trace(App& app, int row, int slot) { void remove_trace(RowSet& rows, int row, int slot) {
if (row < 0 || row >= app.rows->row_count()) return; if (row < 0 || row >= rows.row_count()) return;
app.rows->assign(row, slot, std::string(), std::string(), 0); rows.assign(row, slot, std::string(), std::string(), 0);
} }
/// Disables every math channel that (transitively) reads `name`, and answers with their names. /// Disables every math channel that (transitively) reads `name`, and answers with their names.
@@ -3429,7 +3436,7 @@ int remove_signal_from_plots(App& app, const std::string& name) {
// The slot remembers the *signal* (and its element), never the channel index -- the channel list is // The slot remembers the *signal* (and its element), never the channel index -- the channel list is
// renumbered on every enable change, which is what this is reacting to. // renumbered on every enable change, which is what this is reacting to.
if (app.rows->row(row).signal_name[static_cast<std::size_t>(slot)] != name) continue; if (app.rows->row(row).signal_name[static_cast<std::size_t>(slot)] != name) continue;
remove_trace(app, row, slot); remove_trace(*app.rows, row, slot);
++removed; ++removed;
} }
} }
@@ -3448,14 +3455,14 @@ void on_remove_clicked(GtkButton* button, gpointer user) {
int removed = 0; int removed = 0;
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) { for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
if (r.channel[static_cast<std::size_t>(s)] < 0) continue; if (r.channel[static_cast<std::size_t>(s)] < 0) continue;
remove_trace(app, target->row, s); remove_trace(*target->app->rows, target->row, s);
++removed; ++removed;
} }
what = "removed " + std::to_string(removed) + (removed == 1 ? " trace" : " traces") + what = "removed " + std::to_string(removed) + (removed == 1 ? " trace" : " traces") +
" from plot " + app.rows->plot_label(target->row); " from plot " + app.rows->plot_label(target->row);
} else { } else {
const std::string name = r.name[static_cast<std::size_t>(target->slot)]; const std::string name = r.name[static_cast<std::size_t>(target->slot)];
remove_trace(app, target->row, target->slot); remove_trace(*target->app->rows, target->row, target->slot);
what = "removed '" + name + "' from plot " + app.rows->plot_label(target->row); what = "removed '" + name + "' from plot " + app.rows->plot_label(target->row);
} }
@@ -3656,6 +3663,163 @@ void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer
open_row_menu(*app, row, x, y, widget); open_row_menu(*app, row, x, y, widget);
} }
/// Scope-menu helpers: the rows carry their window, their plot and their slot, the popover to
/// dismiss, and the handlers are free functions -- a menu row's handler is a plain callback, not a
/// lambda, whose commas would split the `G_CALLBACK` macro into arguments.
App::ScopeWindow* scope_menu_scope(GtkButton* b) {
return static_cast<App::ScopeWindow*>(g_object_get_data(G_OBJECT(b), "udpscope-scope"));
}
int scope_menu_row(GtkButton* b) {
return GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), "udpscope-row"));
}
void scope_menu_dismiss(GtkButton* b) {
GtkPopover* p = GTK_POPOVER(g_object_get_data(G_OBJECT(b), "udpscope-popover"));
if (p != nullptr) gtk_popover_popdown(p);
}
void on_scope_y_auto_clicked(GtkButton* b, gpointer) {
App::ScopeWindow* scope = scope_menu_scope(b);
if (scope == nullptr || scope->rows == nullptr) return;
scope->rows->set_y_auto(scope_menu_row(b), true);
scope_menu_dismiss(b);
}
/// The main menu's fixed-range dialog, aimed at **this window's** rows: the dialog's Apply
/// writes the row set the context names, and the context carries the scope's.
void on_scope_y_fixed_clicked(GtkButton* b, gpointer user) {
App* a = static_cast<App*>(user);
App::ScopeWindow* scope = scope_menu_scope(b);
const int row = scope_menu_row(b);
if (scope == nullptr || scope->rows == nullptr || scope->rows->row(row).plot == nullptr) return;
GtkWidget* window = dialog_window(a->window, "Fixed Y range");
GtkWidget* box = gtk_window_get_child(GTK_WINDOW(window));
const plot::Range cur = scope->rows->row(row).plot->y_range(0);
GtkWidget* lo = spin(-1.0e12, 1.0e12, 0.1, 6, cur.min);
GtkWidget* hi = spin(-1.0e12, 1.0e12, 0.1, 6, cur.max);
gtk_box_append(GTK_BOX(box), labeled_box("min", lo));
gtk_box_append(GTK_BOX(box), labeled_box("max", hi));
auto* ctx = new FixedRangeContext{a, scope->rows.get(), row, lo, hi, window};
GtkWidget* cancel = gtk_button_new_with_label("Cancel");
g_signal_connect_swapped(cancel, "clicked", G_CALLBACK(gtk_window_destroy), window);
GtkWidget* apply = gtk_button_new_with_label("Apply");
g_object_set_data_full(G_OBJECT(apply), "udpscope-ctx", ctx,
[](gpointer p) { delete static_cast<FixedRangeContext*>(p); });
g_signal_connect(apply, "clicked", G_CALLBACK(on_fixed_range_apply), ctx);
dialog_add_button(window, cancel);
dialog_add_button(window, apply);
gtk_window_present(GTK_WINDOW(window));
scope_menu_dismiss(b);
}
/// The y fit of a scope window: the window's x follows the main window's span, so the fit is the
/// y axes alone -- `fit_all()`'s x reset would be undone by the next frame's mirror.
void on_scope_fit_clicked(GtkButton* b, gpointer) {
App::ScopeWindow* scope = scope_menu_scope(b);
if (scope == nullptr || scope->rows == nullptr) return;
scope->rows->set_y_auto(scope_menu_row(b), true);
scope_menu_dismiss(b);
}
void on_scope_remove_clicked(GtkButton* b, gpointer) {
App::ScopeWindow* scope = scope_menu_scope(b);
if (scope == nullptr || scope->rows == nullptr) return;
remove_trace(*scope->rows, scope_menu_row(b),
GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), "udpscope-slot")));
scope_menu_dismiss(b);
}
void on_scope_attach_menu_clicked(GtkButton* b, gpointer user) {
App& a = *static_cast<App*>(user);
App::ScopeWindow* scope = scope_menu_scope(b);
if (scope == nullptr) return;
outlive_the_rebuild(GTK_WIDGET(b));
attach_scope_window(a, *scope);
}
/// A scope window's own right-click menu. A scope window is a *view with its own arrangement*:
/// its y scales, its traces and its removals are its own, so the menu acts on **that window's**
/// row set -- the same rows the drop target and the signal page's *Plot on* groups write. What it
/// deliberately does not carry is an x fit: the window follows the main window's time span, so an
/// x fit here would be undone by the next frame.
void on_scope_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer user) {
App* app = static_cast<App*>(user);
GtkWidget* widget = gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(gesture));
App::ScopeWindow* scope = nullptr;
for (const std::unique_ptr<App::ScopeWindow>& s : app->scopes) {
if (s->widget == widget) scope = s.get();
}
if (scope == nullptr || scope->rows == nullptr || scope->rows->row_count() <= 0) return;
const int row = std::clamp(static_cast<int>(scope->area->row_at(x, y)), 0,
scope->rows->row_count() - 1);
gtk_gesture_set_state(GTK_GESTURE(gesture), GTK_EVENT_SEQUENCE_CLAIMED);
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});
}
GtkWidget* popover = gtk_popover_new();
gtk_widget_add_css_class(popover, "udpscope-menu");
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
gtk_widget_set_margin_top(box, 6);
gtk_widget_set_margin_bottom(box, 6);
gtk_widget_set_margin_start(box, 6);
gtk_widget_set_margin_end(box, 6);
const auto row_btn = [&](const char* label) {
GtkWidget* b = gtk_button_new_with_label(label);
gtk_widget_add_css_class(b, "flat");
gtk_widget_set_halign(b, GTK_ALIGN_FILL);
g_object_set_data(G_OBJECT(b), "udpscope-popover", popover);
g_object_set_data(G_OBJECT(b), "udpscope-scope", scope);
g_object_set_data(G_OBJECT(b), "udpscope-row", GINT_TO_POINTER(row));
gtk_box_append(GTK_BOX(box), b);
return b;
};
char markup[64];
std::snprintf(markup, sizeof(markup), "<b>Plot %s</b>", scope->rows->plot_label(row).c_str());
GtkWidget* heading = gtk_label_new(nullptr);
gtk_label_set_markup(GTK_LABEL(heading), markup);
gtk_widget_set_halign(heading, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(box), heading);
GtkWidget* y_auto = row_btn("Y axis: auto scale");
g_signal_connect(y_auto, "clicked", G_CALLBACK(on_scope_y_auto_clicked), user);
GtkWidget* y_fixed = row_btn("Y axis: fixed range...");
g_signal_connect(y_fixed, "clicked", G_CALLBACK(on_scope_y_fixed_clicked), user);
GtkWidget* fit = row_btn("Fit Y axis");
g_signal_connect(fit, "clicked", G_CALLBACK(on_scope_fit_clicked), user);
// One removal per trace on the plot, flat rather than as a second view: a scope window holds
// at most its own figure's traces, and the menu is small.
bool traces = false;
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
const std::size_t si = static_cast<std::size_t>(s);
const std::string name = scope->rows->row(row).signal_name[si];
if (name.empty()) continue;
traces = true;
GtkWidget* rm = row_btn(("Remove '" + name + "'").c_str());
g_object_set_data(G_OBJECT(rm), "udpscope-slot", GINT_TO_POINTER(s));
g_signal_connect(rm, "clicked", G_CALLBACK(on_scope_remove_clicked), user);
}
if (!traces) {
GtkWidget* none = gtk_label_new("no traces on this plot");
gtk_widget_add_css_class(none, "dim-label");
gtk_widget_set_halign(none, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(box), none);
}
if (scope->detached) {
GtkWidget* back = row_btn("Attach back to the main window");
g_signal_connect(back, "clicked", G_CALLBACK(on_scope_attach_menu_clicked), user);
}
gtk_popover_set_child(GTK_POPOVER(popover), box);
GdkRectangle rect{static_cast<int>(x), static_cast<int>(y), 1, 1};
gtk_popover_set_pointing_to(GTK_POPOVER(popover), &rect);
gtk_widget_set_parent(popover, widget);
gtk_popover_popup(GTK_POPOVER(popover));
g_signal_connect(popover, "closed", G_CALLBACK(on_popover_closed), nullptr);
}
/// The availability of everything that does nothing until the stream has signals: the dock and the two /// The availability of everything that does nothing until the stream has signals: the dock and the two
/// slide-over pages that were once in it (Cursors, Measurements). "Disable, do not hide": a page may be /// slide-over pages that were once in it (Cursors, Measurements). "Disable, do not hide": a page may be
/// opened by its key before the first CONFIG, and shows its controls insensitive until then. The signal /// opened by its key before the first CONFIG, and shows its controls insensitive until then. The signal
+7
View File
@@ -1652,6 +1652,9 @@ struct ExportContext {
}; };
struct FixedRangeContext { struct FixedRangeContext {
App* app; App* app;
/// The row set the range applies to: `nullptr` is the main window's. A scope window's own
/// dialog writes its window's rows.
RowSet* rows = nullptr;
int row; int row;
GtkWidget* lo; GtkWidget* lo;
GtkWidget* hi; GtkWidget* hi;
@@ -2070,6 +2073,10 @@ gboolean on_scope_drop_channel(GtkDropTarget*, const GValue* value, double x, do
/// drop target both end here). False, with a notice, when the signal cannot be acquired or the plot /// drop target both end here). False, with a notice, when the signal cannot be acquired or the plot
/// holds four signals already. /// holds four signals already.
bool assign_signal_to_scope(App& app, App::ScopeWindow& scope, std::size_t signal, int row); bool assign_signal_to_scope(App& app, App::ScopeWindow& scope, std::size_t signal, int row);
/// The scope window's own right-click menu: the same row menu's actions, aimed at **that
/// window's** row set.
void on_scope_right_click(GtkGestureClick* gesture, int n_press, double x, double y,
gpointer user);
/// 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);
+18
View File
@@ -1915,6 +1915,24 @@ gboolean scope_probe_step(gpointer user) {
det.rows != nullptr && det.rows->row_count() > 0 det.rows != nullptr && det.rows->row_count() > 0
? det.rows->row(0).signal_name[0].c_str() ? det.rows->row(0).signal_name[0].c_str()
: ""); : "");
// The scope window carries its own right-click menu (its gesture is on the GL area, wired
// beside the drop target): one click gesture whose button is 3, per window.
int right_clicks = 0;
if (det.widget != nullptr) {
GListModel* controllers = gtk_widget_observe_controllers(det.widget);
const guint n = g_list_model_get_n_items(G_LIST_MODEL(controllers));
for (guint i = 0; i < n; ++i) {
GObject* c = G_OBJECT(g_list_model_get_item(controllers, i));
if (GTK_IS_GESTURE_SINGLE(c) &&
gtk_gesture_single_get_button(GTK_GESTURE_SINGLE(c)) == 3) {
++right_clicks;
}
g_object_unref(c);
}
g_object_unref(controllers);
}
std::fprintf(stderr, "[scope-probe] rclick=%d\n", right_clicks);
std::fflush(stderr);
} }
std::fflush(stderr); std::fflush(stderr);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;