working on todo.md features
This commit is contained in:
1 parent
210d124814
commit
8f54841c2c
110 files changed
+15932
-6576
No files matched your search
+424
-18
@@ -3,11 +3,15 @@
|
||||
|
||||
#include "source_dialog.hpp"
|
||||
|
||||
#include "waveform.hpp"
|
||||
|
||||
#include "layout_store.hpp"
|
||||
|
||||
#include <adwaita.h>
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
@@ -24,13 +28,22 @@ void apply_source(Config& cfg, const KnownSource& src) {
|
||||
cfg.port = src.port;
|
||||
cfg.multicast = src.multicast;
|
||||
cfg.interface_addr = src.interface_addr;
|
||||
// The configuration's own startup settings. They are read *before* the model exists, which is the
|
||||
// only moment a buffer size can be applied at all -- and the reason they are stored per
|
||||
// configuration rather than only passed on the command line.
|
||||
if (src.buffer_mb > 0.0) cfg.buffer_mb = src.buffer_mb;
|
||||
cfg.relative_time = src.relative_time;
|
||||
cfg.capture_packets = src.capture_packets;
|
||||
}
|
||||
|
||||
/// One line per known source on stderr, in the style of the probes in `ui.cpp`.
|
||||
void report_sources(const std::vector<KnownSource>& known) {
|
||||
std::fprintf(stderr, "[sources] %zu known:", known.size());
|
||||
for (const KnownSource& s : known) {
|
||||
std::fprintf(stderr, " %s:%u plots=%d signals=%d", s.host.c_str(), s.port, s.rows, s.signals);
|
||||
std::fprintf(stderr, " %s:%u/%s plots=%d signals=%d buffer=%.6g relative=%d capture=%u",
|
||||
s.host.c_str(), s.port, s.config.c_str(), s.rows, s.signals, s.buffer_mb,
|
||||
s.relative_time ? 1 : 0, s.capture_packets);
|
||||
if (!s.comment.empty()) std::fprintf(stderr, " note='%s'", s.comment.c_str());
|
||||
if (!s.multicast.empty()) std::fprintf(stderr, " multicast=%s", s.multicast.c_str());
|
||||
if (!s.interface_addr.empty()) {
|
||||
std::fprintf(stderr, " interface=%s", s.interface_addr.c_str());
|
||||
@@ -51,6 +64,12 @@ struct Chooser {
|
||||
GtkWidget* window = nullptr;
|
||||
GtkWidget* list = nullptr;
|
||||
std::vector<KnownSource> known;
|
||||
/// The Waveforms page: the exports opened before, newest first, and the notebook that holds the
|
||||
/// two pages. A waveform is the *other* answer to "which source", so it gets a page of its own
|
||||
/// rather than a button that hides a file dialog behind one more click.
|
||||
GtkWidget* notebook = nullptr;
|
||||
GtkWidget* wave_list = nullptr;
|
||||
std::vector<std::string> waves;
|
||||
/// The connection form, created on demand. Members of the chooser rather than a heap struct:
|
||||
/// the chooser outlives the form by construction, so there is nothing to free on a close path
|
||||
/// -- and a dialog whose context is freed by a handler is how this project earned a use-after
|
||||
@@ -61,10 +80,26 @@ struct Chooser {
|
||||
GtkWidget* form_group = nullptr;
|
||||
GtkWidget* form_iface = nullptr;
|
||||
GtkWidget* form_connect = nullptr;
|
||||
/// The configuration half of the form: what a source's *session* is called and the settings that
|
||||
/// can only be applied before the model exists. A new host needs all of them, and so does an
|
||||
/// existing one -- which is why the same form is used for both.
|
||||
GtkWidget* form_name = nullptr;
|
||||
GtkWidget* form_note = nullptr;
|
||||
GtkWidget* form_buffer = nullptr;
|
||||
GtkWidget* form_relative = nullptr;
|
||||
/// The configuration the form is editing: empty for a new host. Copied rather than referenced,
|
||||
/// because `chooser_populate()` rebuilds `known` under the form.
|
||||
std::string edit_host;
|
||||
std::uint16_t edit_port = 0;
|
||||
std::string edit_config;
|
||||
/// `UDPSCOPE_PROBE_PICK_MS`'s timeout, kept so a pick made before it fires can cancel it. The
|
||||
/// probe clears it when it runs: `g_source_remove()` on an id whose source already returned
|
||||
/// `G_SOURCE_REMOVE` is a GLib critical, not a no-op.
|
||||
guint pick_source = 0;
|
||||
/// What the *form* last wrote, for `report_pick()`: a save finishes the chooser and destroys the
|
||||
/// list with it, so the row a report would name is gone by the time the probe reads it.
|
||||
std::string saved_config;
|
||||
std::string saved_note;
|
||||
};
|
||||
|
||||
void chooser_finish(Chooser& c, bool chosen) {
|
||||
@@ -88,15 +123,125 @@ void chooser_finish(Chooser& c, bool chosen) {
|
||||
}
|
||||
|
||||
void chooser_populate(Chooser& c);
|
||||
void chooser_populate_waves(Chooser& c);
|
||||
void report_pick(const Chooser& c);
|
||||
/// Defined below; the row buttons reach it, and the row builder sits above it.
|
||||
void open_form(Chooser& c, int edit_index);
|
||||
|
||||
void on_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) {
|
||||
Chooser& c = *static_cast<Chooser*>(user);
|
||||
const int idx = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(row), "source-index"));
|
||||
if (idx < 0 || static_cast<std::size_t>(idx) >= c.known.size()) return;
|
||||
apply_source(*c.cfg, c.known[static_cast<std::size_t>(idx)]);
|
||||
const KnownSource& src = c.known[static_cast<std::size_t>(idx)];
|
||||
// Picking a configuration *is* choosing it as the active one: the app reads that back when it
|
||||
// loads the session, and the next save goes to the same file. Without this, opening `bench`
|
||||
// from the chooser would leave the source pointing at whatever was active before.
|
||||
set_active_configuration(src.host, src.port, src.config);
|
||||
apply_source(*c.cfg, src);
|
||||
chooser_finish(c, true);
|
||||
}
|
||||
|
||||
/// One row per waveform opened before, newest first. The name first (that is what a person reads),
|
||||
/// the directory under it, and a missing file said out loud rather than left to fail on the click.
|
||||
void chooser_populate_waves(Chooser& c) {
|
||||
while (GtkWidget* child = gtk_widget_get_first_child(c.wave_list)) {
|
||||
gtk_list_box_remove(GTK_LIST_BOX(c.wave_list), child);
|
||||
}
|
||||
c.waves = recent_waveforms();
|
||||
if (c.waves.empty()) {
|
||||
GtkWidget* empty = gtk_label_new("No waveform opened yet -- use Open\xe2\x80\xa6");
|
||||
gtk_widget_add_css_class(empty, "dim-label");
|
||||
gtk_widget_set_margin_top(empty, 24);
|
||||
gtk_widget_set_margin_bottom(empty, 24);
|
||||
gtk_list_box_append(GTK_LIST_BOX(c.wave_list), empty);
|
||||
gtk_list_box_set_selection_mode(GTK_LIST_BOX(c.wave_list), GTK_SELECTION_NONE);
|
||||
return;
|
||||
}
|
||||
gtk_list_box_set_selection_mode(GTK_LIST_BOX(c.wave_list), GTK_SELECTION_SINGLE);
|
||||
for (std::size_t i = 0; i < c.waves.size(); ++i) {
|
||||
const std::string& path = c.waves[i];
|
||||
GtkWidget* row = gtk_list_box_row_new();
|
||||
g_object_set_data(G_OBJECT(row), "wave-index", GINT_TO_POINTER(static_cast<int>(i)));
|
||||
|
||||
GtkWidget* line = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
|
||||
gtk_widget_set_margin_start(line, 12);
|
||||
gtk_widget_set_margin_end(line, 6);
|
||||
gtk_widget_set_margin_top(line, 6);
|
||||
gtk_widget_set_margin_bottom(line, 6);
|
||||
GtkWidget* text = gtk_box_new(GTK_ORIENTATION_VERTICAL, 2);
|
||||
gtk_widget_set_hexpand(text, TRUE);
|
||||
const std::size_t slash = path.rfind('/');
|
||||
const std::string base = slash == std::string::npos ? path : path.substr(slash + 1);
|
||||
GtkWidget* title = gtk_label_new(base.c_str());
|
||||
gtk_widget_set_halign(title, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(text), title);
|
||||
const bool present = ::access(path.c_str(), R_OK) == 0;
|
||||
std::string sub = present ? path : path + " (missing)";
|
||||
GtkWidget* under = gtk_label_new(sub.c_str());
|
||||
gtk_widget_add_css_class(under, "dim-label");
|
||||
gtk_widget_set_halign(under, GTK_ALIGN_START);
|
||||
gtk_label_set_ellipsize(GTK_LABEL(under), PANGO_ELLIPSIZE_MIDDLE);
|
||||
gtk_box_append(GTK_BOX(text), under);
|
||||
gtk_box_append(GTK_BOX(line), text);
|
||||
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), line);
|
||||
gtk_list_box_append(GTK_LIST_BOX(c.wave_list), row);
|
||||
}
|
||||
}
|
||||
|
||||
void on_wave_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) {
|
||||
Chooser& c = *static_cast<Chooser*>(user);
|
||||
if (row == nullptr) return;
|
||||
const int idx = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(row), "wave-index"));
|
||||
if (idx < 0 || static_cast<std::size_t>(idx) >= c.waves.size()) return;
|
||||
const std::string& path = c.waves[static_cast<std::size_t>(idx)];
|
||||
if (::access(path.c_str(), R_OK) != 0) {
|
||||
std::fprintf(stderr, "[sources] waveform is gone: %s\n", path.c_str());
|
||||
std::fflush(stderr);
|
||||
return;
|
||||
}
|
||||
// The file *is* the source: the chooser's answer is a path, not a socket.
|
||||
c.cfg->open_waveform = path;
|
||||
report_pick(c);
|
||||
chooser_finish(c, true);
|
||||
}
|
||||
|
||||
void on_wave_open_done(GObject* source, GAsyncResult* result, gpointer user) {
|
||||
Chooser& c = *static_cast<Chooser*>(user);
|
||||
GFile* file = gtk_file_dialog_open_finish(GTK_FILE_DIALOG(source), result, nullptr);
|
||||
if (file == nullptr) return; // cancelled
|
||||
char* path = g_file_get_path(file);
|
||||
g_object_unref(file);
|
||||
if (path != nullptr) {
|
||||
c.cfg->open_waveform = path;
|
||||
g_free(path);
|
||||
report_pick(c);
|
||||
chooser_finish(c, true);
|
||||
}
|
||||
}
|
||||
|
||||
void on_wave_open_clicked(GtkButton*, gpointer user) {
|
||||
Chooser& c = *static_cast<Chooser*>(user);
|
||||
GtkFileDialog* dialog = gtk_file_dialog_new();
|
||||
gtk_file_dialog_set_title(dialog, "Open an exported waveform");
|
||||
GtkFileFilter* filter = gtk_file_filter_new();
|
||||
gtk_file_filter_set_name(filter, "NumPy export (*.npy)");
|
||||
gtk_file_filter_add_pattern(filter, "*.npy");
|
||||
GListStore* filters = g_list_store_new(GTK_TYPE_FILE_FILTER);
|
||||
g_list_store_append(filters, filter);
|
||||
gtk_file_dialog_set_filters(dialog, G_LIST_MODEL(filters));
|
||||
gtk_file_dialog_open(dialog, GTK_WINDOW(c.window), nullptr, on_wave_open_done, &c);
|
||||
g_object_unref(filters);
|
||||
g_object_unref(filter);
|
||||
g_object_unref(dialog);
|
||||
}
|
||||
|
||||
void on_edit_clicked(GtkButton* button, gpointer user) {
|
||||
Chooser& c = *static_cast<Chooser*>(user);
|
||||
const int idx = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "source-index"));
|
||||
if (idx < 0 || static_cast<std::size_t>(idx) >= c.known.size()) return;
|
||||
open_form(c, idx);
|
||||
}
|
||||
|
||||
void on_forget_clicked(GtkButton* button, gpointer user) {
|
||||
Chooser& c = *static_cast<Chooser*>(user);
|
||||
const int idx = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), "source-index"));
|
||||
@@ -145,13 +290,29 @@ void chooser_populate(Chooser& c) {
|
||||
// The *server*, even for a multicast session: that is the host the file is named after and
|
||||
// the one CONFIG is fetched from. Titling the row by the group would name it after something
|
||||
// that is not its identity.
|
||||
const std::string title = s.host + ":" + std::to_string(s.port);
|
||||
const std::string title = s.host + ":" + std::to_string(s.port) + " \u00b7 " + s.config;
|
||||
GtkWidget* title_label = gtk_label_new(title.c_str());
|
||||
gtk_widget_set_halign(title_label, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(text), title_label);
|
||||
|
||||
// The note first when there is one: it is what tells two configurations of one source apart,
|
||||
// and a list that shows only counts makes the user open them to find out.
|
||||
if (!s.comment.empty()) {
|
||||
GtkWidget* note = gtk_label_new(s.comment.c_str());
|
||||
gtk_widget_set_halign(note, GTK_ALIGN_START);
|
||||
gtk_label_set_ellipsize(GTK_LABEL(note), PANGO_ELLIPSIZE_END);
|
||||
gtk_box_append(GTK_BOX(text), note);
|
||||
}
|
||||
|
||||
std::string sub = std::to_string(s.rows) + (s.rows == 1 ? " plot, " : " plots, ") +
|
||||
std::to_string(s.signals) + (s.signals == 1 ? " signal" : " signals");
|
||||
if (s.buffer_mb > 0.0) {
|
||||
sub += ", buffer " + std::to_string(static_cast<int>(s.buffer_mb)) + " MB";
|
||||
}
|
||||
if (s.relative_time) sub += ", relative time";
|
||||
if (s.capture_packets > 0) {
|
||||
sub += ", capture " + std::to_string(s.capture_packets) + " packets";
|
||||
}
|
||||
if (!s.multicast.empty()) sub += ", multicast " + s.multicast;
|
||||
GtkWidget* sub_label = gtk_label_new(sub.c_str());
|
||||
gtk_widget_add_css_class(sub_label, "dim-label");
|
||||
@@ -159,8 +320,18 @@ void chooser_populate(Chooser& c) {
|
||||
gtk_box_append(GTK_BOX(text), sub_label);
|
||||
gtk_box_append(GTK_BOX(line), text);
|
||||
|
||||
GtkWidget* edit = gtk_button_new_from_icon_name("document-edit-symbolic");
|
||||
gtk_widget_set_tooltip_text(edit,
|
||||
"Edit this configuration: its name and note, the buffer size and "
|
||||
"the clock, or the address it points at");
|
||||
gtk_widget_add_css_class(edit, "flat");
|
||||
gtk_widget_set_valign(edit, GTK_ALIGN_CENTER);
|
||||
g_object_set_data(G_OBJECT(edit), "source-index", GINT_TO_POINTER(static_cast<int>(i)));
|
||||
g_signal_connect(edit, "clicked", G_CALLBACK(on_edit_clicked), &c);
|
||||
gtk_box_append(GTK_BOX(line), edit);
|
||||
|
||||
GtkWidget* forget = gtk_button_new_from_icon_name("window-close-symbolic");
|
||||
gtk_widget_set_tooltip_text(forget, "Forget this source (deletes its saved session)");
|
||||
gtk_widget_set_tooltip_text(forget, "Forget this configuration (deletes its saved session)");
|
||||
gtk_widget_add_css_class(forget, "flat");
|
||||
gtk_widget_set_valign(forget, GTK_ALIGN_CENTER);
|
||||
g_object_set_data(G_OBJECT(forget), "source-index", GINT_TO_POINTER(static_cast<int>(i)));
|
||||
@@ -193,11 +364,58 @@ void on_form_connect(GtkButton*, gpointer user) {
|
||||
Chooser& c = *static_cast<Chooser*>(user);
|
||||
const char* server = gtk_editable_get_text(GTK_EDITABLE(c.form_server));
|
||||
if (server == nullptr || server[0] == '\0') return;
|
||||
c.cfg->server = server;
|
||||
c.cfg->port = static_cast<std::uint16_t>(
|
||||
const std::string host = server;
|
||||
const std::uint16_t port = static_cast<std::uint16_t>(
|
||||
gtk_spin_button_get_value_as_int(GTK_SPIN_BUTTON(c.form_port)));
|
||||
c.cfg->multicast = gtk_editable_get_text(GTK_EDITABLE(c.form_group));
|
||||
c.cfg->interface_addr = gtk_editable_get_text(GTK_EDITABLE(c.form_iface));
|
||||
const std::string group = gtk_editable_get_text(GTK_EDITABLE(c.form_group));
|
||||
const std::string iface = gtk_editable_get_text(GTK_EDITABLE(c.form_iface));
|
||||
std::string name = gtk_editable_get_text(GTK_EDITABLE(c.form_name));
|
||||
const std::string note = gtk_editable_get_text(GTK_EDITABLE(c.form_note));
|
||||
const double buffer_mb = gtk_spin_button_get_value(GTK_SPIN_BUTTON(c.form_buffer));
|
||||
const bool relative = gtk_switch_get_active(GTK_SWITCH(c.form_relative));
|
||||
if (name.empty()) name = "default";
|
||||
|
||||
c.cfg->server = host;
|
||||
c.cfg->port = port;
|
||||
c.cfg->multicast = group;
|
||||
c.cfg->interface_addr = iface;
|
||||
// The startup settings. Applied here because this runs *before* `ScopeModel` exists, which is the
|
||||
// only moment a buffer size can be applied at all -- and stored, so the next run gets them back.
|
||||
c.cfg->buffer_mb = buffer_mb;
|
||||
c.cfg->relative_time = relative;
|
||||
|
||||
LayoutState st;
|
||||
if (!c.edit_config.empty()) {
|
||||
// An edit keeps everything the configuration already holds -- plots, calibration, math,
|
||||
// measurements -- and changes only what the form shows.
|
||||
load_layout(configuration_path(c.edit_host, c.edit_port, c.edit_config), st);
|
||||
}
|
||||
st.comment = note;
|
||||
st.buffer_mb = buffer_mb;
|
||||
st.relative_time = relative;
|
||||
st.multicast = group;
|
||||
st.interface_addr = iface;
|
||||
|
||||
const bool editing = !c.edit_config.empty();
|
||||
const bool moved =
|
||||
editing && (c.edit_host != host || c.edit_port != port || c.edit_config != name);
|
||||
if (save_layout(configuration_path(host, port, name), st)) {
|
||||
set_active_configuration(host, port, name);
|
||||
// A renamed configuration, or one re-addressed, is a *move*: the old file has no reason to exist
|
||||
// and leaving it would list the same session twice.
|
||||
if (moved) forget_configuration(c.edit_host, c.edit_port, c.edit_config);
|
||||
c.saved_config = name;
|
||||
c.saved_note = note;
|
||||
std::fprintf(stderr,
|
||||
"[sources] configuration %s: %s:%u/%s note='%s' buffer=%.6g relative=%d\n",
|
||||
editing ? "saved" : "created", host.c_str(), port, name.c_str(), note.c_str(),
|
||||
buffer_mb, relative ? 1 : 0);
|
||||
std::fflush(stderr);
|
||||
} else {
|
||||
std::fprintf(stderr, "[sources] configuration write failed: %s:%u/%s\n", host.c_str(), port,
|
||||
name.c_str());
|
||||
std::fflush(stderr);
|
||||
}
|
||||
form_close(c);
|
||||
chooser_finish(c, true);
|
||||
}
|
||||
@@ -212,13 +430,24 @@ GtkWidget* form_field(GtkWidget* grid, int row, const char* label, GtkWidget* wi
|
||||
return widget;
|
||||
}
|
||||
|
||||
void open_form(Chooser& c) {
|
||||
/// Opens the connection form: on a new host, or on an existing configuration (`edit_index`). One
|
||||
/// form for both, because the fields are the same and a configuration *is* a host plus its
|
||||
/// settings -- the only difference is where the values start and what the button is called.
|
||||
void open_form(Chooser& c, int edit_index = -1) {
|
||||
if (c.form != nullptr) {
|
||||
gtk_window_present(GTK_WINDOW(c.form));
|
||||
return;
|
||||
}
|
||||
const KnownSource* src =
|
||||
edit_index >= 0 && static_cast<std::size_t>(edit_index) < c.known.size()
|
||||
? &c.known[static_cast<std::size_t>(edit_index)]
|
||||
: nullptr;
|
||||
c.edit_host = src != nullptr ? src->host : std::string();
|
||||
c.edit_port = src != nullptr ? src->port : 0;
|
||||
c.edit_config = src != nullptr ? src->config : std::string();
|
||||
|
||||
c.form = gtk_window_new();
|
||||
gtk_window_set_title(GTK_WINDOW(c.form), "New host");
|
||||
gtk_window_set_title(GTK_WINDOW(c.form), src != nullptr ? "Edit configuration" : "New host");
|
||||
gtk_window_set_transient_for(GTK_WINDOW(c.form), GTK_WINDOW(c.window));
|
||||
gtk_window_set_modal(GTK_WINDOW(c.form), TRUE);
|
||||
|
||||
@@ -235,15 +464,52 @@ void open_form(Chooser& c) {
|
||||
gtk_box_append(GTK_BOX(box), grid);
|
||||
|
||||
c.form_server = form_field(grid, 0, "Server", gtk_entry_new());
|
||||
gtk_editable_set_text(GTK_EDITABLE(c.form_server), "127.0.0.1");
|
||||
gtk_editable_set_text(GTK_EDITABLE(c.form_server),
|
||||
src != nullptr ? src->host.c_str() : "127.0.0.1");
|
||||
// A spin button holds no decimals by default, which is right for a port and wrong for every
|
||||
// other numeric field in this project -- see the math dialog's fields.
|
||||
c.form_port = form_field(grid, 1, "Port", gtk_spin_button_new_with_range(1, 65535, 1));
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(c.form_port), 8000);
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(c.form_port), src != nullptr ? src->port : 8000);
|
||||
c.form_group = form_field(grid, 2, "Multicast group", gtk_entry_new());
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(c.form_group), "empty = unicast");
|
||||
gtk_editable_set_text(GTK_EDITABLE(c.form_group),
|
||||
src != nullptr ? src->multicast.c_str() : "");
|
||||
c.form_iface = form_field(grid, 3, "Interface", gtk_entry_new());
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(c.form_iface), "empty = default route");
|
||||
gtk_editable_set_text(GTK_EDITABLE(c.form_iface),
|
||||
src != nullptr ? src->interface_addr.c_str() : "");
|
||||
|
||||
// The session half: what this source's configuration is called, and the settings that have to be
|
||||
// decided before the first socket is opened.
|
||||
GtkWidget* heading = gtk_label_new(nullptr);
|
||||
gtk_label_set_markup(GTK_LABEL(heading), "<b>Configuration</b>");
|
||||
gtk_widget_set_halign(heading, GTK_ALIGN_START);
|
||||
gtk_widget_set_margin_top(heading, 8);
|
||||
gtk_grid_attach(GTK_GRID(grid), heading, 0, 4, 2, 1);
|
||||
|
||||
c.form_name = form_field(grid, 5, "Name", gtk_entry_new());
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(c.form_name), "default");
|
||||
gtk_editable_set_text(GTK_EDITABLE(c.form_name),
|
||||
src != nullptr ? src->config.c_str() : "default");
|
||||
c.form_note = form_field(grid, 6, "Note", gtk_entry_new());
|
||||
gtk_entry_set_placeholder_text(GTK_ENTRY(c.form_note), "what this configuration is for");
|
||||
gtk_editable_set_text(GTK_EDITABLE(c.form_note),
|
||||
src != nullptr ? src->comment.c_str() : "");
|
||||
c.form_buffer = form_field(grid, 7, "Buffer MB", gtk_spin_button_new_with_range(1, 65536, 1));
|
||||
gtk_widget_set_tooltip_text(c.form_buffer,
|
||||
"sample buffer per source, applied when this source is opened");
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(c.form_buffer),
|
||||
src != nullptr && src->buffer_mb > 0.0
|
||||
? src->buffer_mb
|
||||
: std::max(1.0, c.cfg->buffer_mb));
|
||||
GtkWidget* relative = gtk_switch_new();
|
||||
gtk_widget_set_halign(relative, GTK_ALIGN_START);
|
||||
gtk_widget_set_tooltip_text(relative,
|
||||
"plot time from the packet counter and the declared rate, instead "
|
||||
"of the stream's own time signal (--relative-time)");
|
||||
gtk_switch_set_active(GTK_SWITCH(relative),
|
||||
src != nullptr ? src->relative_time : c.cfg->relative_time);
|
||||
c.form_relative = form_field(grid, 8, "Relative time", relative);
|
||||
|
||||
GtkWidget* actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
gtk_widget_set_halign(actions, GTK_ALIGN_END);
|
||||
@@ -252,7 +518,7 @@ void open_form(Chooser& c) {
|
||||
form_close(*static_cast<Chooser*>(user));
|
||||
}),
|
||||
&c);
|
||||
c.form_connect = gtk_button_new_with_label("Connect");
|
||||
c.form_connect = gtk_button_new_with_label(src != nullptr ? "Save" : "Connect");
|
||||
g_signal_connect(c.form_connect, "clicked", G_CALLBACK(on_form_connect), &c);
|
||||
gtk_box_append(GTK_BOX(actions), cancel);
|
||||
gtk_box_append(GTK_BOX(actions), c.form_connect);
|
||||
@@ -274,8 +540,26 @@ void open_form(Chooser& c) {
|
||||
/// What the pick probes report: the connection the chooser came back with. One helper rather than
|
||||
/// the same four lines in each branch, since both probes assert against this exact line.
|
||||
void report_pick(const Chooser& c) {
|
||||
std::fprintf(stderr, "[sources] picked %s:%u multicast=%s interface=%s\n", c.cfg->server.c_str(),
|
||||
c.cfg->port, c.cfg->multicast.c_str(), c.cfg->interface_addr.c_str());
|
||||
if (!c.cfg->open_waveform.empty()) {
|
||||
std::fprintf(stderr, "[sources] picked waveform %s\n", c.cfg->open_waveform.c_str());
|
||||
std::fflush(stderr);
|
||||
return;
|
||||
}
|
||||
// The settings the configuration carried, not just the address: a pick that dropped the buffer
|
||||
// size would look identical here otherwise, and the buffer is the one of them a user notices
|
||||
// only after a long capture has been sized wrong.
|
||||
std::fprintf(stderr,
|
||||
"[sources] picked %s:%u multicast=%s interface=%s buffer=%.6g relative=%d "
|
||||
"capture=%u config=%s note='%s'\n",
|
||||
c.cfg->server.c_str(), c.cfg->port, c.cfg->multicast.c_str(),
|
||||
c.cfg->interface_addr.c_str(), c.cfg->buffer_mb, c.cfg->relative_time ? 1 : 0,
|
||||
c.cfg->capture_packets,
|
||||
(c.saved_config.empty() ? (c.known.empty() ? "-" : c.known.front().config)
|
||||
: c.saved_config)
|
||||
.c_str(),
|
||||
(c.saved_config.empty() ? (c.known.empty() ? "-" : c.known.front().comment)
|
||||
: c.saved_note)
|
||||
.c_str());
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
@@ -304,7 +588,28 @@ gboolean pick_probe(gpointer user) {
|
||||
// -probe call would otherwise be a GLib critical on every run that let the probe fire.
|
||||
c.pick_source = 0;
|
||||
|
||||
// `UDPSCOPE_PROBE_NEWHOST=host:port` drives the form instead of a row.
|
||||
// `UDPSCOPE_PROBE_WAVEFORMS=1` is the waveforms page without a display: the recent list, and the
|
||||
// page selected the way a click on its tab does. `UDPSCOPE_PROBE_WAVE=<path>` is the file dialog's
|
||||
// answer without the dialog -- the same field, set the same way.
|
||||
if (g_getenv("UDPSCOPE_PROBE_WAVEFORMS") != nullptr) {
|
||||
if (c.notebook != nullptr) gtk_notebook_set_current_page(GTK_NOTEBOOK(c.notebook), 1);
|
||||
std::fprintf(stderr, "[sources] waveforms: %zu recent", c.waves.size());
|
||||
for (const std::string& p : c.waves) {
|
||||
std::fprintf(stderr, " %s(%s)", p.c_str(), ::access(p.c_str(), R_OK) == 0 ? "ok" : "missing");
|
||||
}
|
||||
std::fprintf(stderr, " page=%d\n",
|
||||
static_cast<int>(gtk_notebook_get_current_page(GTK_NOTEBOOK(c.notebook))));
|
||||
std::fflush(stderr);
|
||||
}
|
||||
if (const char* path = g_getenv("UDPSCOPE_PROBE_WAVE")) {
|
||||
c.cfg->open_waveform = path;
|
||||
report_pick(c);
|
||||
chooser_finish(c, true);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
// `UDPSCOPE_PROBE_NEWHOST=host:port` drives the form instead of a row: the address, and the four
|
||||
// things a host could not be given before -- a name, a note, a buffer size and a clock.
|
||||
if (const char* spec = g_getenv("UDPSCOPE_PROBE_NEWHOST")) {
|
||||
open_form(c);
|
||||
const std::string s = spec;
|
||||
@@ -314,13 +619,63 @@ gboolean pick_probe(gpointer user) {
|
||||
if (colon != std::string::npos) {
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(c.form_port), std::atof(s.c_str() + colon + 1));
|
||||
}
|
||||
std::fprintf(stderr, "[sources] form open, connect_sensitive=%d\n",
|
||||
gtk_editable_set_text(GTK_EDITABLE(c.form_name), "probe-host");
|
||||
gtk_editable_set_text(GTK_EDITABLE(c.form_note), "made by the new-host probe");
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(c.form_buffer), 32);
|
||||
gtk_switch_set_active(GTK_SWITCH(c.form_relative), TRUE);
|
||||
std::fprintf(stderr,
|
||||
"[sources] form: name='%s' note='%s' buffer=%.6g relative=%d "
|
||||
"connect_sensitive=%d\n",
|
||||
gtk_editable_get_text(GTK_EDITABLE(c.form_name)),
|
||||
gtk_editable_get_text(GTK_EDITABLE(c.form_note)),
|
||||
gtk_spin_button_get_value(GTK_SPIN_BUTTON(c.form_buffer)),
|
||||
gtk_switch_get_active(GTK_SWITCH(c.form_relative)) ? 1 : 0,
|
||||
gtk_widget_get_sensitive(c.form_connect) ? 1 : 0);
|
||||
g_signal_emit_by_name(c.form_connect, "clicked");
|
||||
report_pick(c);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
// `UDPSCOPE_PROBE_EDITHOST=host:port` opens the existing configuration's form, changes its
|
||||
// settings and saves: the path a user reaches by clicking the row's pencil.
|
||||
if (const char* spec = g_getenv("UDPSCOPE_PROBE_EDITHOST")) {
|
||||
const std::string want = spec;
|
||||
int idx = -1;
|
||||
for (std::size_t i = 0; i < c.known.size(); ++i) {
|
||||
if (c.known[i].host + ":" + std::to_string(c.known[i].port) == want) {
|
||||
idx = static_cast<int>(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (idx < 0) {
|
||||
std::fprintf(stderr, "[sources] edit: no configuration for '%s'\n", want.c_str());
|
||||
std::fflush(stderr);
|
||||
chooser_finish(c, false);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
open_form(c, idx);
|
||||
// The reading is the *pre-fill*: an edit form that opens empty is the defect this asserts
|
||||
// against, and the button's label says which mode it is in.
|
||||
std::fprintf(stderr,
|
||||
"[sources] edit form: name='%s' note='%s' buffer=%.6g relative=%d button='%s'\n",
|
||||
gtk_editable_get_text(GTK_EDITABLE(c.form_name)),
|
||||
gtk_editable_get_text(GTK_EDITABLE(c.form_note)),
|
||||
gtk_spin_button_get_value(GTK_SPIN_BUTTON(c.form_buffer)),
|
||||
gtk_switch_get_active(GTK_SWITCH(c.form_relative)) ? 1 : 0,
|
||||
gtk_button_get_label(GTK_BUTTON(c.form_connect)));
|
||||
gtk_editable_set_text(GTK_EDITABLE(c.form_note), "edited by the probe");
|
||||
gtk_spin_button_set_value(GTK_SPIN_BUTTON(c.form_buffer), 16);
|
||||
gtk_switch_set_active(GTK_SWITCH(c.form_relative), FALSE);
|
||||
// `UDPSCOPE_PROBE_EDITNAME=new-name` also renames it, which is a *move*: the old file must not
|
||||
// survive, or the chooser lists the same session twice.
|
||||
if (const char* rename_to = g_getenv("UDPSCOPE_PROBE_EDITNAME")) {
|
||||
gtk_editable_set_text(GTK_EDITABLE(c.form_name), rename_to);
|
||||
}
|
||||
g_signal_emit_by_name(c.form_connect, "clicked");
|
||||
report_pick(c);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
// Check the source list rather than the row count: `gtk_list_box_append` auto-wraps any
|
||||
// non-row widget in a `GtkListBoxRow`, so `gtk_list_box_get_row_at_index` returns non-null
|
||||
// even when the only list child is the empty-state placeholder label.
|
||||
@@ -341,6 +696,27 @@ gboolean pick_probe(gpointer user) {
|
||||
|
||||
} // namespace
|
||||
|
||||
void apply_stored_settings(Config& cfg) {
|
||||
const std::string want = active_configuration(cfg.server, cfg.port);
|
||||
if (want.empty()) return;
|
||||
LayoutState st;
|
||||
if (!load_layout(configuration_path(cfg.server, cfg.port, want), st)) return;
|
||||
// Only where the command line was silent: a flag the user typed wins, because it is the more
|
||||
// recent statement of intent -- and the file is not a place to hide a surprise from the ones who
|
||||
// did type it.
|
||||
if (!cfg.buffer_mb_specified && st.buffer_mb > 0.0) cfg.buffer_mb = st.buffer_mb;
|
||||
if (st.relative_time) cfg.relative_time = true;
|
||||
if (cfg.capture_packets == 0 && st.capture_packets > 0) cfg.capture_packets = st.capture_packets;
|
||||
const bool applied = (!cfg.buffer_mb_specified && st.buffer_mb > 0.0) || st.relative_time ||
|
||||
st.capture_packets > 0;
|
||||
if (applied) {
|
||||
std::fprintf(stderr,
|
||||
"[sources] settings from configuration '%s': buffer=%.6g relative=%d capture=%u\n",
|
||||
want.c_str(), cfg.buffer_mb, cfg.relative_time ? 1 : 0, cfg.capture_packets);
|
||||
std::fflush(stderr);
|
||||
}
|
||||
}
|
||||
|
||||
bool choose_source(Config& cfg) {
|
||||
const std::vector<KnownSource> known = known_sources();
|
||||
|
||||
@@ -366,6 +742,13 @@ bool choose_source(Config& cfg) {
|
||||
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_window_set_child(GTK_WINDOW(c.window), box);
|
||||
|
||||
// Two pages under one window: the sources it can connect to, and the waveforms it can open. A
|
||||
// waveform is not a source with a file in place of an address -- there is no socket and nothing
|
||||
// arrives -- so it is a *page* rather than a field, which is also where its history belongs.
|
||||
c.notebook = gtk_notebook_new();
|
||||
gtk_widget_set_vexpand(c.notebook, TRUE);
|
||||
gtk_box_append(GTK_BOX(box), c.notebook);
|
||||
|
||||
GtkWidget* scroller = gtk_scrolled_window_new();
|
||||
gtk_widget_set_vexpand(scroller, TRUE);
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(scroller), GTK_POLICY_NEVER,
|
||||
@@ -374,7 +757,30 @@ bool choose_source(Config& cfg) {
|
||||
gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(c.list), TRUE);
|
||||
g_signal_connect(c.list, "row-activated", G_CALLBACK(on_row_activated), &c);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scroller), c.list);
|
||||
gtk_box_append(GTK_BOX(box), scroller);
|
||||
gtk_notebook_append_page(GTK_NOTEBOOK(c.notebook), scroller, gtk_label_new("Sources"));
|
||||
|
||||
GtkWidget* wave_page = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
GtkWidget* wave_scroller = gtk_scrolled_window_new();
|
||||
gtk_widget_set_vexpand(wave_scroller, TRUE);
|
||||
gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(wave_scroller), GTK_POLICY_NEVER,
|
||||
GTK_POLICY_AUTOMATIC);
|
||||
c.wave_list = gtk_list_box_new();
|
||||
gtk_list_box_set_activate_on_single_click(GTK_LIST_BOX(c.wave_list), TRUE);
|
||||
g_signal_connect(c.wave_list, "row-activated", G_CALLBACK(on_wave_activated), &c);
|
||||
gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(wave_scroller), c.wave_list);
|
||||
gtk_box_append(GTK_BOX(wave_page), wave_scroller);
|
||||
GtkWidget* wave_actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
gtk_widget_set_halign(wave_actions, GTK_ALIGN_END);
|
||||
gtk_widget_set_margin_top(wave_actions, 6);
|
||||
gtk_widget_set_margin_bottom(wave_actions, 6);
|
||||
gtk_widget_set_margin_end(wave_actions, 12);
|
||||
GtkWidget* wave_open = gtk_button_new_with_label("Open\xe2\x80\xa6");
|
||||
gtk_widget_set_tooltip_text(wave_open, "choose an exported .npy to view and analyse");
|
||||
g_signal_connect(wave_open, "clicked", G_CALLBACK(on_wave_open_clicked), &c);
|
||||
gtk_box_append(GTK_BOX(wave_actions), wave_open);
|
||||
gtk_box_append(GTK_BOX(wave_page), wave_actions);
|
||||
gtk_notebook_append_page(GTK_NOTEBOOK(c.notebook), wave_page, gtk_label_new("Waveforms"));
|
||||
chooser_populate_waves(c);
|
||||
|
||||
GtkWidget* actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
gtk_widget_set_margin_start(actions, 12);
|
||||
|
||||
Reference in new issue
Block a user