ui: compact events block in the dock

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Sonnet 4.6 committed 2026-11-03 06:48:35 +01:00
1 parent b08494dd36
commit baf1c73b21
5 files changed
+287 -5

No files matched your search

+163
View File
@@ -8,13 +8,20 @@
// 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.
//
// Below them, the Events block: one compact row per cached event (marker, title, time; no thumbnails),
// a Manage button for the full list, and one click on a row to replay it. Its rows are built from the
// same `event_store->list()` and the same `event_row_title()` as the events pane, so the two cannot
// disagree.
#include "ui_app.hpp"
#include <gtk/gtk.h>
#include <cstring>
#include <ctime>
#include <string>
#include <vector>
namespace udpscope {
namespace ui_impl {
@@ -226,5 +233,161 @@ void dock_measure_value(App& app, std::size_t i, const std::string& text) {
set_value_text(app.dock_measure_labels[i], text);
}
namespace {
/// The widths of an event row's three cells, in characters. Fixed, so the Events block is as wide with
/// one event as with thirty (the marker is one cell wide even when it is empty).
constexpr int kEventTitleChars = 18;
constexpr int kEventTimeChars = 8;
/// The one-character mark of a row: filled for the event on screen, hollow for a member of the group on
/// screen, blank for the rest. Both questions are the ones `event_row_title()` asks (`ui_events.cpp`).
const char* dock_event_marker(const App& app, const EventRecord& r) {
if (event_on_screen(app, r)) return "\xE2\x97\x8F"; // U+25CF black circle
if (event_in_group(app, r)) return "\xE2\x97\x8B"; // U+25CB white circle
return " ";
}
void on_manage_events(GtkButton*, gpointer user) {
slideover_open(*static_cast<App*>(user), "events");
}
/// One click on a row replays it, as the pane's activation does: `replay_event()` and then the in-place
/// re-mark. The record is copied first, because nothing it triggers may be allowed to free the row it
/// was read from.
void on_dock_event_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) {
App& app = *static_cast<App*>(user);
if (row == nullptr) return;
const auto* record =
static_cast<const EventRecord*>(g_object_get_data(G_OBJECT(row), "udpscope-event"));
if (record == nullptr) return;
const EventRecord copy = *record;
replay_event(app, copy);
mark_events_on_screen(app);
}
std::string event_clock(const EventRecord& r) {
const std::time_t t = static_cast<std::time_t>(r.wall_time);
std::tm tm{};
localtime_r(&t, &tm);
char clock[32];
std::strftime(clock, sizeof(clock), "%H:%M:%S", &tm);
return clock;
}
} // namespace
GtkWidget* build_events_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* head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
GtkWidget* title = gtk_label_new("Events");
gtk_widget_set_halign(title, GTK_ALIGN_START);
gtk_widget_set_hexpand(title, true);
gtk_widget_add_css_class(title, "heading");
gtk_box_append(GTK_BOX(head), title);
GtkWidget* manage = gtk_button_new_with_label("Manage");
gtk_widget_add_css_class(manage, "flat");
gtk_widget_set_tooltip_text(manage, "the events' full list and actions");
g_signal_connect(manage, "clicked", G_CALLBACK(on_manage_events), &app);
gtk_box_append(GTK_BOX(head), manage);
gtk_box_append(GTK_BOX(box), head);
GtkWidget* list = gtk_list_box_new();
gtk_list_box_set_selection_mode(GTK_LIST_BOX(list), GTK_SELECTION_NONE);
gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(list), true);
gtk_widget_add_css_class(list, "boxed-list");
g_signal_connect(list, "row-activated", G_CALLBACK(on_dock_event_activated), &app);
// A fixed height (about four rows), so the block is as tall with no events as with thirty and the
// readouts above it do not move when one is latched.
GtkWidget* scroller = gtk_scrolled_window_new();
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroller), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
gtk_scrolled_window_set_min_content_height(GTK_SCROLLED_WINDOW(scroller), 112);
gtk_scrolled_window_set_max_content_height(GTK_SCROLLED_WINDOW(scroller), 112);
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroller), list);
gtk_box_append(GTK_BOX(box), scroller);
app.dock_events_box = box;
app.dock_events_list = list;
app.dock_events_manage = manage;
// Insensitive until the first availability sync says otherwise: on screen and greyed, never hidden.
gtk_widget_set_sensitive(box, FALSE);
dock_rebuild_events(app);
return box;
}
void dock_rebuild_events(App& app) {
if (app.dock_events_list == nullptr || app.event_store == nullptr) return;
GtkListBox* list = GTK_LIST_BOX(app.dock_events_list);
gtk_list_box_remove_all(list);
const std::vector<EventRecord> records = app.event_store->list();
if (records.empty()) {
GtkWidget* none = gtk_label_new("no events yet");
gtk_widget_set_halign(none, GTK_ALIGN_START);
gtk_widget_add_css_class(none, "caption");
gtk_widget_add_css_class(none, "dim-label");
gtk_widget_set_margin_start(none, 6);
gtk_widget_set_margin_top(none, 4);
gtk_widget_set_margin_bottom(none, 4);
GtkWidget* row = gtk_list_box_row_new();
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), none);
gtk_list_box_row_set_activatable(GTK_LIST_BOX_ROW(row), false);
gtk_list_box_append(list, row);
return;
}
for (const EventRecord& r : records) {
GtkWidget* row = gtk_list_box_row_new();
GtkWidget* line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
gtk_widget_set_margin_start(line, 6);
gtk_widget_set_margin_end(line, 6);
gtk_widget_set_margin_top(line, 3);
gtk_widget_set_margin_bottom(line, 3);
GtkWidget* marker = fixed_label(dock_event_marker(app, r), 1, GTK_ALIGN_CENTER);
gtk_box_append(GTK_BOX(line), marker);
const std::string text = event_row_title(app, r);
GtkWidget* name = fixed_label(text.c_str(), kEventTitleChars, GTK_ALIGN_START);
gtk_widget_set_hexpand(name, true);
gtk_widget_set_tooltip_text(name, text.c_str());
gtk_box_append(GTK_BOX(line), name);
const std::string clock = event_clock(r);
GtkWidget* when = fixed_label(clock.c_str(), kEventTimeChars, GTK_ALIGN_END);
gtk_widget_add_css_class(when, "dim-label");
gtk_box_append(GTK_BOX(line), when);
g_object_set_data(G_OBJECT(row), "udpscope-event-marker", marker);
g_object_set_data(G_OBJECT(row), "udpscope-event-title", name);
g_object_set_data_full(G_OBJECT(row), "udpscope-event", new EventRecord(r),
[](gpointer p) { delete static_cast<EventRecord*>(p); });
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), line);
gtk_list_box_append(list, row);
}
}
void dock_mark_events(App& app) {
if (app.dock_events_list == nullptr) return;
for (GtkWidget* row = gtk_widget_get_first_child(app.dock_events_list); row != nullptr;
row = gtk_widget_get_next_sibling(row)) {
const auto* record =
static_cast<const EventRecord*>(g_object_get_data(G_OBJECT(row), "udpscope-event"));
GtkWidget* marker = GTK_WIDGET(g_object_get_data(G_OBJECT(row), "udpscope-event-marker"));
GtkWidget* name = GTK_WIDGET(g_object_get_data(G_OBJECT(row), "udpscope-event-title"));
// The "no events yet" placeholder carries none of the three: skipped.
if (record == nullptr || marker == nullptr || name == nullptr) continue;
gtk_label_set_text(GTK_LABEL(marker), dock_event_marker(app, *record));
set_value_text(name, event_row_title(app, *record));
}
}
void dock_events_set_sensitive(App& app, bool available) {
if (app.dock_events_box == nullptr) return;
gtk_widget_set_sensitive(app.dock_events_box, available);
}
} // namespace ui_impl
} // namespace udpscope