ui: per-signal slide-over pages; the drop's assignment and the colour reset as shared functions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Opus 4.6 committed 2026-11-03 01:03:51 +01:00
1 parent 0dd235e41c
commit baf4a1f20f
8 files changed
+529 -48

No files matched your search

+86 -48
View File
@@ -200,7 +200,7 @@ GtkWidget* make_math_divider(App& app, bool with_add) {
/// emission, and a button's own code keeps running after the handler returns, so the last reference
/// has to outlive the emission: hold one here and drop it from an idle. This is the same repair
/// `on_signal_switch()` makes, for the same reason.
static void outlive_the_rebuild(GtkWidget* widget) {
void outlive_the_rebuild(GtkWidget* widget) {
g_object_ref(widget);
g_idle_add_full(G_PRIORITY_DEFAULT_IDLE,
+[](gpointer) -> gboolean { return G_SOURCE_REMOVE; }, widget, g_object_unref);
@@ -257,6 +257,34 @@ void on_math_delete_clicked(GtkButton* button, gpointer user) {
apply_math_channels(*a);
}
std::vector<std::size_t> listed_signals(const std::vector<SignalInfo>& signals) {
std::vector<std::size_t> out;
for (std::size_t i = 0; i < signals.size(); ++i) {
if (ScopeModel::is_time_signal(signals, i)) continue; // the time base is not plottable
const std::string base = layer_base_name(signals[i].name);
bool seen = false;
for (std::size_t j = 0; j < i && !seen; ++j) {
seen = !ScopeModel::is_time_signal(signals, j) && layer_base_name(signals[j].name) == base;
}
if (!seen) out.push_back(i);
}
return out;
}
void reset_signal_color(App& app, std::size_t signal) {
std::string name;
{
std::lock_guard<std::mutex> lock(app.model->mutex());
const std::vector<SignalInfo>& sigs = app.model->signals();
if (signal >= sigs.size()) return;
name = sigs[signal].name;
}
app.rows->clear_signal_color(name);
app.relayout_pending = true;
rebuild_signal_panels(app);
schedule_layout_save(app);
}
/// One line per signal: colour chip, checkbox, name, unit, and a menu for the things
/// you touch once (information, calibration). A `GtkSwitch` and two always-visible
/// buttons made every row about twice this tall, which is what turned a four-signal
@@ -282,18 +310,11 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
bool math_divider_done = false;
// Streamed signals, counted as they are drawn: the dump reads `rows == signals + 1 + math`.
std::size_t plain = 0;
for (std::size_t i = 0; i < signals.size(); ++i) {
if (ScopeModel::is_time_signal(signals, i)) continue; // the time base is not plottable
for (std::size_t i : listed_signals(signals)) {
// Locked: a row per signal that is *on*, which is the list a user reads rather than the list they
// configure. Turning one on is what the pane's [edit] button is for.
if (app.signals_locked && !app.model->signal_enabled(i)) continue;
const std::string base = layer_base_name(signals[i].name);
bool seen = false;
for (std::size_t j = 0; j < i && !seen; ++j) {
seen = !ScopeModel::is_time_signal(signals, j) &&
layer_base_name(signals[j].name) == base;
}
if (!seen) rows.push_back(i);
rows.push_back(i);
}
for (std::size_t i : rows) {
@@ -385,20 +406,12 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
g_object_set_data(G_OBJECT(recolor), "udpscope-signal", GSIZE_TO_POINTER(i));
g_signal_connect(recolor, "clicked",
G_CALLBACK(+[](GtkButton* b, gpointer user) {
App* a = static_cast<App*>(user);
const std::size_t idx = static_cast<std::size_t>(
GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(b), "udpscope-signal")));
std::string name;
{
std::lock_guard<std::mutex> lock(a->model->mutex());
const std::vector<SignalInfo>& sigs = a->model->signals();
if (idx >= sigs.size()) return;
name = sigs[idx].name;
}
a->rows->clear_signal_color(name);
a->relayout_pending = true;
rebuild_signal_panels(*a);
schedule_layout_save(*a);
// The rebuild destroys this button (it is in the list it rebuilds).
outlive_the_rebuild(GTK_WIDGET(b));
reset_signal_color(
*static_cast<App*>(user),
static_cast<std::size_t>(GPOINTER_TO_SIZE(
g_object_get_data(G_OBJECT(b), "udpscope-signal"))));
}),
&app);
if (sig.math) {
@@ -451,6 +464,8 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
if (!app.signals_locked) gtk_box_append(GTK_BOX(box), more);
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), box);
// Activating the row opens the signal's page; the row resolves its identity the way its widgets do.
g_object_set_data(G_OBJECT(row), "udpscope-signal", GSIZE_TO_POINTER(i));
DragContext* ctx = new DragContext{&app, i};
GtkDragSource* source = gtk_drag_source_new();
@@ -487,26 +502,18 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
app.updating_panels = false;
}
gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y,
gpointer user) {
App* app = static_cast<App*>(user);
if (!G_VALUE_HOLDS_STRING(value)) return FALSE;
// Where the drop landed decides the row: one area covers them all.
const int row = row_at(*app, x, y);
const char* text = g_value_get_string(value);
const std::size_t signal = static_cast<std::size_t>(std::atoll(text ? text : "0"));
if (signal >= app->model->signals().size()) return FALSE;
bool assign_signal_to_row(App& app, std::size_t signal, int row) {
if (signal >= app.model->signals().size()) return false;
const auto notice = [&](const std::string& what) {
set_notice(*app, what, 4);
set_notice(app, what, 4);
std::fprintf(stderr, "[udpscope] drop: %s\n", what.c_str());
std::fflush(stderr);
};
// The dropped signal's channels are the ones the model built for it; take the
// first, or the first element if the signal is a vector.
const std::vector<ChannelInfo>& channels = app->model->channels();
const std::vector<ChannelInfo>& channels = app.model->channels();
int channel = -1;
std::string name;
for (std::size_t i = 0; i < channels.size(); ++i) {
@@ -518,8 +525,8 @@ gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y
}
if (channel < 0) {
// The signal is not enabled: enable it, then take the channel that appears.
app->model->set_signal_enabled(signal, true);
const std::vector<ChannelInfo>& again = app->model->channels();
app.model->set_signal_enabled(signal, true);
const std::vector<ChannelInfo>& again = app.model->channels();
for (std::size_t i = 0; i < again.size(); ++i) {
if (again[i].signal == signal) {
channel = static_cast<int>(i);
@@ -529,14 +536,14 @@ gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y
}
}
if (channel < 0) {
notice("cannot acquire '" + app->model->signals()[signal].name +
notice("cannot acquire '" + app.model->signals()[signal].name +
"' (multi-element signals are not acquired)");
return FALSE;
return false;
}
int slot = -1;
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
if (!app->rows->slot_bound(row, s)) {
if (!app.rows->slot_bound(row, s)) {
slot = s;
break;
}
@@ -547,18 +554,30 @@ gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y
notice("plot " + std::to_string(row + 1) +
" holds four signals already: drop it on another plot, or add a row with the layout "
"editor (L)");
return FALSE;
return false;
}
// The slot remembers the *signal* (and element), never the channel index: the
// channel list is renumbered whenever a signal is enabled or disabled.
const std::vector<SignalInfo>& signals = app->model->signals();
const std::vector<SignalInfo>& signals = app.model->signals();
const SignalInfo& sig = signals[signal];
app->rows->assign(row, slot, sig.name, name, signal_color_index(signals, signal));
schedule_layout_save(*app);
app->relayout_pending = true;
rebuild_signal_panels(*app); // the row's colour chip now has a trace to name
cancel_drag_on(*app, row);
app.rows->assign(row, slot, sig.name, name, signal_color_index(signals, signal));
schedule_layout_save(app);
app.relayout_pending = true;
rebuild_signal_panels(app); // the row's colour chip now has a trace to name
notice("plotting '" + name + "' on plot " + std::to_string(row + 1));
return true;
}
gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y,
gpointer user) {
App* app = static_cast<App*>(user);
if (!G_VALUE_HOLDS_STRING(value)) return FALSE;
// Where the drop landed decides the row: one area covers them all.
const int row = row_at(*app, x, y);
const char* text = g_value_get_string(value);
const std::size_t signal = static_cast<std::size_t>(std::atoll(text ? text : "0"));
if (!assign_signal_to_row(*app, signal, row)) return FALSE;
cancel_drag_on(*app, row);
return TRUE;
}
@@ -582,6 +601,7 @@ void rebuild_signal_panels(App& app) {
if (!have_signals) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.trigger_toggle), FALSE);
}
signal_pages_sync(app);
app.signal_generation = app.model->generation();
}
@@ -607,6 +627,24 @@ GtkWidget* build_signal_panel(App& app) {
// it, and `[udpscope] signals panel:` reads the difference.
return box != nullptr ? box : gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
}
// Activating a row (a click on the row's own space; its tick, chip and menu consume their own clicks)
// opens that signal's page.
gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(app.signal_list), TRUE);
g_signal_connect(app.signal_list, "row-activated",
G_CALLBACK(+[](GtkListBox*, GtkListBoxRow* row, gpointer user) {
App* a = static_cast<App*>(user);
const std::size_t index = static_cast<std::size_t>(
GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(row), "udpscope-signal")));
std::string name;
{
std::lock_guard<std::mutex> lock(a->model->mutex());
const std::vector<SignalInfo>& s = a->model->signals();
if (index >= s.size()) return;
name = s[index].name;
}
if (signal_page_ensure(*a, name)) slideover_open(*a, "signal:" + name);
}),
&app);
return box;
}