Feature/source plugins #1
No files matched your search
@@ -276,6 +276,7 @@ add_executable(udpscope
|
||||
src/ui_badges.cpp
|
||||
src/ui_trigger.cpp
|
||||
src/ui_signals.cpp
|
||||
src/ui_signal_page.cpp
|
||||
src/ui_context_help.cpp
|
||||
src/ui_tour.cpp
|
||||
)
|
||||
|
||||
@@ -2634,6 +2634,19 @@ loads a file as the previous version wrote it (`side_tab = "measure"`) into a `L
|
||||
leave `dock_shown` at the default (`true`), so `ok=1` requires `dock_shown == default` -- the parser ignores unknown keys, and this is what says the old key still does not break a session. A load that
|
||||
left the caller's `false` alone would read `dock_shown=0 default=1 ok=0`. The probe writes only to a temp path.
|
||||
|
||||
## `[signalpage]`
|
||||
|
||||
```
|
||||
[signalpage] name=sine open=1 pages=1 enable_visible=1 plot_buttons=1 plot_on_row0=1 removed=1 btn_row0=1 back_to_list=1 row_opens=1 pruned=1 rows=1
|
||||
```
|
||||
|
||||
`UDPSCOPE_PROBE_SIGNAL_PAGES_MS` builds and drives the page of the first streamed signal. `open=1` (the slide-over shows `signal:<name>`), `pages=1`
|
||||
(one page in `App::signal_pages`), `enable_visible=1` (the acquire tick is shown in live mode; `0` in a file view), `plot_buttons=` equal to
|
||||
`rows=` (one "Plot on" button per figure row), `plot_on_row0=1` (`assign_signal_to_row()` returned true and the signal is plotted), `removed=1`
|
||||
(the page's "Remove from plots" took it off), `btn_row0=1` (the "Plot on 1" button, the user's path, put it back), `back_to_list=1` ("All signals"
|
||||
target), `row_opens=1` (activating the list row opens the page), and `pruned=1`: a math channel's page was opened, its **Delete** button clicked, and
|
||||
the slide-over closed and the page was dropped by `signal_pages_sync()`. `pruned=-1` means no math channel could be added; `0` is a stale page.
|
||||
|
||||
## `[badges]`
|
||||
|
||||
```
|
||||
|
||||
@@ -447,6 +447,10 @@ precedent, not an exception.
|
||||
read `click_inside_closes=0 still_open=1 click_outside_closes=1 esc_closes=1 closed=1`. For a screenshot
|
||||
add `UDPSCOPE_PROBE_SLIDEOVER_HOLD=1`, which leaves the page open after line 1 (run with `GSK_RENDERER=cairo`
|
||||
and `import -window root`): the panel must be drawn over the traces, opaque.
|
||||
- `UDPSCOPE_PROBE_SIGNAL_PAGES_MS=1500` with `UDPSCOPE_EXIT_AFTER_MS=5000` and `UDPSCOPE_ENABLE_ALL_MS=500` -- the
|
||||
per-signal `signal:<name>` slide-over pages. **Against a config that has already seen the tour** (a first run's
|
||||
auto-tour closes the slide-over at about 1.5 s, which is when this probe opens its page). One `[signalpage]` line;
|
||||
see [probe-readings.md](probe-readings.md).
|
||||
- `UDPSCOPE_PROBE_BADGES_MS=2000` with `UDPSCOPE_EXIT_AFTER_MS=5000` and `UDPSCOPE_ENABLE_ALL_MS=500` -- the badge
|
||||
bar under the plot. One `[badges]` line, which must read `table_bad=0 fixed=1 open_marked=1 page_open=1
|
||||
closed_unmarked=1`: `table_bad` counts the rows of the 16-input `trigger_badge_state()` table the probe
|
||||
|
||||
@@ -5887,6 +5887,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_BADGES_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), badges_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_SIGNAL_PAGES_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), signal_page_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_SEARCH_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), search_probe, &app);
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
@@ -885,6 +886,8 @@ struct App {
|
||||
guint layout_save_source = 0;
|
||||
bool layout_loaded = false;
|
||||
unsigned signal_generation = ~0u;
|
||||
/// Signal name -> the content box of its `signal:<name>` slide-over page (src/ui_signal_page.cpp).
|
||||
std::map<std::string, GtkWidget*> signal_pages;
|
||||
bool updating_panels = false; ///< guards signal handlers during a rebuild
|
||||
bool relayout_pending = false;
|
||||
/// True once the renderer's `Style` has been stated (see `tick()`): the renderer does not
|
||||
@@ -1669,6 +1672,24 @@ std::string signal_chip_markup(App& app, const std::vector<SignalInfo>& signals,
|
||||
void on_signal_color_chosen(GObject* source, GAsyncResult* res, gpointer user);
|
||||
void open_signal_color(App& app, std::size_t signal);
|
||||
void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals);
|
||||
/// Every plottable signal once (the time base skipped, a group's layers folded to the first), in model order.
|
||||
/// The one rule the list and the badges share.
|
||||
std::vector<std::size_t> listed_signals(const std::vector<SignalInfo>& signals);
|
||||
/// Puts `signal` (an index into `signals()`) on plot `row` in the first free slot, switching it on first if
|
||||
/// need be. Says why not through `set_notice` and returns false when it cannot. The one path a drop and a
|
||||
/// signal page's "Plot on" button share.
|
||||
bool assign_signal_to_row(App& app, std::size_t signal, int row);
|
||||
/// Back to the palette colour for the signal's position.
|
||||
void reset_signal_color(App& app, std::size_t signal);
|
||||
/// Holds `widget` alive across the current emission: a handler that rebuilds the list or a page destroys the
|
||||
/// widget it was emitted from.
|
||||
void outlive_the_rebuild(GtkWidget* widget);
|
||||
/// Creates the page `signal:<name>` if it is missing, fills it, and returns whether it exists afterwards.
|
||||
bool signal_page_ensure(App& app, const std::string& name);
|
||||
/// Called at the end of `rebuild_signal_panels`: refills every existing signal page, drops the pages of
|
||||
/// signals the model no longer has (closing the slide-over first if one of them is open).
|
||||
void signal_pages_sync(App& app);
|
||||
gboolean signal_page_probe(gpointer user); // UDPSCOPE_PROBE_SIGNAL_PAGES_MS
|
||||
/// The `MATH` divider row in the one channel list, and the `+` it carries.
|
||||
GtkWidget* make_math_divider(App& app, bool with_add);
|
||||
void on_math_add_clicked(GtkButton* button, gpointer user);
|
||||
@@ -1915,6 +1936,8 @@ gboolean dock_session_probe(gpointer user); // UDPSCOPE_PROBE_DOCK_SESSION_MS
|
||||
void build_slideover(App& app); // once, after the plot_overlay exists
|
||||
bool slideover_add_page(App& app, const std::string& id, const std::string& title, GtkWidget* content);
|
||||
bool slideover_has_page(const App& app, const std::string& id);
|
||||
/// The inverse of `slideover_add_page`: false when the id is unknown or is the open page.
|
||||
bool slideover_remove_page(App& app, const std::string& id);
|
||||
bool slideover_open(App& app, const std::string& id); // false when the id is unknown
|
||||
void slideover_close(App& app);
|
||||
bool slideover_toggle(App& app, const std::string& id); // open if closed or on another page, else close
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||
//
|
||||
// The per-signal slide-over pages (`signal:<name>`): one signal's acquire switch, colour, the plot it is on,
|
||||
// its information and calibration (a math channel's editor instead). Opened from a row of the signal list;
|
||||
// kept in step with the model by `signal_pages_sync()`, which `rebuild_signal_panels()` ends with.
|
||||
|
||||
#include "ui_app.hpp"
|
||||
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace udpscope {
|
||||
namespace ui_impl {
|
||||
|
||||
namespace {
|
||||
|
||||
std::string page_id(const std::string& name) { return "signal:" + name; }
|
||||
|
||||
/// The index of `name` in the model's signals, or npos.
|
||||
std::size_t signal_index(App& app, const std::string& name) {
|
||||
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||
const std::vector<SignalInfo>& s = app.model->signals();
|
||||
for (std::size_t i = 0; i < s.size(); ++i) {
|
||||
if (s[i].name == name) return i;
|
||||
}
|
||||
return std::string::npos;
|
||||
}
|
||||
|
||||
GtkWidget* text_button(const char* label, GtkWidget* parent) {
|
||||
GtkWidget* b = gtk_button_new_with_label(label);
|
||||
gtk_box_append(GTK_BOX(parent), b);
|
||||
return b;
|
||||
}
|
||||
|
||||
/// Stores the signal's index and the page's name on the button, so a handler resolves identity the way the
|
||||
/// list does.
|
||||
void tag(GtkWidget* b, std::size_t index) {
|
||||
g_object_set_data(G_OBJECT(b), "udpscope-signal", GSIZE_TO_POINTER(index));
|
||||
}
|
||||
std::size_t tagged(GtkButton* b) {
|
||||
return static_cast<std::size_t>(GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(b), "udpscope-signal")));
|
||||
}
|
||||
|
||||
void fill(App& app, const std::string& name, GtkWidget* page) {
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(page); c != nullptr;) {
|
||||
GtkWidget* next = gtk_widget_get_next_sibling(c);
|
||||
gtk_box_remove(GTK_BOX(page), c);
|
||||
c = next;
|
||||
}
|
||||
const std::size_t index = signal_index(app, name);
|
||||
if (index == std::string::npos) return;
|
||||
// A snapshot, taken under the lock: the chip helper wants the whole vector, and the producer replaces it
|
||||
// on every CONFIG.
|
||||
std::vector<SignalInfo> all;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||
all = app.model->signals();
|
||||
}
|
||||
if (index >= all.size()) return;
|
||||
const SignalInfo sig = all[index];
|
||||
const bool enabled = app.model->signal_enabled(index);
|
||||
const bool plotted = signal_plotted(app, sig.name);
|
||||
const bool unlocked = !app.signals_locked;
|
||||
|
||||
// Back to the list: a page reached from a list row has no other way home.
|
||||
GtkWidget* back = text_button("All signals", page);
|
||||
gtk_widget_set_halign(back, GTK_ALIGN_START);
|
||||
gtk_widget_add_css_class(back, "flat");
|
||||
g_signal_connect(back, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
slideover_open(*static_cast<App*>(user), "signals");
|
||||
}),
|
||||
&app);
|
||||
|
||||
GtkWidget* title = gtk_label_new(sig.name.c_str());
|
||||
gtk_widget_add_css_class(title, "title-3");
|
||||
gtk_widget_set_halign(title, GTK_ALIGN_START);
|
||||
gtk_label_set_ellipsize(GTK_LABEL(title), PANGO_ELLIPSIZE_END);
|
||||
gtk_box_append(GTK_BOX(page), title);
|
||||
if (!sig.unit.empty()) {
|
||||
GtkWidget* unit = gtk_label_new(sig.unit.c_str());
|
||||
gtk_widget_add_css_class(unit, "dim-label");
|
||||
gtk_widget_set_halign(unit, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(page), unit);
|
||||
}
|
||||
|
||||
// Acquire / compute. The same handler as the list's tick, which reads the signal index off the widget.
|
||||
GtkWidget* enable = gtk_check_button_new_with_label(sig.math ? "Compute this channel" : "Acquire this signal");
|
||||
app.updating_panels = true;
|
||||
gtk_check_button_set_active(GTK_CHECK_BUTTON(enable), enabled);
|
||||
app.updating_panels = false;
|
||||
g_object_set_data(G_OBJECT(enable), "udpscope-signal", GSIZE_TO_POINTER(index));
|
||||
g_signal_connect(enable, "notify::active", G_CALLBACK(on_signal_switch), &app);
|
||||
gtk_widget_set_visible(enable, sig.math || app.waveform_name.empty());
|
||||
gtk_box_append(GTK_BOX(page), enable);
|
||||
|
||||
// Colour.
|
||||
GtkWidget* colour_head = gtk_label_new("Colour");
|
||||
gtk_widget_add_css_class(colour_head, "heading");
|
||||
gtk_widget_set_halign(colour_head, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(page), colour_head);
|
||||
GtkWidget* chip = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(chip), signal_chip_markup(app, all, index).c_str());
|
||||
GtkWidget* colour_row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_box_append(GTK_BOX(colour_row), chip);
|
||||
GtkWidget* pick = text_button("Change colour...", colour_row);
|
||||
gtk_widget_set_sensitive(pick, plotted && unlocked);
|
||||
gtk_widget_set_tooltip_text(pick, plotted ? "the colour this signal is plotted in"
|
||||
: "on no plot, so there is no trace to colour");
|
||||
tag(pick, index);
|
||||
g_signal_connect(pick, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
|
||||
open_signal_color(*static_cast<App*>(user), tagged(b));
|
||||
}),
|
||||
&app);
|
||||
GtkWidget* reset = text_button("Default colour", colour_row);
|
||||
gtk_widget_set_sensitive(reset, unlocked && app.rows->signal_colors().count(sig.name) != 0);
|
||||
tag(reset, index);
|
||||
g_signal_connect(reset, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
|
||||
outlive_the_rebuild(GTK_WIDGET(b));
|
||||
reset_signal_color(*static_cast<App*>(user), tagged(b));
|
||||
}),
|
||||
&app);
|
||||
gtk_box_append(GTK_BOX(page), colour_row);
|
||||
|
||||
// Row: "Plot on 1 2 3 ..." and the way back off.
|
||||
GtkWidget* row_head = gtk_label_new("Plot on");
|
||||
gtk_widget_add_css_class(row_head, "heading");
|
||||
gtk_widget_set_halign(row_head, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(page), row_head);
|
||||
GtkWidget* rows_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
|
||||
gtk_widget_set_name(rows_box, "udpscope-plot-on"); // the probe counts its buttons
|
||||
for (int r = 0; r < app.rows->row_count(); ++r) {
|
||||
GtkWidget* b = text_button(std::to_string(r + 1).c_str(), rows_box);
|
||||
tag(b, index);
|
||||
g_object_set_data(G_OBJECT(b), "udpscope-row", GINT_TO_POINTER(r));
|
||||
gtk_widget_set_tooltip_text(b, "draw this signal on this plot");
|
||||
g_signal_connect(b, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
|
||||
outlive_the_rebuild(GTK_WIDGET(b));
|
||||
assign_signal_to_row(*static_cast<App*>(user), tagged(b),
|
||||
GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), "udpscope-row")));
|
||||
}),
|
||||
&app);
|
||||
}
|
||||
gtk_box_append(GTK_BOX(page), rows_box);
|
||||
GtkWidget* off = text_button("Remove from plots", page);
|
||||
gtk_widget_set_halign(off, GTK_ALIGN_START);
|
||||
gtk_widget_set_sensitive(off, plotted);
|
||||
tag(off, index);
|
||||
g_signal_connect(off, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
outlive_the_rebuild(GTK_WIDGET(b));
|
||||
std::string n;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a->model->mutex());
|
||||
const std::vector<SignalInfo>& s = a->model->signals();
|
||||
if (tagged(b) >= s.size()) return;
|
||||
n = s[tagged(b)].name;
|
||||
}
|
||||
const int gone = remove_signal_from_plots(*a, n);
|
||||
a->relayout_pending = true;
|
||||
rebuild_signal_panels(*a);
|
||||
schedule_layout_save(*a);
|
||||
set_notice(*a, "'" + n + "' left " + std::to_string(gone) + (gone == 1 ? " plot" : " plots"), 4);
|
||||
}),
|
||||
&app);
|
||||
|
||||
// Information (a streamed signal's), calibration (ditto), and a math channel's editor.
|
||||
GtkWidget* more_head = gtk_label_new(sig.math ? "Math channel" : "Signal");
|
||||
gtk_widget_add_css_class(more_head, "heading");
|
||||
gtk_widget_set_halign(more_head, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(page), more_head);
|
||||
GtkWidget* more = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
if (sig.math) {
|
||||
GtkWidget* edit = text_button("Edit graph...", more);
|
||||
tag(edit, index);
|
||||
g_signal_connect(edit, "clicked", G_CALLBACK(on_math_edit_clicked), &app);
|
||||
GtkWidget* del = text_button("Delete", more);
|
||||
gtk_widget_add_css_class(del, "destructive-action");
|
||||
tag(del, index);
|
||||
g_signal_connect(del, "clicked", G_CALLBACK(on_math_delete_clicked), &app);
|
||||
} else {
|
||||
GtkWidget* info = text_button("Information", more);
|
||||
tag(info, index);
|
||||
g_signal_connect(info, "clicked", G_CALLBACK(on_info_clicked), &app);
|
||||
GtkWidget* cal = text_button("Calibrate...", more);
|
||||
gtk_widget_set_sensitive(cal, unlocked);
|
||||
gtk_widget_set_tooltip_text(cal, "gain and offset, applied at ingest");
|
||||
tag(cal, index);
|
||||
g_signal_connect(cal, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
open_calibration(*a, GTK_WINDOW(a->window), tagged(b));
|
||||
}),
|
||||
&app);
|
||||
}
|
||||
gtk_box_append(GTK_BOX(page), more);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool signal_page_ensure(App& app, const std::string& name) {
|
||||
if (signal_index(app, name) == std::string::npos) return false;
|
||||
auto it = app.signal_pages.find(name);
|
||||
if (it == app.signal_pages.end()) {
|
||||
GtkWidget* page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||
gtk_widget_set_margin_top(page, 12);
|
||||
gtk_widget_set_margin_bottom(page, 12);
|
||||
gtk_widget_set_margin_start(page, 12);
|
||||
gtk_widget_set_margin_end(page, 12);
|
||||
if (!slideover_add_page(app, page_id(name), name, page)) return false;
|
||||
it = app.signal_pages.emplace(name, page).first;
|
||||
}
|
||||
fill(app, name, it->second);
|
||||
return true;
|
||||
}
|
||||
|
||||
void signal_pages_sync(App& app) {
|
||||
for (auto it = app.signal_pages.begin(); it != app.signal_pages.end();) {
|
||||
if (signal_index(app, it->first) == std::string::npos) {
|
||||
if (slideover_page(app) == page_id(it->first)) slideover_close(app);
|
||||
slideover_remove_page(app, page_id(it->first));
|
||||
it = app.signal_pages.erase(it);
|
||||
} else {
|
||||
fill(app, it->first, it->second);
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- the probe ---------------------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
/// The first descendant of `root` for which `want` holds, or null.
|
||||
template <typename Pred>
|
||||
GtkWidget* find_descendant(GtkWidget* root, Pred want) {
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(root); c != nullptr; c = gtk_widget_get_next_sibling(c)) {
|
||||
if (want(c)) return c;
|
||||
if (GtkWidget* deeper = find_descendant(c, want)) return deeper;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool is_button_labelled(GtkWidget* w, const char* label) {
|
||||
if (!GTK_IS_BUTTON(w)) return false;
|
||||
const char* l = gtk_button_get_label(GTK_BUTTON(w));
|
||||
return l != nullptr && std::string(l) == label;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
gboolean signal_page_probe(gpointer user) {
|
||||
App& a = *static_cast<App*>(user);
|
||||
std::vector<SignalInfo> all;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(a.model->mutex());
|
||||
all = a.model->signals();
|
||||
}
|
||||
std::size_t index = std::string::npos;
|
||||
for (std::size_t i : listed_signals(all)) {
|
||||
if (!all[i].math) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index == std::string::npos) {
|
||||
std::fprintf(stderr, "[signalpage] no listed signal\n");
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
const std::string name = all[index].name;
|
||||
const std::string id = page_id(name);
|
||||
const bool made = signal_page_ensure(a, name);
|
||||
slideover_open(a, id);
|
||||
const int open = slideover_page(a) == id ? 1 : 0;
|
||||
const int pages = static_cast<int>(a.signal_pages.size());
|
||||
|
||||
int enable_visible = -1;
|
||||
int plot_buttons = -1;
|
||||
if (made) {
|
||||
GtkWidget* page = a.signal_pages[name];
|
||||
GtkWidget* enable = find_descendant(page, [](GtkWidget* w) { return GTK_IS_CHECK_BUTTON(w); });
|
||||
if (enable != nullptr) enable_visible = gtk_widget_get_visible(enable) ? 1 : 0;
|
||||
GtkWidget* rows_box = find_descendant(page, [](GtkWidget* w) {
|
||||
return std::string(gtk_widget_get_name(w)) == "udpscope-plot-on";
|
||||
});
|
||||
if (rows_box != nullptr) {
|
||||
plot_buttons = 0;
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(rows_box); c != nullptr;
|
||||
c = gtk_widget_get_next_sibling(c)) {
|
||||
++plot_buttons;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Plot on row 0 through the function the "Plot on" buttons call, then take it off through the page's own
|
||||
// button (outlive_the_rebuild + remove_signal_from_plots + the page's refill).
|
||||
const bool assigned = assign_signal_to_row(a, index, 0);
|
||||
const int plot_on_row0 = assigned && signal_plotted(a, name) ? 1 : 0;
|
||||
int removed = -1;
|
||||
auto page_it = a.signal_pages.find(name);
|
||||
GtkWidget* off = page_it == a.signal_pages.end()
|
||||
? nullptr
|
||||
: find_descendant(page_it->second, [](GtkWidget* w) {
|
||||
return is_button_labelled(w, "Remove from plots");
|
||||
});
|
||||
if (off != nullptr) {
|
||||
g_signal_emit_by_name(off, "clicked");
|
||||
removed = signal_plotted(a, name) ? 0 : 1;
|
||||
}
|
||||
// The page's own "Plot on 1" button (the user's path), then off again.
|
||||
int btn_row0 = -1;
|
||||
page_it = a.signal_pages.find(name);
|
||||
GtkWidget* rows_box_now =
|
||||
page_it == a.signal_pages.end() ? nullptr : find_descendant(page_it->second, [](GtkWidget* w) {
|
||||
return std::string(gtk_widget_get_name(w)) == "udpscope-plot-on";
|
||||
});
|
||||
if (rows_box_now != nullptr && gtk_widget_get_first_child(rows_box_now) != nullptr) {
|
||||
g_signal_emit_by_name(gtk_widget_get_first_child(rows_box_now), "clicked");
|
||||
btn_row0 = signal_plotted(a, name) ? 1 : 0;
|
||||
page_it = a.signal_pages.find(name);
|
||||
off = page_it == a.signal_pages.end()
|
||||
? nullptr
|
||||
: find_descendant(page_it->second, [](GtkWidget* w) { return is_button_labelled(w, "Remove from plots"); });
|
||||
if (off != nullptr) g_signal_emit_by_name(off, "clicked");
|
||||
}
|
||||
slideover_open(a, "signals");
|
||||
const int back_to_list = slideover_page(a) == "signals" ? 1 : 0;
|
||||
|
||||
// Activating the signal's row in the list opens its page (the path a click takes).
|
||||
slideover_close(a);
|
||||
int row_opens = -1;
|
||||
for (GtkWidget* r = gtk_widget_get_first_child(a.signal_list); r != nullptr; r = gtk_widget_get_next_sibling(r)) {
|
||||
if (GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(r), "udpscope-signal")) == index && GTK_IS_LIST_BOX_ROW(r)) {
|
||||
g_signal_emit_by_name(r, "activate");
|
||||
row_opens = slideover_page(a) == id ? 1 : 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Pruning: a math channel's page, open, then the channel deleted; the sync must close the panel and drop
|
||||
// the page. Left at -1 when no channel can be added (nothing acquired to build it from).
|
||||
int pruned = -1;
|
||||
const std::size_t math_before = a.math.size();
|
||||
add_math_channel(a);
|
||||
if (a.math.size() == math_before + 1) {
|
||||
const std::string mname = a.math.back().name;
|
||||
if (signal_page_ensure(a, mname)) {
|
||||
slideover_open(a, page_id(mname));
|
||||
// Through the page's own Delete button: the click destroys the page it came from.
|
||||
GtkWidget* del = find_descendant(a.signal_pages[mname],
|
||||
[](GtkWidget* w) { return is_button_labelled(w, "Delete"); });
|
||||
if (del != nullptr) {
|
||||
g_signal_emit_by_name(del, "clicked");
|
||||
} else {
|
||||
a.math.pop_back();
|
||||
apply_math_channels(a);
|
||||
}
|
||||
pruned = a.signal_pages.count(mname) == 0 && !slideover_has_page(a, page_id(mname)) &&
|
||||
slideover_page(a).empty()
|
||||
? 1
|
||||
: 0;
|
||||
} else {
|
||||
a.math.pop_back();
|
||||
apply_math_channels(a);
|
||||
}
|
||||
}
|
||||
slideover_close(a);
|
||||
std::fprintf(stderr,
|
||||
"[signalpage] name=%s open=%d pages=%d enable_visible=%d plot_buttons=%d plot_on_row0=%d "
|
||||
"removed=%d btn_row0=%d back_to_list=%d row_opens=%d pruned=%d rows=%d\n",
|
||||
name.c_str(), open, pages, enable_visible, plot_buttons, plot_on_row0, removed, btn_row0,
|
||||
back_to_list, row_opens, pruned, a.rows->row_count());
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
} // namespace ui_impl
|
||||
} // namespace udpscope
|
||||
+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;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#include <adwaita.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -122,6 +123,24 @@ bool slideover_has_page(const App& app, const std::string& id) {
|
||||
gtk_stack_get_child_by_name(GTK_STACK(app.slideover.stack), id.c_str()) != nullptr;
|
||||
}
|
||||
|
||||
bool slideover_remove_page(App& app, const std::string& id) {
|
||||
SlideOver& s = app.slideover;
|
||||
if (!slideover_has_page(app, id) || s.page == id) return false;
|
||||
GtkWidget* child = gtk_stack_get_child_by_name(GTK_STACK(s.stack), id.c_str());
|
||||
if (is_source_page(id)) {
|
||||
s.source_pages.erase(std::remove(s.source_pages.begin(), s.source_pages.end(), id),
|
||||
s.source_pages.end());
|
||||
AdwToggle* t = adw_toggle_group_get_toggle_by_name(ADW_TOGGLE_GROUP(s.source_switch), id.c_str());
|
||||
if (t != nullptr) {
|
||||
s.syncing = true;
|
||||
adw_toggle_group_remove(ADW_TOGGLE_GROUP(s.source_switch), t);
|
||||
s.syncing = false;
|
||||
}
|
||||
}
|
||||
gtk_stack_remove(GTK_STACK(s.stack), child);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool slideover_open(App& app, const std::string& id) {
|
||||
SlideOver& s = app.slideover;
|
||||
if (!slideover_has_page(app, id)) return false;
|
||||
|
||||
Reference in new issue
Block a user