// 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 #include #include namespace udpscope { namespace ui_impl { void on_arm_clicked(GtkButton* button, gpointer user) { App* app = static_cast(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 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(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. /// The trigger's four enumerations as words, and back. These are the panel's dropdown labels *and* /// what the session file stores: an index would tie the file to a declaration order, which is the /// lesson `math_op_at()` already carries, and two vocabularies (one for the UI, one for the file) /// drift apart the moment one of them gains a member. const char* trigger_type_text(TriggerType t) { switch (t) { case TriggerType::Edge: return "edge"; case TriggerType::PulseWidth: return "pulse width"; case TriggerType::Window: return "window"; case TriggerType::Dropout: return "dropout"; } return "edge"; } TriggerType trigger_type_from_text(const std::string& s) { if (s == "pulse width") return TriggerType::PulseWidth; if (s == "window") return TriggerType::Window; if (s == "dropout") return TriggerType::Dropout; return TriggerType::Edge; } const char* trigger_edge_text(TriggerEdge e) { switch (e) { case TriggerEdge::Rising: return "rising"; case TriggerEdge::Falling: return "falling"; case TriggerEdge::Both: return "both"; } return "rising"; } TriggerEdge trigger_edge_from_text(const std::string& s) { if (s == "falling") return TriggerEdge::Falling; if (s == "both") return TriggerEdge::Both; return TriggerEdge::Rising; } const char* width_cmp_text(WidthCmp c) { switch (c) { case WidthCmp::Less: return "<"; case WidthCmp::Greater: return ">"; case WidthCmp::Within: return "within"; case WidthCmp::Outside: return "outside"; } return ">"; } WidthCmp width_cmp_from_text(const std::string& s) { if (s == "<") return WidthCmp::Less; if (s == "within") return WidthCmp::Within; if (s == "outside") return WidthCmp::Outside; return WidthCmp::Greater; } const char* trigger_mode_text(TriggerMode m) { return m == TriggerMode::Single ? "single" : "normal"; } TriggerMode trigger_mode_from_text(const std::string& s) { return s == "single" ? TriggerMode::Single : TriggerMode::Normal; } /// The dropdown's row for a piece of text, or 0. Asked of the widget's own model, so the mapping from /// a stored word to a selection is the list the user sees and not a second table to keep in step. static guint dropdown_index_of(GtkWidget* drop, const char* text) { GListModel* model = drop != nullptr ? gtk_drop_down_get_model(GTK_DROP_DOWN(drop)) : nullptr; const guint n = model != nullptr ? g_list_model_get_n_items(model) : 0; for (guint i = 0; i < n; ++i) { GtkStringObject* item = GTK_STRING_OBJECT(g_list_model_get_item(model, i)); const bool hit = item != nullptr && std::strcmp(gtk_string_object_get_string(item), text) == 0; if (item != nullptr) g_object_unref(item); if (hit) return i; } return 0; } /// The panel's controls from the model: what a session load has just put in the model, so the pane /// shows the trigger it is about to run. Guarded, because a control's own handler publishes what it /// holds and this is the other direction. void write_trigger_widgets(App& app) { if (app.trigger_level_spin == nullptr) return; TriggerConfig tc; { std::lock_guard lock(app.model->mutex()); tc = app.model->trigger(); } const bool was = app.updating_panels; app.updating_panels = true; gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_level_spin), tc.condition.level); gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_pre_spin), tc.pretrigger_pct); gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_holdoff_spin), tc.holdoff_s); gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_width_spin), tc.condition.width_s); gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_width2_spin), tc.condition.width2_s); gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_level2_spin), tc.condition.level2); gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_timeout_spin), tc.condition.timeout_s); gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_band_spin), tc.condition.noise_band); gtk_spin_button_set_value(GTK_SPIN_BUTTON(app.trigger_hf_spin), tc.condition.hf_fc); gtk_drop_down_set_selected(GTK_DROP_DOWN(app.trigger_type_drop), dropdown_index_of(app.trigger_type_drop, trigger_type_text(tc.condition.type))); gtk_drop_down_set_selected(GTK_DROP_DOWN(app.trigger_edge_drop), dropdown_index_of(app.trigger_edge_drop, trigger_edge_text(tc.condition.edge))); gtk_drop_down_set_selected(GTK_DROP_DOWN(app.trigger_cmp_drop), dropdown_index_of(app.trigger_cmp_drop, width_cmp_text(tc.condition.cmp))); gtk_drop_down_set_selected(GTK_DROP_DOWN(app.trigger_mode_drop), dropdown_index_of(app.trigger_mode_drop, trigger_mode_text(tc.mode))); app.updating_panels = was; sync_trigger_rows(app); } /// The compact line: what the pane is set to, in the words the panel's own controls use. Read from the /// model rather than from the widgets, because the widgets are not on screen in this mode and are not /// rebuilt when a session loads. void sync_trigger_summary(App& app) { if (app.trigger_summary == nullptr) return; int row = 0; std::string reading; // the same content as one line, for the dump const auto put = [&](const char* key, const std::string& value) { if (row >= App::kTriggerSummaryRows) return; gtk_label_set_text(GTK_LABEL(app.trigger_summary_keys[row]), key); gtk_label_set_text(GTK_LABEL(app.trigger_summary_values[row]), value.c_str()); gtk_widget_set_visible(app.trigger_summary_keys[row], TRUE); gtk_widget_set_visible(app.trigger_summary_values[row], TRUE); reading += std::string(reading.empty() ? "" : " | ") + key + ": " + value; ++row; }; const auto ms = [](double seconds) { char b[64]; std::snprintf(b, sizeof(b), "%.6g ms", seconds * 1000.0); return std::string(b); }; const auto number = [](double v) { char b[64]; std::snprintf(b, sizeof(b), "%.6g", v); return std::string(b); }; TriggerConfig tc; std::string channel; { std::lock_guard lock(app.model->mutex()); tc = app.model->trigger(); const std::vector& channels = app.model->channels(); if (tc.channel >= 0 && tc.channel < static_cast(channels.size())) { channel = channels[static_cast(tc.channel)].name; } } // With trigger mode off the scope is streaming, and that is the whole of what this pane has to say: // the four lines below describe a trigger that is not running. if (!tc.enabled) { put("mode", "stream (trigger off)"); for (int i = row; i < App::kTriggerSummaryRows; ++i) { gtk_widget_set_visible(app.trigger_summary_keys[i], FALSE); gtk_widget_set_visible(app.trigger_summary_values[i], FALSE); } app.dbg_trigger_summary = reading; gtk_widget_set_visible(app.trigger_summary, app.trigger_locked); return; } put("signal", channel.empty() ? std::string("none acquired") : channel); // The condition: the shape, and whatever qualifies it. A pulse width carries its comparison and its // width, which is what the shape *is*; a window carries its two levels on the line below. std::string condition = trigger_type_text(tc.condition.type); switch (tc.condition.type) { case TriggerType::Edge: case TriggerType::Window: condition += std::string(", ") + trigger_edge_text(tc.condition.edge); break; case TriggerType::PulseWidth: condition += std::string(", ") + width_cmp_text(tc.condition.cmp) + " " + ms(tc.condition.width_s); if (tc.condition.cmp == WidthCmp::Within || tc.condition.cmp == WidthCmp::Outside) { condition += " .. " + ms(tc.condition.width2_s); } break; case TriggerType::Dropout: condition += std::string(", ") + ms(tc.condition.timeout_s); break; } put("condition", condition); switch (tc.condition.type) { case TriggerType::Window: put("level", number(tc.condition.level) + " .. " + number(tc.condition.level2)); break; case TriggerType::Dropout: put("timeout", ms(tc.condition.timeout_s)); break; default: put("level", number(tc.condition.level)); break; } put("mode", trigger_mode_text(tc.mode)); // Everything else only when it is not at its default: a line that says "band 0" is a line that says // nothing, and the four lines above are the ones a user reads. if (tc.condition.noise_band > 0.0 && tc.condition.type != TriggerType::Dropout) { put("noise band", number(tc.condition.noise_band)); } if (tc.condition.hf_fc > 0.0) put("HF reject", number(tc.condition.hf_fc) + " Hz"); if (std::fabs(tc.pretrigger_pct - 50.0) > 1.0e-9) put("pre-trigger", number(tc.pretrigger_pct) + " %"); if (std::fabs(tc.holdoff_s - 0.010) > 1.0e-12) put("hold-off", ms(tc.holdoff_s)); for (int i = row; i < App::kTriggerSummaryRows; ++i) { gtk_widget_set_visible(app.trigger_summary_keys[i], FALSE); gtk_widget_set_visible(app.trigger_summary_values[i], FALSE); } app.dbg_trigger_summary = reading; gtk_widget_set_visible(app.trigger_summary, app.trigger_locked); } 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 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(user)); } /// The same publish, for the two dropdowns. It needs its own entry point because /// `notify::` 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(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(user); publish_trigger_fields(app); sync_trigger_rows(*app); } void on_trigger_signal_changed(GObject* obj, GParamSpec*, gpointer user) { App* app = static_cast(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 lock(app->model->mutex()); if (sel >= app->model->channels().size()) return; app->model->trigger().channel = static_cast(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(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 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(signal), true); } // The panel's body stays *live* whether or not the trigger is running: a trigger is configured // before it is switched on, and switching it off to edit it -- or to check what it is set to -- // is the ordinary way to work. (It used to go insensitive with the mode, which left the settings // unreachable exactly when a user wanted to change them.) What that costs is the reading: the // `trigger panel sensitive` line is now a regression guard, and a `0` there means the // configuration has been locked out again. // 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"); // Arming is a *runtime* control: with trigger mode off there is no engine to arm, and the button is the // one part of this pane that is not a setting. (The body itself stays live -- see above.) if (app->arm_button != nullptr) gtk_widget_set_sensitive(app->arm_button, on); // The pane's own switch shows the same state, whichever of the two was clicked. sync_panel_switches(*app); } /// 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. /// 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); // The *body* is what trigger-off disables; this panel's index into it is `trigger_body`. app.trigger_body = box; 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[] = {trigger_type_text(TriggerType::Edge), trigger_type_text(TriggerType::PulseWidth), trigger_type_text(TriggerType::Window), trigger_type_text(TriggerType::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[] = {trigger_edge_text(TriggerEdge::Rising), trigger_edge_text(TriggerEdge::Falling), trigger_edge_text(TriggerEdge::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[] = {width_cmp_text(WidthCmp::Less), width_cmp_text(WidthCmp::Greater), width_cmp_text(WidthCmp::Within), width_cmp_text(WidthCmp::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(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[] = {trigger_mode_text(TriggerMode::Normal), trigger_mode_text(TriggerMode::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); } 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(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 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(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(user); if (a->search.prev()) { search_jump(*a, a->search.hits()[static_cast(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(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(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(u)); }), &app); } sync_trigger_rows(app); // The heading row, and the switch that is the *same* control as the header bar's button. Built here // rather than in the file because it must not go insensitive with the body: turning trigger mode // back on would otherwise mean finding the toolbar button that greyed the panel out. GtkWidget* head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8); gtk_widget_set_margin_top(head, 8); gtk_widget_set_margin_start(head, 8); gtk_widget_set_margin_end(head, 8); GtkWidget* title = gtk_label_new("Trigger"); gtk_widget_set_halign(title, GTK_ALIGN_START); gtk_widget_set_hexpand(title, TRUE); gtk_box_append(GTK_BOX(head), title); // What the pane is set to, for the mode where its settings are not on screen. app.trigger_summary = gtk_label_new(""); gtk_widget_add_css_class(app.trigger_summary, "dim-label"); gtk_label_set_ellipsize(GTK_LABEL(app.trigger_summary), PANGO_ELLIPSIZE_END); gtk_widget_set_halign(app.trigger_summary, GTK_ALIGN_START); gtk_widget_set_hexpand(app.trigger_summary, TRUE); gtk_widget_set_visible(app.trigger_summary, FALSE); gtk_box_append(GTK_BOX(head), app.trigger_summary); app.trigger_edit = gtk_button_new_from_icon_name("document-edit-symbolic"); gtk_widget_add_css_class(app.trigger_edit, "flat"); gtk_widget_set_valign(app.trigger_edit, GTK_ALIGN_CENTER); gtk_widget_set_tooltip_text(app.trigger_edit, "configure the trigger"); g_signal_connect(app.trigger_edit, "clicked", G_CALLBACK(on_pane_edit_clicked), &app); gtk_box_append(GTK_BOX(head), app.trigger_edit); app.trigger_switch = gtk_switch_new(); gtk_widget_set_valign(app.trigger_switch, GTK_ALIGN_CENTER); gtk_widget_set_tooltip_text(app.trigger_switch, "trigger mode, the same switch the header bar carries"); g_signal_connect(app.trigger_switch, "notify::active", G_CALLBACK(on_panel_switch_toggled), &app); gtk_box_append(GTK_BOX(head), app.trigger_switch); // The locked pane's content: the settings as a definition list, so the answer to "what is this set to" // is four short lines rather than a row of controls. app.trigger_summary = gtk_grid_new(); gtk_grid_set_row_spacing(GTK_GRID(app.trigger_summary), 2); gtk_grid_set_column_spacing(GTK_GRID(app.trigger_summary), 12); gtk_widget_set_margin_top(app.trigger_summary, 6); gtk_widget_set_margin_bottom(app.trigger_summary, 6); gtk_widget_set_margin_start(app.trigger_summary, 8); gtk_widget_set_margin_end(app.trigger_summary, 8); for (int i = 0; i < App::kTriggerSummaryRows; ++i) { app.trigger_summary_keys[i] = gtk_label_new(""); gtk_widget_add_css_class(app.trigger_summary_keys[i], "dim-label"); gtk_widget_set_halign(app.trigger_summary_keys[i], GTK_ALIGN_START); app.trigger_summary_values[i] = gtk_label_new(""); gtk_widget_set_halign(app.trigger_summary_values[i], GTK_ALIGN_START); gtk_label_set_ellipsize(GTK_LABEL(app.trigger_summary_values[i]), PANGO_ELLIPSIZE_END); gtk_grid_attach(GTK_GRID(app.trigger_summary), app.trigger_summary_keys[i], 0, i, 1, 1); gtk_grid_attach(GTK_GRID(app.trigger_summary), app.trigger_summary_values[i], 1, i, 1, 1); } gtk_widget_set_visible(app.trigger_summary, FALSE); GtkWidget* outer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); gtk_box_append(GTK_BOX(outer), head); gtk_box_append(GTK_BOX(outer), app.trigger_summary); gtk_box_append(GTK_BOX(outer), box); return outer; } } // namespace ui_impl } // namespace udpscope