Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
231 lines
9.5 KiB
C++
231 lines
9.5 KiB
C++
// SPDX-License-Identifier: MIT
|
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
|
|
|
// The compact readouts at the top of the dock: a read-only copy of the cursors table and of the
|
|
// measurement values, in a fixed width, with an Edit button for each that opens the full panel.
|
|
//
|
|
// The strings are not formatted here. `update_cursors_pane()` and `update_measures()` format each value
|
|
// once and hand the same variable to the page's label and to `dock_cursor_cell()` / `dock_measure_value()`,
|
|
// so the two views cannot disagree. Every label is ellipsized at a fixed `width-chars`, so the dock's
|
|
// layout does not depend on what a value reads; the full text is the label's tooltip.
|
|
|
|
#include "ui_app.hpp"
|
|
|
|
#include <gtk/gtk.h>
|
|
|
|
#include <cstring>
|
|
#include <string>
|
|
|
|
namespace udpscope {
|
|
namespace ui_impl {
|
|
|
|
namespace {
|
|
|
|
/// The width, in characters, of a value cell and of a `who` / type cell. Fixed, so the dock is as wide
|
|
/// with `--` as with `-123.456 mV`; their sum (5 + 3 x 9 characters) is what keeps the readouts inside the
|
|
/// dock's 280 px.
|
|
constexpr int kValueChars = 9;
|
|
constexpr int kNameChars = 5;
|
|
|
|
GtkWidget* fixed_label(const char* text, int chars, GtkAlign halign) {
|
|
GtkWidget* l = gtk_label_new(text);
|
|
gtk_widget_set_halign(l, halign);
|
|
gtk_label_set_ellipsize(GTK_LABEL(l), PANGO_ELLIPSIZE_END);
|
|
gtk_label_set_width_chars(GTK_LABEL(l), chars);
|
|
gtk_label_set_max_width_chars(GTK_LABEL(l), chars);
|
|
gtk_widget_add_css_class(l, "caption");
|
|
return l;
|
|
}
|
|
|
|
/// Sets a value label's text and, with it, the tooltip that carries what the ellipsis cut. The text is
|
|
/// compared first so the per-frame write of an unchanged value costs nothing.
|
|
void set_value_text(GtkWidget* label, const std::string& text) {
|
|
if (label == nullptr) return;
|
|
if (std::strcmp(gtk_label_get_text(GTK_LABEL(label)), text.c_str()) == 0) return;
|
|
gtk_label_set_text(GTK_LABEL(label), text.c_str());
|
|
gtk_widget_set_tooltip_text(label, text.c_str());
|
|
}
|
|
|
|
void on_edit_cursors(GtkButton*, gpointer user) {
|
|
slideover_open(*static_cast<App*>(user), "cursors");
|
|
}
|
|
|
|
void on_edit_measures(GtkButton*, gpointer user) {
|
|
slideover_open(*static_cast<App*>(user), "measure");
|
|
}
|
|
|
|
/// A click on a measurement's line opens that measurement's dialog, as the page row's `edit` button
|
|
/// does. The line carries its index (plus one, so that zero is "none") in its data.
|
|
void on_measure_line_pressed(GtkGestureClick* gesture, int n_press, double, double, gpointer user) {
|
|
if (n_press != 1) return;
|
|
GtkWidget* line = gtk_event_controller_get_widget(GTK_EVENT_CONTROLLER(gesture));
|
|
const int idx = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(line), "udpscope-measure-index")) - 1;
|
|
if (idx < 0) return;
|
|
open_measure_dialog(*static_cast<App*>(user), idx);
|
|
}
|
|
|
|
/// A section's heading row: its title at the left, its Edit button at the right.
|
|
GtkWidget* section_head(const char* title, GtkWidget** edit_out, GCallback on_edit, App* app,
|
|
const char* tip) {
|
|
GtkWidget* row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
|
GtkWidget* t = gtk_label_new(title);
|
|
gtk_widget_set_halign(t, GTK_ALIGN_START);
|
|
gtk_widget_set_hexpand(t, true);
|
|
gtk_widget_add_css_class(t, "caption-heading");
|
|
gtk_box_append(GTK_BOX(row), t);
|
|
GtkWidget* edit = gtk_button_new_with_label("Edit");
|
|
gtk_widget_add_css_class(edit, "flat");
|
|
gtk_widget_set_tooltip_text(edit, tip);
|
|
g_signal_connect(edit, "clicked", on_edit, app);
|
|
gtk_box_append(GTK_BOX(row), edit);
|
|
*edit_out = edit;
|
|
return row;
|
|
}
|
|
|
|
/// The same header `cursors_header()` writes into the page's table, small.
|
|
void dock_cursors_header(GtkGrid* grid) {
|
|
const char* texts[4] = {"", "A", "B", "delta"};
|
|
for (int c = 0; c < 4; ++c) {
|
|
GtkWidget* l = fixed_label(texts[c], c == 0 ? kNameChars : kValueChars,
|
|
c == 0 ? GTK_ALIGN_START : GTK_ALIGN_END);
|
|
gtk_widget_add_css_class(l, "dim-label");
|
|
gtk_grid_attach(grid, l, c, 0, 1, 1);
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
GtkWidget* build_readouts_dock(App& app) {
|
|
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
|
gtk_widget_set_margin_top(box, 6);
|
|
gtk_widget_set_margin_bottom(box, 6);
|
|
gtk_widget_set_margin_start(box, 6);
|
|
gtk_widget_set_margin_end(box, 6);
|
|
|
|
GtkWidget* heading = gtk_label_new("Readouts");
|
|
gtk_widget_set_halign(heading, GTK_ALIGN_START);
|
|
gtk_widget_add_css_class(heading, "heading");
|
|
gtk_box_append(GTK_BOX(box), heading);
|
|
|
|
gtk_box_append(GTK_BOX(box), section_head("Cursors", &app.dock_cursors_edit,
|
|
G_CALLBACK(on_edit_cursors), &app,
|
|
"the cursors' full panel"));
|
|
app.dock_cursor_grid = gtk_grid_new();
|
|
gtk_grid_set_row_spacing(GTK_GRID(app.dock_cursor_grid), 2);
|
|
gtk_grid_set_column_spacing(GTK_GRID(app.dock_cursor_grid), 4);
|
|
gtk_box_append(GTK_BOX(box), app.dock_cursor_grid);
|
|
|
|
gtk_box_append(GTK_BOX(box), section_head("Measurements", &app.dock_measures_edit,
|
|
G_CALLBACK(on_edit_measures), &app,
|
|
"the measurements' full panel"));
|
|
app.dock_measures_box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
|
|
gtk_box_append(GTK_BOX(box), app.dock_measures_box);
|
|
|
|
app.dock_readouts = box;
|
|
// The tables are built from the pages' current lines, which exist already when the pages were built
|
|
// first; otherwise the pages' own rebuild fills them.
|
|
dock_rebuild_cursors(app);
|
|
dock_rebuild_measures(app);
|
|
return box;
|
|
}
|
|
|
|
void dock_rebuild_cursors(App& app) {
|
|
if (app.dock_cursor_grid == nullptr) return;
|
|
GtkGrid* grid = GTK_GRID(app.dock_cursor_grid);
|
|
GtkWidget* child = gtk_widget_get_first_child(app.dock_cursor_grid);
|
|
while (child != nullptr) {
|
|
GtkWidget* next = gtk_widget_get_next_sibling(child);
|
|
gtk_grid_remove(grid, child);
|
|
child = next;
|
|
}
|
|
app.dock_cursor_lines.clear();
|
|
dock_cursors_header(grid);
|
|
int at = 1;
|
|
for (const App::CursorLine& src : app.cursors_lines) {
|
|
App::DockCursorLine line;
|
|
line.label = fixed_label("", kNameChars, GTK_ALIGN_START);
|
|
if (src.label != nullptr) {
|
|
// The page's `who` cell, verbatim: a chip and a name as markup, or plain text.
|
|
const char* text = gtk_label_get_label(GTK_LABEL(src.label));
|
|
if (gtk_label_get_use_markup(GTK_LABEL(src.label))) {
|
|
gtk_label_set_markup(GTK_LABEL(line.label), text);
|
|
} else {
|
|
gtk_label_set_text(GTK_LABEL(line.label), text);
|
|
}
|
|
const char* tip = gtk_widget_get_tooltip_text(src.label);
|
|
gtk_widget_set_tooltip_text(line.label, tip != nullptr ? tip : gtk_label_get_text(GTK_LABEL(src.label)));
|
|
}
|
|
gtk_grid_attach(grid, line.label, 0, at, 1, 1);
|
|
GtkWidget** cells[3] = {&line.a, &line.b, &line.d};
|
|
for (int c = 0; c < 3; ++c) {
|
|
GtkWidget* v = fixed_label("", kValueChars, GTK_ALIGN_END);
|
|
gtk_widget_add_css_class(v, "numeric");
|
|
gtk_label_set_selectable(GTK_LABEL(v), true);
|
|
gtk_grid_attach(grid, v, c + 1, at, 1, 1);
|
|
*cells[c] = v;
|
|
}
|
|
app.dock_cursor_lines.push_back(line);
|
|
++at;
|
|
}
|
|
}
|
|
|
|
void dock_rebuild_measures(App& app) {
|
|
if (app.dock_measures_box == nullptr) return;
|
|
GtkWidget* child = gtk_widget_get_first_child(app.dock_measures_box);
|
|
while (child != nullptr) {
|
|
GtkWidget* next = gtk_widget_get_next_sibling(child);
|
|
gtk_box_remove(GTK_BOX(app.dock_measures_box), child);
|
|
child = next;
|
|
}
|
|
app.dock_measure_labels.clear();
|
|
if (app.measures.empty()) {
|
|
GtkWidget* none = gtk_label_new("no measurements");
|
|
gtk_widget_set_halign(none, GTK_ALIGN_START);
|
|
gtk_widget_add_css_class(none, "caption");
|
|
gtk_widget_add_css_class(none, "dim-label");
|
|
gtk_box_append(GTK_BOX(app.dock_measures_box), none);
|
|
return;
|
|
}
|
|
for (std::size_t i = 0; i < app.measures.size(); ++i) {
|
|
const Measure& m = app.measures[i];
|
|
GtkWidget* line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
|
g_object_set_data(G_OBJECT(line), "udpscope-measure-index",
|
|
GINT_TO_POINTER(static_cast<int>(i) + 1));
|
|
GtkWidget* chip = gtk_label_new("");
|
|
gtk_label_set_markup(GTK_LABEL(chip), measure_chip_markup(app, m.signal).c_str());
|
|
gtk_widget_set_tooltip_text(chip, m.signal.c_str());
|
|
gtk_box_append(GTK_BOX(line), chip);
|
|
GtkWidget* kind = fixed_label(measure_type_name(m.type), 12, GTK_ALIGN_START);
|
|
gtk_widget_set_hexpand(kind, true);
|
|
gtk_widget_set_tooltip_text(kind, (m.signal + "\n" + measure_type_name(m.type)).c_str());
|
|
gtk_box_append(GTK_BOX(line), kind);
|
|
GtkWidget* value = fixed_label("--", kValueChars, GTK_ALIGN_END);
|
|
gtk_widget_add_css_class(value, "numeric");
|
|
gtk_label_set_selectable(GTK_LABEL(value), true);
|
|
gtk_box_append(GTK_BOX(line), value);
|
|
// On the line and not on the value label, which is selectable and so keeps its own clicks.
|
|
GtkGesture* click = gtk_gesture_click_new();
|
|
g_signal_connect(click, "pressed", G_CALLBACK(on_measure_line_pressed), &app);
|
|
gtk_widget_add_controller(line, GTK_EVENT_CONTROLLER(click));
|
|
gtk_box_append(GTK_BOX(app.dock_measures_box), line);
|
|
app.dock_measure_labels.push_back(value);
|
|
}
|
|
}
|
|
|
|
void dock_cursor_cell(App& app, std::size_t line, const std::string& a, const std::string& b,
|
|
const std::string& d) {
|
|
if (line >= app.dock_cursor_lines.size()) return; // mid-rebuild
|
|
const App::DockCursorLine& l = app.dock_cursor_lines[line];
|
|
set_value_text(l.a, a);
|
|
set_value_text(l.b, b);
|
|
set_value_text(l.d, d);
|
|
}
|
|
|
|
void dock_measure_value(App& app, std::size_t i, const std::string& text) {
|
|
if (i >= app.dock_measure_labels.size()) return; // mid-rebuild
|
|
set_value_text(app.dock_measure_labels[i], text);
|
|
}
|
|
|
|
} // namespace ui_impl
|
|
} // namespace udpscope
|