working on todo.md features
This commit is contained in:
1 parent
210d124814
commit
8f54841c2c
110 files changed
+15932
-6576
No files matched your search
@@ -0,0 +1,432 @@
|
||||
// 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));
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
gtk_widget_set_visible(app.trigger_cmp_row, pulse);
|
||||
gtk_widget_set_visible(app.trigger_width_row, pulse);
|
||||
gtk_widget_set_visible(app.trigger_width2_row,
|
||||
pulse && (cmp == WidthCmp::Within || cmp == WidthCmp::Outside));
|
||||
gtk_widget_set_visible(app.trigger_level2_row, window);
|
||||
gtk_widget_set_visible(app.trigger_timeout_row, type == TriggerType::Dropout);
|
||||
// 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);
|
||||
}
|
||||
gtk_revealer_set_reveal_child(GTK_REVEALER(app->trigger_revealer), 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);
|
||||
}
|
||||
|
||||
GtkWidget* build_trigger_panel(App& app) {
|
||||
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||
gtk_widget_set_margin_top(box, 8);
|
||||
gtk_widget_set_margin_bottom(box, 8);
|
||||
gtk_widget_set_margin_start(box, 8);
|
||||
gtk_widget_set_margin_end(box, 8);
|
||||
|
||||
GtkWidget* heading = gtk_label_new("Trigger");
|
||||
gtk_widget_set_halign(heading, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(box), heading);
|
||||
|
||||
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);
|
||||
gtk_box_append(GTK_BOX(box), labeled_box("signal", 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");
|
||||
gtk_box_append(GTK_BOX(box), labeled_box("condition", app.trigger_type_drop));
|
||||
|
||||
const char* edges[] = {"rising", "falling", "both", nullptr};
|
||||
app.trigger_edge_drop = gtk_drop_down_new_from_strings(edges);
|
||||
gtk_box_append(GTK_BOX(box), labeled_box("edge", 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 = labeled_box("width is", app.trigger_cmp_drop);
|
||||
gtk_box_append(GTK_BOX(box), app.trigger_cmp_row);
|
||||
|
||||
app.trigger_width_spin = spin(0.0, 3600.0, 0.001, 6, 0.001);
|
||||
app.trigger_width_row = labeled_box("width s", app.trigger_width_spin);
|
||||
gtk_box_append(GTK_BOX(box), app.trigger_width_row);
|
||||
|
||||
app.trigger_width2_spin = spin(0.0, 3600.0, 0.001, 6, 0.002);
|
||||
app.trigger_width2_row = labeled_box("and s", app.trigger_width2_spin);
|
||||
gtk_box_append(GTK_BOX(box), app.trigger_width2_row);
|
||||
|
||||
// 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 = labeled_box("to level", app.trigger_level2_spin);
|
||||
gtk_box_append(GTK_BOX(box), app.trigger_level2_row);
|
||||
|
||||
app.trigger_timeout_spin = spin(0.0, 3600.0, 0.001, 6, 0.001);
|
||||
app.trigger_timeout_row = labeled_box("timeout s", app.trigger_timeout_spin);
|
||||
gtk_box_append(GTK_BOX(box), app.trigger_timeout_row);
|
||||
|
||||
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);
|
||||
gtk_box_append(GTK_BOX(box), labeled_box("level", app.trigger_level_spin));
|
||||
|
||||
app.trigger_pre_spin = spin(0.0, 99.0, 1.0, 0, 50.0);
|
||||
gtk_box_append(GTK_BOX(box), labeled_box("pre-trigger %", app.trigger_pre_spin));
|
||||
|
||||
app.trigger_holdoff_spin = spin(0.0, 3600.0, 0.001, 4,
|
||||
app.model->config().trigger_holdoff_s);
|
||||
gtk_box_append(GTK_BOX(box), labeled_box("hold-off s", app.trigger_holdoff_spin));
|
||||
|
||||
const char* modes[] = {"normal", "single", nullptr};
|
||||
app.trigger_mode_drop = gtk_drop_down_new_from_strings(modes);
|
||||
gtk_box_append(GTK_BOX(box), labeled_box("mode", 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 `labeled_box()`: 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.
|
||||
{
|
||||
GtkWidget* row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
GtkWidget* text = gtk_label_new("record events");
|
||||
gtk_widget_set_halign(text, GTK_ALIGN_START);
|
||||
gtk_widget_set_hexpand(text, TRUE);
|
||||
gtk_box_append(GTK_BOX(row), text);
|
||||
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(row), app.events_record_switch);
|
||||
gtk_box_append(GTK_BOX(box), row);
|
||||
}
|
||||
|
||||
app.arm_button = gtk_button_new_with_label("Arm");
|
||||
g_signal_connect(app.arm_button, "clicked", G_CALLBACK(on_arm_clicked), &app);
|
||||
gtk_box_append(GTK_BOX(box), app.arm_button);
|
||||
|
||||
GtkWidget* live = gtk_button_new_with_label("Go live (trigger off)");
|
||||
gtk_widget_set_tooltip_text(live, "disable trigger mode and stream continuously");
|
||||
g_signal_connect(live, "clicked", G_CALLBACK(on_go_live), &app);
|
||||
gtk_box_append(GTK_BOX(box), live);
|
||||
|
||||
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}) {
|
||||
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.
|
||||
GtkWidget* search_exp = gtk_expander_new("Search the buffer");
|
||||
gtk_widget_set_tooltip_text(search_exp,
|
||||
"find every place in the retained buffer where the condition "
|
||||
"above holds, and step through them");
|
||||
GtkWidget* sbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||
gtk_widget_set_margin_top(sbox, 4);
|
||||
|
||||
GtkWidget* srun = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
app.search_run_button = gtk_button_new_with_label("Search");
|
||||
gtk_widget_set_hexpand(app.search_run_button, true);
|
||||
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;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
c = a->model->trigger().condition;
|
||||
channel = a->model->trigger().channel;
|
||||
}
|
||||
a->search.start(c, channel);
|
||||
update_search_panel(*a);
|
||||
}),
|
||||
&app);
|
||||
gtk_box_append(GTK_BOX(srun), app.search_run_button);
|
||||
app.search_cancel_button = gtk_button_new_with_label("Stop");
|
||||
gtk_widget_set_sensitive(app.search_cancel_button, false);
|
||||
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);
|
||||
gtk_box_append(GTK_BOX(srun), app.search_cancel_button);
|
||||
gtk_box_append(GTK_BOX(sbox), srun);
|
||||
|
||||
app.search_status_label = gtk_label_new("not searched");
|
||||
gtk_widget_set_halign(app.search_status_label, GTK_ALIGN_START);
|
||||
gtk_label_set_wrap(GTK_LABEL(app.search_status_label), true);
|
||||
gtk_widget_add_css_class(app.search_status_label, "dim-label");
|
||||
gtk_box_append(GTK_BOX(sbox), app.search_status_label);
|
||||
|
||||
GtkWidget* snav = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
app.search_prev_button = gtk_button_new_from_icon_name("go-previous-symbolic");
|
||||
gtk_widget_set_tooltip_text(app.search_prev_button, "the previous hit");
|
||||
gtk_widget_set_sensitive(app.search_prev_button, false);
|
||||
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);
|
||||
gtk_box_append(GTK_BOX(snav), app.search_prev_button);
|
||||
|
||||
app.search_pos_label = gtk_label_new("--");
|
||||
gtk_widget_set_hexpand(app.search_pos_label, true);
|
||||
gtk_box_append(GTK_BOX(snav), app.search_pos_label);
|
||||
|
||||
app.search_next_button = gtk_button_new_from_icon_name("go-next-symbolic");
|
||||
gtk_widget_set_tooltip_text(app.search_next_button, "the next hit");
|
||||
gtk_widget_set_sensitive(app.search_next_button, false);
|
||||
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);
|
||||
gtk_box_append(GTK_BOX(snav), app.search_next_button);
|
||||
gtk_box_append(GTK_BOX(sbox), snav);
|
||||
|
||||
app.search_marks_label = gtk_label_new(" ");
|
||||
gtk_widget_set_halign(app.search_marks_label, GTK_ALIGN_START);
|
||||
gtk_widget_add_css_class(app.search_marks_label, "dim-label");
|
||||
// Ellipsize so the label's minimum width is bounded: without this, the longest text
|
||||
// (~58 chars) would widen the side pane just as `search_status_label` would without its own
|
||||
// wrap — the same fix the statistics label carries at line 2780.
|
||||
gtk_label_set_ellipsize(GTK_LABEL(app.search_marks_label), PANGO_ELLIPSIZE_END);
|
||||
gtk_box_append(GTK_BOX(sbox), app.search_marks_label);
|
||||
|
||||
gtk_expander_set_child(GTK_EXPANDER(search_exp), sbox);
|
||||
gtk_box_append(GTK_BOX(box), search_exp);
|
||||
|
||||
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
|
||||
Reference in new issue
Block a user