udpscope: an oscilloscope view for a UDPStreamer source
GTK4 and libadwaita. It receives one UDP stream through the vendored udpsclient, buffers it in a large ring, and draws it with the vendored plotter: a trigger with a pre/post event window, a pause that freezes the window, cursors, measurements, math channels in a node editor, an event cache, named configurations, preferences, and an export of the visible window to .npy/.csv. Waveform files can be opened and analysed in place. The interface is two threads -- a producer that owns the socket and the GTK main thread that owns every widget and the GL context -- sharing one model under one mutex, with samples crossing through lock-free rings. deps/plotter is a submodule pinned to the commit this tree builds against; deps/udpsclient is vendored in place. docs/claude/ documents the rules that cost a defect to learn, and SPECS.md is the requirement document.
This commit is contained in:
1 parent
8f54841c2c
commit
0e256764d5
41 files changed
+2081
-154
No files matched your search
+31
-55
@@ -16,12 +16,16 @@ namespace {
|
||||
/// Nulled by each window's own close handler, so a second open cannot present a dead widget.
|
||||
/// One pointer per window, file-scope because there is one application.
|
||||
///
|
||||
/// The help window is a `GtkWindow`. The other two are `AdwDialog`s, which are **not**
|
||||
/// `GtkWindow`s: they have no `::close-request`, so they clear on `::closed` and close with
|
||||
/// `adw_dialog_close()`.
|
||||
/// The help window and the shortcut overview are `GtkWindow`s. The About dialog is an
|
||||
/// `AdwDialog`, which is **not** a `GtkWindow`: it has no `::close-request`, so it clears on
|
||||
/// `::closed` and closes with `adw_dialog_close()`.
|
||||
GtkWidget* g_help = nullptr;
|
||||
AdwDialog* g_about = nullptr;
|
||||
AdwDialog* g_shortcuts = nullptr;
|
||||
/// The overview is the one window here built from a `.ui` file, so its builder is held too: the window
|
||||
/// belongs to the builder, and a *destroyed* `GtkWindow` cannot be presented again -- which is why the
|
||||
/// builder is replaced on the next opening rather than reused.
|
||||
GtkWidget* g_shortcuts = nullptr;
|
||||
GtkBuilder* g_shortcuts_ui = nullptr;
|
||||
|
||||
gboolean on_window_close(GtkWindow* window, gpointer slot) {
|
||||
(void)window;
|
||||
@@ -285,67 +289,39 @@ void show_help(GtkWindow* parent) {
|
||||
gtk_window_present(GTK_WINDOW(window));
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
/// Appends one table row to `dialog`, opening a new section whenever the row's `section`
|
||||
/// string differs from the last one. `*current` is the section being filled, `*title` the
|
||||
/// name it carries; both are owned by the caller so the two tables share one run.
|
||||
///
|
||||
/// `accel` is handed to `AdwShortcutsItem` as an accelerator when it is non-empty and
|
||||
/// `subtitle` is used when it is not, which is how a wheel or a drag gets a row: neither
|
||||
/// GTK's nor libadwaita's accelerator syntax can name one, and asking them to parse
|
||||
/// "Wheel" costs a parse warning and draws nothing.
|
||||
void append_shortcut(AdwShortcutsDialog* dialog, AdwShortcutsSection** current,
|
||||
const char** title, const char* section, const char* accel,
|
||||
const char* subtitle, const char* description) {
|
||||
if (*current == nullptr || std::string(*title) != section) {
|
||||
*current = adw_shortcuts_section_new(section);
|
||||
*title = section;
|
||||
adw_shortcuts_dialog_add(dialog, *current);
|
||||
}
|
||||
AdwShortcutsItem* item = adw_shortcuts_item_new(description, accel);
|
||||
if (subtitle != nullptr && *subtitle != '\0') {
|
||||
adw_shortcuts_item_set_subtitle(item, subtitle);
|
||||
}
|
||||
adw_shortcuts_section_add(*current, item);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void show_shortcuts(GtkWindow* parent) {
|
||||
if (g_shortcuts != nullptr) {
|
||||
adw_dialog_present(g_shortcuts, GTK_WIDGET(parent));
|
||||
gtk_window_present(GTK_WINDOW(g_shortcuts));
|
||||
return;
|
||||
}
|
||||
|
||||
AdwDialog* dialog = adw_shortcuts_dialog_new();
|
||||
g_shortcuts = dialog;
|
||||
adw_dialog_set_title(dialog, "Keyboard and mouse shortcuts");
|
||||
|
||||
AdwShortcutsSection* section = nullptr;
|
||||
const char* section_title = nullptr;
|
||||
int accel_count = 0;
|
||||
const Accel* accels = app_accels(&accel_count);
|
||||
for (int i = 0; i < accel_count; ++i) {
|
||||
append_shortcut(ADW_SHORTCUTS_DIALOG(dialog), §ion, §ion_title,
|
||||
accels[i].section, accels[i].accel, "", accels[i].description);
|
||||
}
|
||||
int gesture_count = 0;
|
||||
const Gesture* gestures = plot_gestures(&gesture_count);
|
||||
for (int i = 0; i < gesture_count; ++i) {
|
||||
append_shortcut(ADW_SHORTCUTS_DIALOG(dialog), §ion, §ion_title,
|
||||
gestures[i].section, gestures[i].accel, gestures[i].subtitle,
|
||||
gestures[i].description);
|
||||
}
|
||||
|
||||
g_signal_connect(dialog, "closed", G_CALLBACK(on_dialog_closed), &g_shortcuts);
|
||||
adw_dialog_present(dialog, GTK_WIDGET(parent));
|
||||
// A `GtkShortcutsWindow` from `src/ui/shortcuts.ui` -- gedit's arrangement, and GTK's own: one
|
||||
// section per page, with the sidebar and the search box drawn by the window itself.
|
||||
//
|
||||
// A UI file rather than widget construction because of the mouse rows. An accelerator cannot spell
|
||||
// "Ctrl and drag", and libadwaita's `AdwShortcutsItem` -- the modern replacement for this widget --
|
||||
// has no icon property at all, so its cell reads "No Shortcut". `GtkShortcutsShortcut`'s one generic
|
||||
// gesture type draws the `icon` the row names, which is what puts a mouse glyph where the keycaps
|
||||
// would be. (The widget is deprecated since 4.20, but this uses none of its functions: the file is
|
||||
// loaded as data and presented as a `GtkWindow`, so the build stays at its four warnings.)
|
||||
//
|
||||
// A builder per opening, not one reused: the window belongs to the builder and `close-request`
|
||||
// destroys the window, so the resource is parsed again rather than an already-dead window fetched.
|
||||
g_clear_object(&g_shortcuts_ui);
|
||||
g_shortcuts_ui = gtk_builder_new_from_resource("/org/udpscope/ui/shortcuts.ui");
|
||||
if (g_shortcuts_ui == nullptr) return;
|
||||
GObject* overlay = gtk_builder_get_object(g_shortcuts_ui, "help_overlay");
|
||||
if (overlay == nullptr) return;
|
||||
g_shortcuts = GTK_WIDGET(overlay);
|
||||
gtk_window_set_transient_for(GTK_WINDOW(g_shortcuts), parent);
|
||||
g_signal_connect(g_shortcuts, "close-request", G_CALLBACK(on_window_close), &g_shortcuts);
|
||||
gtk_window_present(GTK_WINDOW(g_shortcuts));
|
||||
}
|
||||
|
||||
void close_help_windows() {
|
||||
if (g_help != nullptr) gtk_window_close(GTK_WINDOW(g_help));
|
||||
if (g_about != nullptr) adw_dialog_close(g_about);
|
||||
if (g_shortcuts != nullptr) adw_dialog_close(g_shortcuts);
|
||||
if (g_shortcuts != nullptr) gtk_window_close(GTK_WINDOW(g_shortcuts));
|
||||
}
|
||||
|
||||
} // namespace udpscope
|
||||
Reference in new issue
Block a user