// SPDX-License-Identifier: MIT // SPDX-FileCopyrightText: 2026 Martino Ferrari // // The cursors pane: what the two vertical markers and the per-row rulers read, in the four cursor\n// modes, rebuilt only when the rows or the mode change. #include "ui_app.hpp" #include #include namespace udpscope { namespace ui_impl { /// `%.6g s` is exact enough for a cursor readout and short enough for the panel. void update_cursors_pane(App& app) { if (app.cursors_body == nullptr) return; if (!app.rows->cursors_visible()) { // A readout quotes a stored sample; with the instruments off there is no sample it quotes, so // the cells blank rather than keeping the last ones. `"--"` is the same spelling an // unavailable cell already uses -- two spellings of one state in one grid would be the defect. for (const App::CursorLine& line : app.cursors_lines) { if (line.a != nullptr) gtk_label_set_text(GTK_LABEL(line.a), "--"); if (line.b != nullptr) gtk_label_set_text(GTK_LABEL(line.b), "--"); if (line.d != nullptr) gtk_label_set_text(GTK_LABEL(line.d), "--"); } return; } if (app.cursors_pane_mode != static_cast(app.rows->cursor_mode()) || app.cursors_pane_rows != app.rows->row_count() || app.cursors_pane_generation != app.model->generation() || app.cursors_pane_bound != bound_slot_mask(app)) { rebuild_cursors_pane(app); } const double t0 = app.rows->cursor_a_x(); const double t1 = app.rows->cursor_b_x(); const auto put = [](GtkWidget* w, const std::string& text) { if (w != nullptr) gtk_label_set_text(GTK_LABEL(w), text.c_str()); }; const RowSet::CursorMode mode = app.rows->cursor_mode(); const bool rulers = mode == RowSet::CursorMode::Horizontal || mode == RowSet::CursorMode::Both; const bool crossings = mode == RowSet::CursorMode::Mix; for (const App::CursorLine& line : app.cursors_lines) { if (line.a == nullptr) continue; // One measurement per cell, each with the unit its own axis declares: the pane is a // readout beside a plot, where the values are the point. std::string va; std::string vb; std::string vd; if (line.slot == -2) { va = format_measure(t0, "s"); vb = format_measure(t1, "s"); vd = format_measure(t1 - t0, "s"); } else if (line.slot == -1 && rulers) { const std::string unit = row_unit(app, line.row); const double y1 = app.rows->ruler_y(line.row); const double y2 = app.rows->ruler_b_y(line.row); va = format_measure(y1, unit); vb = format_measure(y2, unit); vd = format_measure(y2 - y1, unit); } else if (crossings && line.slot >= 0) { // Where each cursor crosses this trace: the value the trace has at that time, // read the way a crosshair reads it. // The same call the crossing markers use, on the same trace: the pane and the // marks on the plot cannot disagree about what the value is. const RowSet::Row& row = app.rows->row(line.row); double y1 = 0.0; double y2 = 0.0; const bool a = row.plot != nullptr && row.plot->crosshair_value(static_cast(line.slot), t0, y1); const bool b = row.plot != nullptr && row.plot->crosshair_value(static_cast(line.slot), t1, y2); if (a && b) { const std::string unit = slot_unit(app, line.row, line.slot); va = format_measure(y1, unit); vb = format_measure(y2, unit); vd = format_measure(y2 - y1, unit); } else { va = vb = vd = a || b ? "partial" : "--"; } } else { va = vb = vd = "--"; } put(line.a, va); put(line.b, vb); put(line.d, vd); } } GtkWidget* build_cursors_panel(App& app) { // The panel's shape is a UI file; the *contents* of its grid are not, because they follow the cursor // mode and the rows and are rebuilt whenever either changes. GtkBuilder* ui = ui_load(app, "/org/udpscope/ui/cursors-panel.ui"); GtkWidget* box = GTK_WIDGET(gtk_builder_get_object(ui, "cursors_panel")); app.cursors_mode_drop = GTK_WIDGET(gtk_builder_get_object(ui, "mode_drop")); app.cursors_reset = GTK_WIDGET(gtk_builder_get_object(ui, "reset")); app.cursors_body = GTK_WIDGET(gtk_builder_get_object(ui, "body")); app.cursors_switch = GTK_WIDGET(gtk_builder_get_object(ui, "cursors_switch")); if (app.cursors_switch != nullptr) { gtk_widget_set_tooltip_text(app.cursors_switch, "the cursors, the same switch the header bar carries"); g_signal_connect(app.cursors_switch, "notify::active", G_CALLBACK(on_panel_switch_toggled), &app); } if (box == nullptr || app.cursors_mode_drop == nullptr || app.cursors_reset == nullptr || app.cursors_body == nullptr) { // A missing id is a mistake in the `.ui`. The panel comes up empty rather than taking the window // with it, and `[cursors]` reads differently rather than the application failing to start. return box != nullptr ? box : gtk_box_new(GTK_ORIENTATION_VERTICAL, 0); } // The panel's heading row -- its first child, which carries the title, the mode dropdown, Reset and // the pane's switch. It is where the contextual help's "?" joins (see `ui_context_help.cpp`). app.cursors_head = gtk_widget_get_first_child(box); // The mode names are the dropdown's model, and they are a C array, so they are set here. const char* names[5] = {kCursorModeNames[0], kCursorModeNames[1], kCursorModeNames[2], kCursorModeNames[3], nullptr}; gtk_drop_down_set_model(GTK_DROP_DOWN(app.cursors_mode_drop), G_LIST_MODEL(gtk_string_list_new(names))); gtk_widget_set_tooltip_text(app.cursors_mode_drop, "vertical: two times, shared by every row\n" "horizontal: one value per row, at the two times\n" "both: the two times and one value per row\n" "mix: the two times, read off each trace where they cross"); g_signal_connect(app.cursors_mode_drop, "notify::selected", G_CALLBACK(on_cursor_mode_changed), &app); // Reset lives beside the mode it resets rather than in the overflow menu: it acts on this pane's // instruments and is only visible while they are on, both of which are this pane's facts. gtk_widget_set_tooltip_text(app.cursors_reset, "re-place the cursors at the window's edges"); g_signal_connect(app.cursors_reset, "clicked", G_CALLBACK(on_cursors_reset), &app); return box; } void on_cursors_toggled(GtkToggleButton* button, gpointer user) { App* app = static_cast(user); const bool on = gtk_toggle_button_get_active(button); app->rows->set_cursors_visible(on); // Disabled, not hidden, and the same for the reset beside it: with the cursors off there is // nothing to reset, but the button must stay where the user will look for it. The *body* goes // insensitive and not the panel, because the panel's header carries the switch that turns the // cursors back on -- and a control inside an insensitive parent cannot be clicked. gtk_widget_set_sensitive(app->cursors_reset, on); if (app->cursors_body != nullptr) gtk_widget_set_sensitive(app->cursors_body, on); if (!on) update_cursors_pane(*app); if (on) { app->rows->reset_cursors(); rebuild_cursors_pane(*app); update_cursors_pane(*app); } // The pane's own switch shows the same state, whichever of the two was clicked. sync_panel_switches(*app); } void on_cursors_reset(GtkButton*, gpointer user) { App* app = static_cast(user); // A reset the user cannot see reads as a dead button, so it also switches the // cursors on. if (!gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app->cursors_toggle))) { gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app->cursors_toggle), true); } app->rows->reset_cursors(); } } // namespace ui_impl } // namespace udpscope