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:
1 parent
0dd235e41c
commit
baf4a1f20f
8 files changed
+529
-48
No files matched your search
+86
-48
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user