Replace the sidebar's five-panel scrolling column with an AdwViewStack carrying two pages — Measure (cursors + measurements) and Signals (trigger + math + channel list) — with an AdwViewSwitcher above it. The trigger and cursors toggles change from "reveal/hide my pane" to "enable/disable my feature, leaving the pane insensitive", following the disable-not-hide principle. The keys probe loses a duplicate registration that was toggling the sidebar back on before the exit probe captured its state; the exit-probe captures now read adw_overlay_split_view_get_show_ sidebar() instead of gtk_widget_get_visible(), which AdwOverlaySplitView never changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
430 lines
22 KiB
C++
430 lines
22 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
//
|
|
// The trigger panel and everything the trigger change reaches: the condition rows it shows and hides\n// per shape, the arming, and the switch between the stream and a stored event.
|
|
|
|
#include "ui_app.hpp"
|
|
|
|
#include <gtk/gtk.h>
|
|
|
|
#include <string>
|
|
|
|
namespace udpscope {
|
|
namespace ui_impl {
|
|
|
|
void on_arm_clicked(GtkButton* button, gpointer user) {
|
|
App* app = static_cast<App*>(user);
|
|
(void)button;
|
|
bool armed = false;
|
|
int signal = -1;
|
|
{
|
|
// The producer reads (and, on a single-shot latch, writes) these fields under
|
|
// the same lock; an unguarded read-modify-write here can lose its update.
|
|
std::lock_guard<std::mutex> lock(app->model->mutex());
|
|
TriggerConfig& tc = app->model->trigger();
|
|
tc.armed = !tc.armed;
|
|
armed = tc.armed;
|
|
signal = tc.channel;
|
|
}
|
|
if (armed) {
|
|
app->event_held = false;
|
|
app->capturing = false;
|
|
app->model->trigger_time_valid().store(false, std::memory_order_relaxed);
|
|
}
|
|
// Arming is a statement that this signal should be watched, so its acquisition
|
|
// is switched on with it -- otherwise the trigger could never see a sample.
|
|
// Outside the lock: set_signal_enabled() takes it.
|
|
if (armed && signal >= 0) app->model->set_signal_enabled(static_cast<std::size_t>(signal), true);
|
|
gtk_button_set_label(GTK_BUTTON(app->arm_button), armed ? "Disarm" : "Arm");
|
|
}
|
|
|
|
/// The trigger type dropdown's order, as a table rather than a cast: the dropdown lists
|
|
/// only the conditions that are implemented, so its index is not `TriggerType`'s value.
|
|
TriggerType trigger_type_at(guint i) {
|
|
switch (i) {
|
|
case 1:
|
|
return TriggerType::PulseWidth;
|
|
case 2:
|
|
return TriggerType::Window;
|
|
case 3:
|
|
return TriggerType::Dropout;
|
|
default:
|
|
return TriggerType::Edge;
|
|
}
|
|
}
|
|
|
|
void publish_trigger_fields(App* app) {
|
|
if (app->updating_panels) return;
|
|
// Read the widgets first (they may be touched again by a re-entrant handler if a
|
|
// setter ran while the lock was held), then publish the values under the lock the
|
|
// producer reads them with.
|
|
const double level = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_level_spin));
|
|
const double pre = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_pre_spin));
|
|
const double holdoff = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_holdoff_spin));
|
|
const guint edge = gtk_drop_down_get_selected(GTK_DROP_DOWN(app->trigger_edge_drop));
|
|
const guint mode = gtk_drop_down_get_selected(GTK_DROP_DOWN(app->trigger_mode_drop));
|
|
const guint type = gtk_drop_down_get_selected(GTK_DROP_DOWN(app->trigger_type_drop));
|
|
const guint cmp = gtk_drop_down_get_selected(GTK_DROP_DOWN(app->trigger_cmp_drop));
|
|
const double width = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_width_spin));
|
|
const double width2 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_width2_spin));
|
|
const double level2 = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_level2_spin));
|
|
const double timeout = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_timeout_spin));
|
|
const double band = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_band_spin));
|
|
const double hf = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app->trigger_hf_spin));
|
|
{
|
|
std::lock_guard<std::mutex> lock(app->model->mutex());
|
|
TriggerConfig& tc = app->model->trigger();
|
|
tc.condition.level = level;
|
|
tc.pretrigger_pct = pre;
|
|
tc.holdoff_s = holdoff;
|
|
tc.condition.edge = edge == 0 ? TriggerEdge::Rising
|
|
: edge == 1 ? TriggerEdge::Falling
|
|
: TriggerEdge::Both;
|
|
tc.mode = mode == 0 ? TriggerMode::Normal : TriggerMode::Single;
|
|
tc.condition.type = trigger_type_at(type);
|
|
tc.condition.cmp = width_cmp_at(cmp);
|
|
tc.condition.width_s = width;
|
|
tc.condition.width2_s = width2;
|
|
tc.condition.level2 = level2;
|
|
tc.condition.timeout_s = timeout;
|
|
tc.condition.noise_band = band;
|
|
tc.condition.hf_fc = hf;
|
|
}
|
|
}
|
|
|
|
/// Shows only the rows the selected condition reads, so the panel never offers a figure the
|
|
/// condition ignores. The widgets keep their values while hidden: `EventCondition` carries
|
|
/// all eight fields whatever its type, and switching away and back must not lose a setting.
|
|
void sync_trigger_rows(App& app) {
|
|
if (app.trigger_type_drop == nullptr) return;
|
|
const TriggerType type =
|
|
trigger_type_at(gtk_drop_down_get_selected(GTK_DROP_DOWN(app.trigger_type_drop)));
|
|
const WidthCmp cmp =
|
|
width_cmp_at(gtk_drop_down_get_selected(GTK_DROP_DOWN(app.trigger_cmp_drop)));
|
|
const bool pulse = type == TriggerType::PulseWidth;
|
|
const bool window = type == TriggerType::Window;
|
|
// The rows come from the panel's `.ui`, so a missing one is a mistake there rather than an impossibility
|
|
// here: `show()` names that by doing nothing, and `[udpscope] trigger panel:` is what proves the row
|
|
// exists.
|
|
const auto show = [](GtkWidget* row, bool visible) {
|
|
if (row != nullptr) gtk_widget_set_visible(row, visible);
|
|
};
|
|
show(app.trigger_cmp_row, pulse);
|
|
show(app.trigger_width_row, pulse);
|
|
show(app.trigger_width2_row, pulse && (cmp == WidthCmp::Within || cmp == WidthCmp::Outside));
|
|
show(app.trigger_level2_row, window);
|
|
show(app.trigger_timeout_row, type == TriggerType::Dropout);
|
|
// A band widens the comparator every type but Dropout compares with; a dropout is an absence and a
|
|
// band cannot describe one. The HF reject sits before every comparison, so it applies to all types.
|
|
// The rows come from the file and hold their spin, so the spin's parent *is* the row.
|
|
show(gtk_widget_get_parent(app.trigger_band_spin), type != TriggerType::Dropout);
|
|
show(gtk_widget_get_parent(app.trigger_hf_spin), true);
|
|
// A band of zero width reduces the condition to a float-equality test on a real-valued
|
|
// signal, which is unusable: it fires on the exact value and nothing else, which in practice
|
|
// is never. The panel is where that is refused: the second level is pushed at least one
|
|
// increment clear of the first, in whichever direction it already lay.
|
|
if (window) {
|
|
GtkSpinButton* l2 = GTK_SPIN_BUTTON(app.trigger_level2_spin);
|
|
const double level = gtk_spin_button_get_value(GTK_SPIN_BUTTON(app.trigger_level_spin));
|
|
double step = 0.0;
|
|
gtk_spin_button_get_increments(l2, &step, nullptr);
|
|
const double other = gtk_spin_button_get_value(l2);
|
|
if (std::fabs(other - level) < step) {
|
|
gtk_spin_button_set_value(l2, other < level ? level - step : level + step);
|
|
}
|
|
}
|
|
}
|
|
|
|
void on_trigger_field_changed(GtkWidget*, gpointer user) {
|
|
publish_trigger_fields(static_cast<App*>(user));
|
|
}
|
|
|
|
/// The same publish, for the two dropdowns. It needs its own entry point because
|
|
/// `notify::<prop>` passes a `GParamSpec*` ahead of the user data while
|
|
/// `GtkSpinButton::value-changed` does not: a two-parameter callback on a `notify::`
|
|
/// signal reads the pspec as its `App*` and crashes on the first field it touches.
|
|
void on_trigger_drop_changed(GObject*, GParamSpec*, gpointer user) {
|
|
publish_trigger_fields(static_cast<App*>(user));
|
|
}
|
|
|
|
/// The two dropdowns that change *which rows exist*, as opposed to a value within them.
|
|
/// Same `notify::selected` signature trap as `on_trigger_drop_changed`.
|
|
void on_trigger_shape_changed(GObject*, GParamSpec*, gpointer user) {
|
|
App* app = static_cast<App*>(user);
|
|
publish_trigger_fields(app);
|
|
sync_trigger_rows(*app);
|
|
}
|
|
|
|
void on_trigger_signal_changed(GObject* obj, GParamSpec*, gpointer user) {
|
|
App* app = static_cast<App*>(user);
|
|
if (app->updating_panels) return;
|
|
(void)obj;
|
|
const guint sel = gtk_drop_down_get_selected(GTK_DROP_DOWN(app->trigger_signal_drop));
|
|
if (sel == GTK_INVALID_LIST_POSITION) return;
|
|
{
|
|
std::lock_guard<std::mutex> lock(app->model->mutex());
|
|
if (sel >= app->model->channels().size()) return;
|
|
app->model->trigger().channel = static_cast<int>(sel);
|
|
}
|
|
app->trigger_level_touched = false; // a new signal gets a fresh default level
|
|
}
|
|
|
|
void on_trigger_toggled(GtkToggleButton* button, gpointer user) {
|
|
App* app = static_cast<App*>(user);
|
|
const bool on = gtk_toggle_button_get_active(button);
|
|
int signal = -1;
|
|
app->model->set_trigger_enabled(on); // takes the lock and mirrors the atomic
|
|
{
|
|
std::lock_guard<std::mutex> lock(app->model->mutex());
|
|
TriggerConfig& tc = app->model->trigger();
|
|
tc.armed = on; // trigger mode means "wait for an event", so it starts armed
|
|
signal = tc.channel;
|
|
}
|
|
app->event_held = false;
|
|
app->capturing = false;
|
|
if (!on) {
|
|
app->model->trigger_time_valid().store(false, std::memory_order_relaxed);
|
|
// The epoch is *not* written here: the frame tick owns it and states the live answer -- minus
|
|
// the newest acquired sample -- after it has bound and acquired, which is the only moment the
|
|
// right number is knowable. This used to set it to 0 ("seconds since the stream started"),
|
|
// which is no longer a display coordinate anything uses. Nothing is left stale in the
|
|
// meantime: the group follows the live edge again from here, and `Figure::layout()` re-derives
|
|
// the range from it on every frame.
|
|
app->rows->set_time_window(app->rows->time_window()); // and follow the live edge
|
|
}
|
|
if (on && signal >= 0) {
|
|
app->model->set_signal_enabled(static_cast<std::size_t>(signal), true);
|
|
}
|
|
// Disabled, not hidden: a panel you can still see is a panel you can read, and the settings you
|
|
// are about to switch back on must not vanish while you look for them.
|
|
gtk_widget_set_sensitive(app->trigger_panel, on);
|
|
// The event-history button is part of trigger mode and the pane it opens is *on demand*; leaving
|
|
// trigger mode takes the button away and collapses the pane, so going back to streaming is not a
|
|
// column of stale events beside the plot. Recording is the other half of that answer, so this is
|
|
// `sync_events_availability()`'s call and not the trigger's alone.
|
|
sync_events_availability(*app);
|
|
gtk_button_set_label(GTK_BUTTON(app->arm_button), on ? "Disarm" : "Arm");
|
|
}
|
|
|
|
/// The way out of trigger mode, from inside the panel that froze the view: it is
|
|
/// the toolbar's trigger switch, so there is exactly one piece of state.
|
|
void on_go_live(GtkButton*, gpointer user) {
|
|
App* app = static_cast<App*>(user);
|
|
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app->trigger_toggle), FALSE);
|
|
}
|
|
|
|
/// The row the `.ui` declares for a control, with the control appended to it.
|
|
///
|
|
/// The label is the file's, because it is text a translator owns; the control is code's, because a
|
|
/// dropdown's model and a spin button's range are the application's. `labeled_box()` did both in code and
|
|
/// this is what replaces it, row by row. `nullptr` comes back when the file has no such row: the control is
|
|
/// then built and never placed, which is what `[udpscope] trigger panel:` reads.
|
|
GtkWidget* row_for(GtkBuilder* ui, const char* id, GtkWidget* child) {
|
|
GtkWidget* row = GTK_WIDGET(gtk_builder_get_object(ui, id));
|
|
if (row == nullptr) return nullptr;
|
|
gtk_widget_set_hexpand(child, true);
|
|
gtk_box_append(GTK_BOX(row), child);
|
|
return row;
|
|
}
|
|
|
|
GtkWidget* build_trigger_panel(App& app) {
|
|
// The panel's frame -- the heading, one labelled row per setting, the two buttons and the search block --
|
|
// is a UI file. What is not is every control that carries a value: a dropdown's model and a spin button's
|
|
// range are the application's, so they are built here and appended into the file's rows. `make test-ui`
|
|
// checks the file's types against the GIR; the ids below are the only thing that keeps the code and the
|
|
// file agreeing on the names.
|
|
GtkBuilder* ui = ui_load(app, "/org/udpscope/ui/trigger-panel.ui");
|
|
GtkWidget* box = GTK_WIDGET(gtk_builder_get_object(ui, "trigger_panel"));
|
|
if (box == nullptr) return gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
|
|
|
app.trigger_signal_drop = gtk_drop_down_new(G_LIST_MODEL(gtk_string_list_new(nullptr)), nullptr);
|
|
g_signal_connect(app.trigger_signal_drop, "notify::selected",
|
|
G_CALLBACK(on_trigger_signal_changed), &app);
|
|
row_for(ui, "signal_row", app.trigger_signal_drop);
|
|
|
|
const char* types[] = {"edge", "pulse width", "window", "dropout", nullptr};
|
|
app.trigger_type_drop = gtk_drop_down_new_from_strings(types);
|
|
gtk_widget_set_tooltip_text(app.trigger_type_drop,
|
|
"what counts as an event; the fields below change to suit it");
|
|
row_for(ui, "condition_row", app.trigger_type_drop);
|
|
|
|
const char* edges[] = {"rising", "falling", "both", nullptr};
|
|
app.trigger_edge_drop = gtk_drop_down_new_from_strings(edges);
|
|
row_for(ui, "edge_row", app.trigger_edge_drop);
|
|
|
|
const char* cmps[] = {"<", ">", "within", "outside", nullptr};
|
|
app.trigger_cmp_drop = gtk_drop_down_new_from_strings(cmps);
|
|
gtk_widget_set_tooltip_text(app.trigger_cmp_drop,
|
|
"how the measured pulse width is compared: '<' is the glitch "
|
|
"trigger -- a pulse shorter than it should be");
|
|
app.trigger_cmp_row = row_for(ui, "cmp_row", app.trigger_cmp_drop);
|
|
|
|
app.trigger_width_spin = spin(0.0, 3600.0, 0.001, 6, 0.001);
|
|
app.trigger_width_row = row_for(ui, "width_row", app.trigger_width_spin);
|
|
|
|
app.trigger_width2_spin = spin(0.0, 3600.0, 0.001, 6, 0.002);
|
|
app.trigger_width2_row = row_for(ui, "width2_row", app.trigger_width2_spin);
|
|
|
|
// Zero, not the range's minimum: the level this row reads is unset until a channel is chosen,
|
|
// and the panel's sync writes the declared range's middle then -- but a dialog that opens at
|
|
// -1e12 is a number nobody typed.
|
|
app.trigger_level2_spin = spin(-1.0e12, 1.0e12, 0.01, 4, 0.0);
|
|
gtk_widget_set_tooltip_text(app.trigger_level2_spin,
|
|
"the band's other edge; it is kept at least one increment away "
|
|
"from the level, because a band of zero width is not a band: the "
|
|
"condition becomes an exact float-equality test against the level, "
|
|
"which an ordinary signal never satisfies and a signal sitting at "
|
|
"the level satisfies on every transition");
|
|
app.trigger_level2_row = row_for(ui, "level2_row", app.trigger_level2_spin);
|
|
|
|
app.trigger_timeout_spin = spin(0.0, 3600.0, 0.001, 6, 0.001);
|
|
app.trigger_timeout_row = row_for(ui, "timeout_row", app.trigger_timeout_spin);
|
|
|
|
app.trigger_level_spin = spin(-1.0e12, 1.0e12, 0.01, 4, 0.0);
|
|
g_signal_connect(app.trigger_level_spin, "value-changed", G_CALLBACK(+[](GtkWidget*, gpointer u) {
|
|
static_cast<App*>(u)->trigger_level_touched = true;
|
|
}),
|
|
&app);
|
|
row_for(ui, "level_row", app.trigger_level_spin);
|
|
|
|
app.trigger_pre_spin = spin(0.0, 99.0, 1.0, 0, 50.0);
|
|
row_for(ui, "pre_row", app.trigger_pre_spin);
|
|
|
|
app.trigger_holdoff_spin = spin(0.0, 3600.0, 0.001, 4,
|
|
app.model->config().trigger_holdoff_s);
|
|
row_for(ui, "holdoff_row", app.trigger_holdoff_spin);
|
|
// A band in signal units, on the same scale as the level spins.
|
|
app.trigger_band_spin = spin(0.0, 1.0e12, 0.01, 4, 0.0);
|
|
row_for(ui, "band_row", app.trigger_band_spin);
|
|
// 0 is off. The upper bound is generous; `configure()` clamps to just under the stream's Nyquist,
|
|
// which the panel cannot know.
|
|
app.trigger_hf_spin = spin(0.0, 1.0e7, 1.0, 3, 0.0);
|
|
row_for(ui, "hf_row", app.trigger_hf_spin);
|
|
|
|
const char* modes[] = {"normal", "single", nullptr};
|
|
app.trigger_mode_drop = gtk_drop_down_new_from_strings(modes);
|
|
row_for(ui, "mode_row", app.trigger_mode_drop);
|
|
|
|
app.events_record_switch = gtk_switch_new();
|
|
gtk_widget_set_valign(app.events_record_switch, GTK_ALIGN_CENTER);
|
|
gtk_switch_set_active(GTK_SWITCH(app.events_record_switch), app.event_recording);
|
|
gtk_widget_set_tooltip_text(app.events_record_switch,
|
|
"keep every triggered acquisition in the event cache, with a "
|
|
"picture of each; the history button goes with it");
|
|
g_signal_connect(app.events_record_switch, "notify::active",
|
|
G_CALLBACK(on_events_record_toggled), &app);
|
|
// Not `row_for()`: that expands its child, which is what a spin button wants and what a switch must not
|
|
// have -- a switch's natural width *is* its size, so an expanded one stretches into a wide pill that
|
|
// reads as a slider. The label takes the slack instead, which is why the file's label carries the
|
|
// `hexpand` on this one row and no other.
|
|
if (GtkWidget* record_row = GTK_WIDGET(gtk_builder_get_object(ui, "record_row"))) {
|
|
gtk_widget_set_halign(app.events_record_switch, GTK_ALIGN_END);
|
|
gtk_widget_set_valign(app.events_record_switch, GTK_ALIGN_CENTER);
|
|
gtk_box_append(GTK_BOX(record_row), app.events_record_switch);
|
|
}
|
|
|
|
app.arm_button = GTK_WIDGET(gtk_builder_get_object(ui, "trigger_arm"));
|
|
if (app.arm_button != nullptr) {
|
|
g_signal_connect(app.arm_button, "clicked", G_CALLBACK(on_arm_clicked), &app);
|
|
}
|
|
GtkWidget* live = GTK_WIDGET(gtk_builder_get_object(ui, "trigger_live"));
|
|
if (live != nullptr) g_signal_connect(live, "clicked", G_CALLBACK(on_go_live), &app);
|
|
|
|
for (GtkWidget* w : {app.trigger_level_spin, app.trigger_pre_spin, app.trigger_holdoff_spin,
|
|
app.trigger_width_spin, app.trigger_width2_spin,
|
|
app.trigger_level2_spin, app.trigger_timeout_spin, app.trigger_band_spin,
|
|
app.trigger_hf_spin}) {
|
|
g_signal_connect(w, "value-changed", G_CALLBACK(on_trigger_field_changed), &app);
|
|
}
|
|
g_signal_connect(app.trigger_edge_drop, "notify::selected",
|
|
G_CALLBACK(on_trigger_drop_changed), &app);
|
|
g_signal_connect(app.trigger_mode_drop, "notify::selected",
|
|
G_CALLBACK(on_trigger_drop_changed), &app);
|
|
|
|
// Searching *is* the trigger's condition applied to history, so it belongs in this panel
|
|
// rather than in one of its own: a second place to spell the same condition would be a
|
|
// second place to get it wrong. Collapsed by default -- it is not part of running a trigger.
|
|
// Its widgets come from the same file, and only their signals are wired here.
|
|
app.search_run_button = GTK_WIDGET(gtk_builder_get_object(ui, "search_run"));
|
|
app.search_cancel_button = GTK_WIDGET(gtk_builder_get_object(ui, "search_cancel"));
|
|
app.search_status_label = GTK_WIDGET(gtk_builder_get_object(ui, "search_status"));
|
|
app.search_prev_button = GTK_WIDGET(gtk_builder_get_object(ui, "search_prev"));
|
|
app.search_pos_label = GTK_WIDGET(gtk_builder_get_object(ui, "search_pos"));
|
|
app.search_next_button = GTK_WIDGET(gtk_builder_get_object(ui, "search_next"));
|
|
app.search_marks_label = GTK_WIDGET(gtk_builder_get_object(ui, "search_marks"));
|
|
if (app.search_run_button != nullptr) {
|
|
g_signal_connect(app.search_run_button, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
|
App* a = static_cast<App*>(user);
|
|
// Published, not read back off the widgets: the producer's copy is the
|
|
// one the live trigger uses, so the search asks exactly its question.
|
|
publish_trigger_fields(a);
|
|
EventCondition c;
|
|
int channel = -1;
|
|
double dt = 0.0;
|
|
{
|
|
std::lock_guard<std::mutex> lock(a->model->mutex());
|
|
c = a->model->trigger().condition;
|
|
channel = a->model->trigger().channel;
|
|
dt = a->model->partition().slot_dt();
|
|
}
|
|
a->search.start(c, channel, dt);
|
|
update_search_panel(*a);
|
|
}),
|
|
&app);
|
|
}
|
|
if (app.search_cancel_button != nullptr) {
|
|
g_signal_connect(app.search_cancel_button, "clicked",
|
|
G_CALLBACK(+[](GtkButton*, gpointer user) {
|
|
App* a = static_cast<App*>(user);
|
|
a->search.cancel();
|
|
update_search_panel(*a);
|
|
}),
|
|
&app);
|
|
}
|
|
if (app.search_prev_button != nullptr) {
|
|
g_signal_connect(app.search_prev_button, "clicked",
|
|
G_CALLBACK(+[](GtkButton*, gpointer user) {
|
|
App* a = static_cast<App*>(user);
|
|
if (a->search.prev()) {
|
|
search_jump(*a, a->search.hits()[static_cast<std::size_t>(a->search.cursor())]);
|
|
}
|
|
update_search_panel(*a);
|
|
}),
|
|
&app);
|
|
}
|
|
if (app.search_next_button != nullptr) {
|
|
g_signal_connect(app.search_next_button, "clicked",
|
|
G_CALLBACK(+[](GtkButton*, gpointer user) {
|
|
App* a = static_cast<App*>(user);
|
|
// The first press with no cursor goes to the first hit, not the second.
|
|
if (a->search.cursor() < 0) {
|
|
a->search.select(0);
|
|
} else if (!a->search.next()) {
|
|
update_search_panel(*a);
|
|
return;
|
|
}
|
|
search_jump(*a, a->search.hits()[static_cast<std::size_t>(a->search.cursor())]);
|
|
update_search_panel(*a);
|
|
}),
|
|
&app);
|
|
}
|
|
|
|
g_signal_connect(app.trigger_type_drop, "notify::selected",
|
|
G_CALLBACK(on_trigger_shape_changed), &app);
|
|
g_signal_connect(app.trigger_cmp_drop, "notify::selected",
|
|
G_CALLBACK(on_trigger_shape_changed), &app);
|
|
// The clamp lives in `sync_trigger_rows()`, so the two level spins have to run it rather
|
|
// than the plain field publish.
|
|
for (GtkWidget* w : {app.trigger_level_spin, app.trigger_level2_spin}) {
|
|
g_signal_connect(w, "value-changed", G_CALLBACK(+[](GtkWidget*, gpointer u) {
|
|
sync_trigger_rows(*static_cast<App*>(u));
|
|
}),
|
|
&app);
|
|
}
|
|
sync_trigger_rows(app);
|
|
return box;
|
|
}
|
|
|
|
} // namespace ui_impl
|
|
} // namespace udpscope
|