feat: About dialog with version, commit, build time and settings path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino FerrariandClaude Sonnet 4.6 committed 2026-10-04 11:52:43 +02:00
1 parent 82f91d00c0
commit a0fa845b58
8 files changed
+170 -9

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
// SPDX-License-Identifier: MIT
#pragma once
namespace udpscope::build_info {
/// The CMake project version, e.g. "0.1.0".
extern const char* const kVersion;
/// `git describe --always --dirty` at build time, or "unknown" if git was not available.
extern const char* const kCommit;
/// When the binary was configured/built, as "YYYY-MM-DD HH:MM UTC".
extern const char* const kTimestamp;
} // namespace udpscope::build_info
+53
View File
@@ -0,0 +1,53 @@
// SPDX-License-Identifier: MIT
#include "help_window.hpp"
#include <cstdio>
#include "build_info.hpp"
#include "layout_store.hpp"
namespace udpscope {
namespace {
/// Nulled by the `close-request` handler so a second open cannot present a dead widget.
/// One pointer per window, file-scope because there is one application.
GtkWidget* g_about = nullptr;
gboolean on_window_close(GtkWindow* window, gpointer slot) {
(void)window;
*static_cast<GtkWidget**>(slot) = nullptr;
return FALSE; // let the default handler destroy it
}
} // namespace
void show_about(GtkWindow* parent, const std::string& session_path) {
if (g_about != nullptr) {
gtk_window_present(GTK_WINDOW(g_about));
return;
}
// GtkAboutDialog is not deprecated in GTK 4 (unlike GtkShortcutsWindow), so the
// stock dialog is used here and the overview is hand-rolled.
GtkWidget* dialog = gtk_about_dialog_new();
g_about = dialog;
GtkAboutDialog* about = GTK_ABOUT_DIALOG(dialog);
gtk_about_dialog_set_program_name(about, "udpscope");
gtk_about_dialog_set_version(about, build_info::kVersion);
gtk_about_dialog_set_comments(about, "An oscilloscope for UDPStreamer sources.");
gtk_about_dialog_set_license_type(about, GTK_LICENSE_MIT_X11);
char details[1024];
std::snprintf(details, sizeof(details),
"Commit: %s\nBuilt: %s\nSettings: %s\nSession file: %s",
build_info::kCommit, build_info::kTimestamp, config_dir().c_str(),
session_path.c_str());
gtk_about_dialog_set_system_information(about, details);
gtk_window_set_transient_for(GTK_WINDOW(dialog), parent);
gtk_window_set_modal(GTK_WINDOW(dialog), TRUE);
g_signal_connect(dialog, "close-request", G_CALLBACK(on_window_close), &g_about);
gtk_window_present(GTK_WINDOW(dialog));
}
} // namespace udpscope
+14
View File
@@ -0,0 +1,14 @@
// SPDX-License-Identifier: MIT
#pragma once
#include <gtk/gtk.h>
#include <string>
namespace udpscope {
/// The About dialog: version, commit, build timestamp and where the session files live.
/// Modal on `parent`. `session_path` is this run's own session file.
void show_about(GtkWindow* parent, const std::string& session_path);
} // namespace udpscope
+8 -8
View File
@@ -31,14 +31,6 @@ std::string sanitize(const std::string& host) {
return out.empty() ? "_" : out;
}
std::string config_dir() {
const char* xdg = std::getenv("XDG_CONFIG_HOME");
if (xdg != nullptr && xdg[0] != '\0') return std::string(xdg) + "/udpscope";
const char* home = std::getenv("HOME");
if (home == nullptr || home[0] == '\0') return "/tmp/udpscope-config";
return std::string(home) + "/.config/udpscope";
}
void mkdirs(const std::string& path) {
// **Every** component, the last one included. The condition used to be "the path has a trailing
// slash", so the final directory was never created: `~/.config/udpscope` did not exist, the
@@ -114,6 +106,14 @@ bool parse_string_array(const std::string& in, std::vector<std::string>& out) {
} // namespace
std::string config_dir() {
const char* xdg = std::getenv("XDG_CONFIG_HOME");
if (xdg != nullptr && xdg[0] != '\0') return std::string(xdg) + "/udpscope";
const char* home = std::getenv("HOME");
if (home == nullptr || home[0] == '\0') return "/tmp/udpscope-config";
return std::string(home) + "/.config/udpscope";
}
std::string layout_path(const std::string& host, std::uint16_t port) {
const std::string dir = config_dir();
mkdirs(dir);
+4
View File
@@ -81,6 +81,10 @@ bool forget_source(const KnownSource& src);
std::string math_node_text(const MathNode& node);
bool math_node_from_text(const std::string& text, MathNode& out);
/// The directory session files live in: $XDG_CONFIG_HOME/udpscope, else
/// ~/.config/udpscope, else /tmp/udpscope-config. Not created by this call.
std::string config_dir();
/// `~/.config/udpscope/<host>_<port>.toml`, with the directory created.
/// Characters unsafe in a filename are replaced in the host part.
std::string layout_path(const std::string& host, std::uint16_t port);
+18
View File
@@ -17,6 +17,7 @@
#include "acquisition.hpp"
#include "config.hpp"
#include "export.hpp"
#include "help_window.hpp"
#include "layout_store.hpp"
#include "measure.hpp"
#include "model.hpp"
@@ -4083,6 +4084,16 @@ void on_export_clicked(GtkButton*, gpointer user) {
g_object_unref(dialog);
}
void on_about_clicked(GtkButton*, gpointer user) {
App* app = static_cast<App*>(user);
if (app->menu_button != nullptr) {
GtkPopover* popover = gtk_menu_button_get_popover(GTK_MENU_BUTTON(app->menu_button));
if (popover != nullptr) gtk_popover_popdown(popover);
}
show_about(GTK_WINDOW(app->window),
layout_path(app->model->config().server, app->model->config().port));
}
// --- right-click menu -------------------------------------------------------
void on_y_auto_clicked(GtkButton* button, gpointer user) {
@@ -5542,6 +5553,13 @@ GtkWidget* build_toolbar(App& app) {
g_signal_connect(app.signal_toggle, "toggled", G_CALLBACK(on_signals_toggled), &app);
gtk_box_append(GTK_BOX(menu_box), app.signal_toggle);
gtk_box_append(GTK_BOX(menu_box), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
GtkWidget* about_item = gtk_button_new_with_label("About udpscope");
gtk_widget_set_tooltip_text(about_item, "version, build timestamp and settings paths");
g_signal_connect(about_item, "clicked", G_CALLBACK(on_about_clicked), &app);
gtk_box_append(GTK_BOX(menu_box), about_item);
GtkWidget* popover = gtk_popover_new();
gtk_popover_set_child(GTK_POPOVER(popover), menu_box);
app.menu_button = gtk_menu_button_new();