ui: cap and cheapen the dock's events rebuild
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
baf1c73b21
commit
cb098eff75
4 files changed
+133
-34
No files matched your search
+74
-16
@@ -18,6 +18,7 @@
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
@@ -238,7 +239,7 @@ 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;
|
||||
constexpr int kEventSeqChars = 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`).
|
||||
@@ -255,6 +256,11 @@ void on_manage_events(GtkButton*, gpointer user) {
|
||||
/// 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.
|
||||
///
|
||||
/// Safety depends on `replay_event()` never rebuilding the dock's list (it must go through
|
||||
/// `dock_mark_events()`, which rewrites labels in place): a rebuild here would free this very row, and
|
||||
/// `row`, `record` and the list box's own emission of `row-activated` would all be dangling. The copy
|
||||
/// guards the record only, not the row. If a replay ever has to refresh the cache's view, defer it.
|
||||
void on_dock_event_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) {
|
||||
App& app = *static_cast<App*>(user);
|
||||
if (row == nullptr) return;
|
||||
@@ -266,15 +272,6 @@ void on_dock_event_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) {
|
||||
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) {
|
||||
@@ -323,9 +320,47 @@ GtkWidget* build_events_dock(App& app) {
|
||||
|
||||
void dock_rebuild_events(App& app) {
|
||||
if (app.dock_events_list == nullptr || app.event_store == nullptr) return;
|
||||
dock_rebuild_events(app, app.event_store->list());
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/// Puts the list's vertical scroll back after a refill. The adjustment's bounds are only updated at the
|
||||
/// next allocation, so the value set at once is clamped against the old ones; an idle (which runs after the
|
||||
/// frame that laid the new rows out) sets it again against the new bounds, where the clamp is exact.
|
||||
void restore_dock_scroll(GtkWidget* list, double value) {
|
||||
GtkWidget* scroller = gtk_widget_get_ancestor(list, GTK_TYPE_SCROLLED_WINDOW);
|
||||
if (scroller == nullptr) return;
|
||||
GtkAdjustment* adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroller));
|
||||
gtk_adjustment_set_value(adj, value);
|
||||
struct Pending {
|
||||
GtkAdjustment* adj;
|
||||
double value;
|
||||
};
|
||||
auto* p = new Pending{GTK_ADJUSTMENT(g_object_ref(adj)), value};
|
||||
g_idle_add_full(
|
||||
G_PRIORITY_DEFAULT_IDLE,
|
||||
[](gpointer d) -> gboolean {
|
||||
auto* q = static_cast<Pending*>(d);
|
||||
gtk_adjustment_set_value(q->adj, q->value);
|
||||
g_object_unref(q->adj);
|
||||
delete q;
|
||||
return G_SOURCE_REMOVE;
|
||||
},
|
||||
p, nullptr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void dock_rebuild_events(App& app, const std::vector<EventRecord>& records) {
|
||||
if (app.dock_events_list == nullptr) return;
|
||||
GtkListBox* list = GTK_LIST_BOX(app.dock_events_list);
|
||||
GtkWidget* scroller = gtk_widget_get_ancestor(app.dock_events_list, GTK_TYPE_SCROLLED_WINDOW);
|
||||
const double scroll =
|
||||
scroller != nullptr
|
||||
? gtk_adjustment_get_value(gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(scroller)))
|
||||
: 0.0;
|
||||
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);
|
||||
@@ -340,7 +375,11 @@ void dock_rebuild_events(App& app) {
|
||||
gtk_list_box_append(list, row);
|
||||
return;
|
||||
}
|
||||
for (const EventRecord& r : records) {
|
||||
// The newest `kDockEventsMax` only: the list is newest first, and a block that builds four widgets per
|
||||
// cached event is a cost that grows with a cache the user may have set to unbounded.
|
||||
const std::size_t shown = std::min(records.size(), kDockEventsMax);
|
||||
for (std::size_t i = 0; i < shown; ++i) {
|
||||
const EventRecord& r = records[i];
|
||||
GtkWidget* row = gtk_list_box_row_new();
|
||||
GtkWidget* line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
gtk_widget_set_margin_start(line, 6);
|
||||
@@ -355,10 +394,13 @@ void dock_rebuild_events(App& app) {
|
||||
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);
|
||||
// The latch's sequence number, as the pane's third line ends with it: the title already carries the
|
||||
// clock, so this is the part of the row that is not in it.
|
||||
const std::string seq = "#" + std::to_string(r.seq);
|
||||
GtkWidget* num = fixed_label(seq.c_str(), kEventSeqChars, GTK_ALIGN_END);
|
||||
gtk_widget_set_tooltip_text(num, ("latch " + seq).c_str());
|
||||
gtk_widget_add_css_class(num, "dim-label");
|
||||
gtk_box_append(GTK_BOX(line), num);
|
||||
|
||||
g_object_set_data(G_OBJECT(row), "udpscope-event-marker", marker);
|
||||
g_object_set_data(G_OBJECT(row), "udpscope-event-title", name);
|
||||
@@ -367,6 +409,22 @@ void dock_rebuild_events(App& app) {
|
||||
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), line);
|
||||
gtk_list_box_append(list, row);
|
||||
}
|
||||
if (records.size() > shown) {
|
||||
// A last row with no record (like the placeholder: `dock_mark_events()` skips it, and it does nothing
|
||||
// when activated). Fixed text and the same ellipsis as the rest, so it cannot widen the dock.
|
||||
const std::string more = "\xE2\x80\xA6 " + std::to_string(records.size() - shown) + " more, see Manage";
|
||||
GtkWidget* label = fixed_label(more.c_str(), kEventTitleChars + kEventSeqChars, GTK_ALIGN_START);
|
||||
gtk_widget_add_css_class(label, "dim-label");
|
||||
gtk_widget_set_tooltip_text(label, more.c_str());
|
||||
gtk_widget_set_margin_start(label, 6);
|
||||
gtk_widget_set_margin_top(label, 4);
|
||||
gtk_widget_set_margin_bottom(label, 4);
|
||||
GtkWidget* row = gtk_list_box_row_new();
|
||||
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), label);
|
||||
gtk_list_box_row_set_activatable(GTK_LIST_BOX_ROW(row), false);
|
||||
gtk_list_box_append(list, row);
|
||||
}
|
||||
restore_dock_scroll(app.dock_events_list, scroll);
|
||||
}
|
||||
|
||||
void dock_mark_events(App& app) {
|
||||
|
||||
Reference in new issue
Block a user