Gated measurements #2

Merged
martino merged 12 commits from gated-measurements into main 2026-11-06 12:30:43 +01:00
6 changed files with 327 additions and 37 deletions
Showing only changes of commit 0c9333858c - Show all commits

No files matched your search

+5 -3
View File
@@ -145,9 +145,11 @@ 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
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. A right-click on a scope window's plot opens **its own
menu** — y axis (auto or a fixed range), *Fit X axis*, *Fit all*, remove a trace — acting on that
window's rows (the x fit resets the **shared** time span, so every view follows); a detached
"plot 2"`), one button per plot of it. A scope window also **carries its own layout editor**: `L` (or the grid button in its corner,
revealed like the others) shows the same `+`/`×`/Done controls the main window's editor has, and
they change *that* window's rows — so a second window can hold several figures as easily as the
main one. A right-click on a scope window's plot opens **its own menu** — y axis (auto or a fixed
range), *Fit X axis*, *Fit all*, remove a trace — acting on that window's rows (the x fit resets the **shared** time span, so every view follows); a detached
window's menu carries **Attach back to the main window** beside them. A window that cannot paint
says so on stderr and in the status bar after two seconds, naming its **GSK renderer**: a blank
plot is indistinguishable from a context that failed, and the GTK **Vulkan** renderer is known to
@@ -380,3 +380,12 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
figure row carries **no** toggle (and no menu entry): there is nothing to enlarge *to*, and a
visible no-op is worse than none. The button reappears while a row *is* maximized, because
there it is the way back.
- **A view's arrangement is the view's, editor included.** A scope window holds its own figure
rows and edits them with its own `+`/`×`/Done set (`set_scope_layout_editing()`,
`rebuild_scope_layout_editor()`, `sync_scope_layout_editor()`): the buttons are tagged with the
window and the handlers act on *that* window's row set, so the two editors cannot move each
other's figures. `L` acts on the window the key arrived in (a `GtkEventControllerKey` per
window; every other accelerator reaches the application through `run_app_action()`), and a
pinned layout change is **not** written to the main window's session — the windows themselves
are not persisted.
+4
View File
@@ -1500,6 +1500,10 @@ a defect in the copy. Measured output:
is the defect this exists for: `pin_event_window()` takes **spans**, and passing it a range's
signed `min` turned the window inside out, so a scope in Held or Paused mode drew no trace and no
x labels at all.
- `layout buttons=3 rows=1->2` is the window's **own layout editor**: the toggle builds
`2 × rows + 1` buttons (a `×` per row, a `+` per boundary, Done is not counted) for one figure,
and emitting the first boundary `+` adds a row. A wrong count is a button set built for the main
window's rows; `rows` unchanged is a handler writing the wrong row set.
- `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.
+276 -30
View File
@@ -645,30 +645,31 @@ void open_scope_window(App& app, int figure_row, bool detached) {
if (detached) title += " (detached)";
gtk_window_set_title(scope->window, title.c_str());
gtk_window_set_default_size(scope->window, 900, 500);
// The overlay holds every corner control this window has: the attach button (detached windows
// only), the layout toggle, and the layout editor's own buttons. It is **always** created, not
// only for detached windows, because a scope is a view with its own arrangement and every one of
// them can be rearranged. The motion controller lives on the overlay, so a revealed button sits
// *under* the pointer and the event still travels the ancestor -- a leave on the GL widget would
// fire the moment the button appeared and hide it again.
scope->overlay = gtk_overlay_new();
gtk_overlay_set_child(GTK_OVERLAY(scope->overlay), scope->widget);
gtk_window_set_child(scope->window, scope->overlay);
GtkEventController* motion = gtk_event_controller_motion_new();
g_signal_connect(motion, "motion", G_CALLBACK(+[](GtkEventControllerMotion*, double x,
double y, gpointer user) {
App::ScopeWindow* scope = static_cast<App::ScopeWindow*>(user);
scope->pointer_x = x;
scope->pointer_y = y;
scope->pointer_valid = true;
}),
scope.get());
g_signal_connect(motion, "leave", G_CALLBACK(+[](GtkEventControllerMotion*, gpointer user) {
static_cast<App::ScopeWindow*>(user)->pointer_valid = false;
}),
scope.get());
gtk_widget_add_controller(scope->overlay, GTK_EVENT_CONTROLLER(motion));
if (detached) {
// The attach control, and it is the main window's own reveal mechanism in miniature: an
// overlay wrapping the GL area (so a revealed button sits *under* the pointer and the motion
// event still travels the ancestor -- a leave on the GL widget would fire the moment the
// button appeared and hide it again), and one button in the top-right corner, hidden until
// the pointer nears it. Only a detached window carries one: a copy or an empty window has
// nothing to put back.
scope->attach_overlay = gtk_overlay_new();
gtk_overlay_set_child(GTK_OVERLAY(scope->attach_overlay), scope->widget);
gtk_window_set_child(scope->window, scope->attach_overlay);
GtkEventController* motion = gtk_event_controller_motion_new();
g_signal_connect(motion, "motion", G_CALLBACK(+[](GtkEventControllerMotion*, double x,
double y, gpointer user) {
App::ScopeWindow* scope = static_cast<App::ScopeWindow*>(user);
scope->pointer_x = x;
scope->pointer_y = y;
scope->pointer_valid = true;
}),
scope.get());
g_signal_connect(motion, "leave", G_CALLBACK(+[](GtkEventControllerMotion*, gpointer user) {
static_cast<App::ScopeWindow*>(user)->pointer_valid = false;
}),
scope.get());
gtk_widget_add_controller(scope->attach_overlay, GTK_EVENT_CONTROLLER(motion));
// Only a detached window has something to put back.
GtkWidget* attach = gtk_button_new_from_icon_name("view-restore-symbolic");
gtk_widget_set_tooltip_text(attach, "attach this figure back to the main window");
gtk_widget_add_css_class(attach, "circular");
@@ -686,11 +687,34 @@ void open_scope_window(App& app, int figure_row, bool detached) {
attach_scope_window(a, *scope);
}),
&app);
gtk_overlay_add_overlay(GTK_OVERLAY(scope->attach_overlay), attach);
gtk_overlay_add_overlay(GTK_OVERLAY(scope->overlay), attach);
scope->attach_buttons.push_back(attach);
} else {
gtk_window_set_child(scope->window, scope->widget);
}
// The layout toggle: the editor's door, the same one the main window's overflow menu carries,
// revealed by the same proximity rule.
scope->layout_toggle = gtk_button_new_from_icon_name("view-grid-symbolic");
gtk_widget_set_tooltip_text(scope->layout_toggle,
"layout editor: add and remove figure rows in this window");
gtk_widget_add_css_class(scope->layout_toggle, "circular");
gtk_widget_add_css_class(scope->layout_toggle, "osd");
gtk_widget_set_size_request(scope->layout_toggle, kLayoutButtonPx, kLayoutButtonPx);
gtk_widget_set_visible(scope->layout_toggle, FALSE);
g_object_set_data(G_OBJECT(scope->layout_toggle), "udpscope-scope", scope.get());
g_signal_connect(scope->layout_toggle, "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;
set_scope_layout_editing(a, *scope, !scope->layout_editing);
}),
&app);
gtk_overlay_add_overlay(GTK_OVERLAY(scope->overlay), scope->layout_toggle);
// The window's keyboard: the same accelerators the main window's keys carry. `L` acts on *this*
// window's layout; every other action reaches the application (see `on_scope_key()`).
GtkEventController* keys = gtk_event_controller_key_new();
g_signal_connect(keys, "key-pressed", G_CALLBACK(on_scope_key), &app);
gtk_widget_add_controller(GTK_WIDGET(scope->window), GTK_EVENT_CONTROLLER(keys));
if (GtkApplication* application = gtk_window_get_application(GTK_WINDOW(app.window))) {
gtk_window_set_application(scope->window, application);
}
@@ -1255,9 +1279,7 @@ void on_detach_clicked(GtkButton* b, gpointer user) {
/// numbers so they cannot disagree.
void sync_scope_attach(App& app, App::ScopeWindow& scope) {
(void)app;
if (scope.attach_overlay == nullptr || scope.attach_buttons.empty() || scope.widget == nullptr) {
return;
}
if (scope.overlay == nullptr || scope.widget == nullptr) return;
const double scale = scope.area != nullptr && scope.area->device_scale() > 0.0f
? static_cast<double>(scope.area->device_scale())
: 1.0;
@@ -1280,6 +1302,227 @@ void sync_scope_attach(App& app, App::ScopeWindow& scope) {
scope.pointer_y <= by + kLayoutButtonPx + kMaximizeRevealPx;
gtk_widget_set_visible(b, revealed);
}
if (scope.layout_toggle != nullptr) {
// The layout toggle sits at the corner, to the left of the attach button when there is one.
const double bx = std::clamp(static_cast<double>(width - kLayoutButtonPx - kLayoutButtonPad -
(scope.attach_buttons.empty()
? 0
: kLayoutButtonPx + kLayoutButtonPad)),
0.0, std::max(0.0, width - kLayoutButtonPx));
const double by = std::clamp(static_cast<double>(kLayoutButtonPad), 0.0,
std::max(0.0, height - kLayoutButtonPx));
gtk_widget_set_halign(scope.layout_toggle, GTK_ALIGN_START);
gtk_widget_set_valign(scope.layout_toggle, GTK_ALIGN_START);
gtk_widget_set_margin_start(scope.layout_toggle, static_cast<int>(bx));
gtk_widget_set_margin_top(scope.layout_toggle, static_cast<int>(by));
const bool revealed =
!scope.layout_editing && scope.pointer_valid &&
scope.pointer_x >= bx - kMaximizeRevealPx &&
scope.pointer_x <= bx + kLayoutButtonPx + kMaximizeRevealPx &&
scope.pointer_y >= by - kMaximizeRevealPx &&
scope.pointer_y <= by + kLayoutButtonPx + kMaximizeRevealPx;
gtk_widget_set_visible(scope.layout_toggle, revealed);
}
// The attach button steps aside while the editor is up: Done is the way out of that mode.
for (GtkWidget* b : scope.attach_buttons) {
if (scope.layout_editing) gtk_widget_set_visible(b, FALSE);
}
}
// --- the scope windows' own layout editor -----------------------------------------------------
//
// A scope window is a view with its own arrangement, and its arrangement is the user's: it holds
// as many figure rows as it is given and edits them with the same controls the main window's
// editor carries (`+` on a boundary, `×` on a row, Done to leave). The buttons are the window's
// own -- the main editor's target is the main window -- so the two editors cannot move each
// other's rows.
namespace {
/// Data keys of a scope editor's buttons: the figure row a `×` removes, the boundary a `+` splits.
constexpr const char* kScopeLayoutRowKey = "udpscope-scope-layout-row";
constexpr const char* kScopeLayoutBoundaryKey = "udpscope-scope-layout-boundary";
App::ScopeWindow* scope_layout_scope(GtkWidget* w) {
return static_cast<App::ScopeWindow*>(g_object_get_data(G_OBJECT(w), "udpscope-scope"));
}
} // namespace
void rebuild_scope_layout_editor(App& app, App::ScopeWindow& scope);
void set_scope_layout_editing(App& app, App::ScopeWindow& scope, bool on);
void clear_scope_layout_editor(App::ScopeWindow& scope) {
if (scope.overlay != nullptr) {
for (GtkWidget* b : scope.layout_buttons) {
gtk_overlay_remove_overlay(GTK_OVERLAY(scope.overlay), b);
}
if (scope.layout_done != nullptr) {
gtk_overlay_remove_overlay(GTK_OVERLAY(scope.overlay), scope.layout_done);
}
}
scope.layout_buttons.clear();
scope.layout_done = nullptr;
scope.layout_buttons_rows = -1;
}
/// One of the scope editor's buttons: the main editor's look (`circular`, `osd`, the button size)
/// on this window's overlay.
GtkWidget* scope_layout_button(App::ScopeWindow& scope, const char* icon, const char* tip) {
GtkWidget* b = gtk_button_new_from_icon_name(icon);
gtk_widget_set_tooltip_text(b, tip);
gtk_widget_add_css_class(b, "circular");
gtk_widget_add_css_class(b, "osd");
gtk_widget_set_size_request(b, kLayoutButtonPx, kLayoutButtonPx);
g_object_set_data(G_OBJECT(b), "udpscope-scope", &scope);
gtk_overlay_add_overlay(GTK_OVERLAY(scope.overlay), b);
scope.layout_buttons.push_back(b);
return b;
}
void on_scope_layout_row_remove(GtkButton* b, gpointer user) {
App& app = *static_cast<App*>(user);
App::ScopeWindow* scope = scope_layout_scope(GTK_WIDGET(b));
if (scope == nullptr || scope->rows == nullptr) return;
const int fr = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), kScopeLayoutRowKey)) - 1;
outlive_the_rebuild(GTK_WIDGET(b));
if (!scope->rows->remove_row(fr)) {
set_notice(app, "a window keeps at least one figure", 4);
return;
}
if (scope->area != nullptr) scope->area->set_figure(&scope->rows->figure());
rebuild_scope_layout_editor(app, *scope);
if (scope->widget != nullptr) gtk_gl_area_queue_render(GTK_GL_AREA(scope->widget));
}
void on_scope_layout_row_insert(GtkButton* b, gpointer user) {
App& app = *static_cast<App*>(user);
App::ScopeWindow* scope = scope_layout_scope(GTK_WIDGET(b));
if (scope == nullptr || scope->rows == nullptr) return;
const int at = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), kScopeLayoutBoundaryKey));
outlive_the_rebuild(GTK_WIDGET(b));
if (!scope->rows->insert_row(at)) {
set_notice(app, "this window holds " + std::to_string(RowSet::kMaxRows) + " figures already", 4);
return;
}
if (scope->area != nullptr) scope->area->set_figure(&scope->rows->figure());
rebuild_scope_layout_editor(app, *scope);
if (scope->widget != nullptr) gtk_gl_area_queue_render(GTK_GL_AREA(scope->widget));
}
void on_scope_layout_done(GtkButton* b, gpointer user) {
App& app = *static_cast<App*>(user);
App::ScopeWindow* scope = scope_layout_scope(GTK_WIDGET(b));
if (scope == nullptr) return;
set_scope_layout_editing(app, *scope, false);
}
void rebuild_scope_layout_editor(App& app, App::ScopeWindow& scope) {
clear_scope_layout_editor(scope);
if (!scope.layout_editing || scope.rows == nullptr || scope.overlay == nullptr) return;
const int frows = scope.rows->figure_row_count();
scope.layout_buttons_rows = frows;
for (int fr = 0; fr < frows; ++fr) {
GtkWidget* rm = scope_layout_button(scope, "window-close-symbolic", "remove this figure");
g_object_set_data(G_OBJECT(rm), kScopeLayoutRowKey, GINT_TO_POINTER(fr + 1));
g_signal_connect(rm, "clicked", G_CALLBACK(on_scope_layout_row_remove), &app);
}
for (int at = 0; at <= frows; ++at) {
GtkWidget* add = scope_layout_button(scope, "list-add-symbolic", "add a figure here");
g_object_set_data(G_OBJECT(add), kScopeLayoutBoundaryKey, GINT_TO_POINTER(at));
g_signal_connect(add, "clicked", G_CALLBACK(on_scope_layout_row_insert), &app);
}
scope.layout_done = gtk_button_new_with_label("Done");
gtk_widget_set_tooltip_text(scope.layout_done, "leave the layout editor");
gtk_widget_add_css_class(scope.layout_done, "osd");
gtk_widget_set_size_request(scope.layout_done, -1, kLayoutButtonPx);
g_object_set_data(G_OBJECT(scope.layout_done), "udpscope-scope", &scope);
g_signal_connect(scope.layout_done, "clicked", G_CALLBACK(on_scope_layout_done), &app);
gtk_overlay_add_overlay(GTK_OVERLAY(scope.overlay), scope.layout_done);
}
/// Restates where the editor's buttons sit, from the rects the last frame was laid out with: the
/// `×` inside its row's left edge, the `+` on its boundary, Done in the corner.
void sync_scope_layout_editor(App& app, App::ScopeWindow& scope) {
if (!scope.layout_editing || scope.rows == nullptr || scope.area == nullptr ||
scope.widget == nullptr) {
return;
}
const int frows = scope.rows->figure_row_count();
if (frows != scope.layout_buttons_rows) rebuild_scope_layout_editor(app, scope);
const plot::FigureLayout& fl = scope.rows->figure().layout();
if (static_cast<int>(fl.rows.size()) != frows) return;
const double scale =
scope.area->device_scale() > 0.0f ? static_cast<double>(scope.area->device_scale()) : 1.0;
const double height = static_cast<double>(gtk_widget_get_height(scope.widget));
const double width = static_cast<double>(gtk_widget_get_width(scope.widget));
if (!(height > 0.0) || !(width > 0.0)) return;
const double half = kLayoutButtonPx * 0.5;
for (GtkWidget* b : scope.layout_buttons) {
const int fr = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), kScopeLayoutRowKey)) - 1;
const int at = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), kScopeLayoutBoundaryKey));
gtk_widget_set_halign(b, GTK_ALIGN_START);
gtk_widget_set_valign(b, GTK_ALIGN_START);
if (fr >= 0) {
if (fr >= frows) continue;
const plot::Rect& row = fl.rows[static_cast<std::size_t>(fr)];
gtk_widget_set_margin_top(
b, static_cast<int>(std::clamp(static_cast<double>(row.y) / scale + kLayoutButtonPad, 0.0,
height - kLayoutButtonPx) + 0.5));
// Inside the axes, not over the y labels: the row's first cell's plot knows where they end.
const plot::Plot* p = scope.rows->row(scope.rows->first_plot_of_row(fr)).plot;
const double left =
(p != nullptr ? static_cast<double>(p->layout().axes.x) / scale
: static_cast<double>(row.x) / scale) + kLayoutButtonPad;
gtk_widget_set_margin_start(b, static_cast<int>(std::clamp(left, 0.0, width - kLayoutButtonPx) + 0.5));
} else if (at >= 0) {
const double edge = at < frows
? static_cast<double>(fl.rows[static_cast<std::size_t>(at)].y) / scale
: (static_cast<double>(fl.rows.back().y) +
static_cast<double>(fl.rows.back().h)) / scale;
gtk_widget_set_margin_top(
b, static_cast<int>(std::clamp(edge - half, 0.0, height - kLayoutButtonPx) + 0.5));
gtk_widget_set_margin_start(b, static_cast<int>(kLayoutButtonPad));
}
}
if (scope.layout_done != nullptr) {
gtk_widget_set_halign(scope.layout_done, GTK_ALIGN_END);
gtk_widget_set_valign(scope.layout_done, GTK_ALIGN_START);
gtk_widget_set_margin_end(scope.layout_done, kLayoutButtonPad);
gtk_widget_set_margin_top(scope.layout_done, kLayoutButtonPad);
}
}
void set_scope_layout_editing(App& app, App::ScopeWindow& scope, bool on) {
if (scope.layout_editing == on) return;
scope.layout_editing = on;
if (on) {
rebuild_scope_layout_editor(app, scope);
} else {
clear_scope_layout_editor(scope);
}
sync_scope_attach(app, scope);
if (scope.widget != nullptr) gtk_gl_area_queue_render(GTK_GL_AREA(scope.widget));
}
gboolean on_scope_key(GtkEventControllerKey* controller, guint keyval, guint, GdkModifierType state,
gpointer user) {
App& app = *static_cast<App*>(user);
const Accel* accel = find_accel(keyval, static_cast<unsigned>(state));
if (accel == nullptr) return FALSE;
if (accel->action == AppAction::LayoutEditor) {
// The window the key arrived in, not the main one: `L` is the layout editor of the view you
// are looking at.
GtkWidget* widget = gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(controller));
GtkWidget* window = widget != nullptr ? gtk_widget_get_ancestor(widget, GTK_TYPE_WINDOW) : nullptr;
for (const std::unique_ptr<App::ScopeWindow>& scope : app.scopes) {
if (GTK_WIDGET(scope->window) != window) continue;
set_scope_layout_editing(app, *scope, !scope->layout_editing);
return TRUE;
}
return FALSE;
}
// Everything else is the application's, exactly as the main window's keys take it.
run_app_action(app, accel->action);
return TRUE;
}
/// **Detach**: the figure row moves to a window of its own. Copied first (the new window opens
@@ -4650,7 +4893,10 @@ gboolean tick(gpointer user) {
if (scope->rows->rebuild_if_dirty() && scope->area != nullptr) {
scope->area->set_figure(&scope->rows->figure());
}
if (scope->detached) sync_scope_attach(*app, *scope);
if (scope->detached || scope->layout_toggle != nullptr) sync_scope_attach(*app, *scope);
// The window's own editor: buttons restated from the rects this frame was laid out with.
// Its own arrangement is its own -- nothing here saves the main window's session.
if (scope->layout_editing) sync_scope_layout_editor(*app, *scope);
// **A window that cannot paint says so.** A blank plot is indistinguishable from a
// context that failed or an area that never got a frame, and the report ("nothing is
// drawn, no axes") is exactly what this turns into a sentence: the window's own title
+19 -4
View File
@@ -225,10 +225,18 @@ struct App {
/// carries the attach control and can be put back. A copied or empty window is a plain second
/// view, and has no attach control.
bool detached = false;
/// The attach control: an overlay wrapping the GL widget, its button in the top-right corner
/// (revealed by pointer proximity, the same rule the main window's toggles follow), and the
/// pointer state the overlay's own motion controller keeps.
GtkWidget* attach_overlay = nullptr;
/// The overlay wrapping the GL widget: the corner controls (attach, the layout toggle, the
/// layout editor's own buttons) all live on it. Its motion controller keeps the pointer state
/// they are revealed by.
GtkWidget* overlay = nullptr;
/// Layout editing in this window: its own editor and toggle. A scope is a view with its own
/// arrangement, so it holds as many figure rows as it is given and edits them with the same
/// `+` / `×` / Done controls the main window's editor has -- its own, never the main window's.
bool layout_editing = false;
GtkWidget* layout_toggle = nullptr;
std::vector<GtkWidget*> layout_buttons;
GtkWidget* layout_done = nullptr;
int layout_buttons_rows = -1;
std::vector<GtkWidget*> attach_buttons;
double pointer_x = 0.0;
double pointer_y = 0.0;
@@ -2077,6 +2085,13 @@ 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
/// holds four signals already.
bool assign_signal_to_scope(App& app, App::ScopeWindow& scope, std::size_t signal, int row);
/// Runs one keyboard accelerator. Scope windows carry the same shortcuts as the main window: the
/// view-level ones (`L` -- the layout editor) act on the window the key arrived in; the rest reach
/// the application through `run_app_action()`.
gboolean on_scope_key(GtkEventControllerKey* controller, guint keyval, guint keycode,
GdkModifierType state, gpointer user);
/// Turns a scope window's own layout editor on or off, building or taking down its buttons.
void set_scope_layout_editing(App& app, App::ScopeWindow& scope, bool on);
/// 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,
+14
View File
@@ -1960,6 +1960,20 @@ gboolean scope_probe_step(gpointer user) {
scope_pin.max, scope_x.min, scope_x.max);
std::fflush(stderr);
a->rows->set_time_window(2.0); // unpin: leave the window as the run found it
// **The scope window's own layout editor**: the toggle builds its buttons and a `+` adds a
// figure row -- the arrangement of a view is the view's.
set_scope_layout_editing(*a, *a->scopes[1], true);
const std::size_t layout_buttons = a->scopes[1]->layout_buttons.size();
const int rows_before = a->scopes[1]->rows->row_count();
if (layout_buttons > static_cast<std::size_t>(rows_before)) {
g_signal_emit_by_name(a->scopes[1]->layout_buttons[static_cast<std::size_t>(rows_before)],
"clicked");
}
const int rows_after = a->scopes[1]->rows->row_count();
set_scope_layout_editing(*a, *a->scopes[1], false);
std::fprintf(stderr, "[scope-probe] layout buttons=%zu rows=%d->%d\n", layout_buttons,
rows_before, rows_after);
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
default: