ui: compact cursor and measurement readouts at the top of the dock
Both views are written from the same variable at the same call site, so the dock cannot disagree with the full panels. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
a7a1609d48
commit
a0fc0762d5
7 files changed
+350
-22
No files matched your search
+229
@@ -0,0 +1,229 @@
|
||||
// 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`.
|
||||
constexpr int kValueChars = 9;
|
||||
constexpr int kNameChars = 8;
|
||||
|
||||
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
|
||||
Reference in new issue
Block a user