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:
1 parent
82f91d00c0
commit
a0fa845b58
8 files changed
+170
-9
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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();
|
||||
|
||||
Reference in new issue
Block a user