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
+22
-1
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.24)
|
cmake_minimum_required(VERSION 3.24)
|
||||||
project(udpscope LANGUAGES C CXX)
|
project(udpscope VERSION 0.1.0 LANGUAGES C CXX)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
@@ -51,9 +51,28 @@ add_custom_command(
|
|||||||
COMMENT "Compiling udpscope icon resources"
|
COMMENT "Compiling udpscope icon resources"
|
||||||
VERBATIM)
|
VERBATIM)
|
||||||
|
|
||||||
|
# --- build info: version, commit and timestamp compiled into the binary -------
|
||||||
|
# Written at configure time so add_executable can name the file, and again on every
|
||||||
|
# build so the commit and the timestamp belong to the binary that is running rather
|
||||||
|
# than to whenever the tree was last configured.
|
||||||
|
set(UDPSCOPE_BUILD_INFO ${CMAKE_CURRENT_BINARY_DIR}/build_info.cpp)
|
||||||
|
set(UDPSCOPE_BUILD_INFO_ARGS
|
||||||
|
-DSRC=${CMAKE_CURRENT_SOURCE_DIR}
|
||||||
|
-DOUT=${UDPSCOPE_BUILD_INFO}
|
||||||
|
-DVERSION=${PROJECT_VERSION}
|
||||||
|
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/write_build_info.cmake)
|
||||||
|
execute_process(COMMAND ${CMAKE_COMMAND} ${UDPSCOPE_BUILD_INFO_ARGS})
|
||||||
|
add_custom_target(udpscope_build_info
|
||||||
|
BYPRODUCTS ${UDPSCOPE_BUILD_INFO}
|
||||||
|
COMMAND ${CMAKE_COMMAND} ${UDPSCOPE_BUILD_INFO_ARGS}
|
||||||
|
COMMENT "Writing udpscope build info"
|
||||||
|
VERBATIM)
|
||||||
|
set_source_files_properties(${UDPSCOPE_BUILD_INFO} PROPERTIES GENERATED TRUE)
|
||||||
|
|
||||||
# --- the application ----------------------------------------------------------
|
# --- the application ----------------------------------------------------------
|
||||||
add_executable(udpscope
|
add_executable(udpscope
|
||||||
${CMAKE_CURRENT_BINARY_DIR}/udpscope_resources.c
|
${CMAKE_CURRENT_BINARY_DIR}/udpscope_resources.c
|
||||||
|
${UDPSCOPE_BUILD_INFO}
|
||||||
src/main.cpp
|
src/main.cpp
|
||||||
src/config.cpp
|
src/config.cpp
|
||||||
src/channel.cpp
|
src/channel.cpp
|
||||||
@@ -66,11 +85,13 @@ add_executable(udpscope
|
|||||||
src/source_dialog.cpp
|
src/source_dialog.cpp
|
||||||
src/measure.cpp
|
src/measure.cpp
|
||||||
src/math_channel.cpp
|
src/math_channel.cpp
|
||||||
|
src/help_window.cpp
|
||||||
src/ui.cpp
|
src/ui.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(udpscope PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
target_include_directories(udpscope PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
||||||
target_link_libraries(udpscope PRIVATE plot::gtk4 udpsclient Threads::Threads)
|
target_link_libraries(udpscope PRIVATE plot::gtk4 udpsclient Threads::Threads)
|
||||||
|
add_dependencies(udpscope udpscope_build_info)
|
||||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||||
target_compile_options(udpscope PRIVATE -Wall -Wextra -Wpedantic)
|
target_compile_options(udpscope PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
endif()
|
endif()
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# SPDX-License-Identifier: MIT
|
||||||
|
#
|
||||||
|
# Writes ${OUT} with the three strings src/build_info.hpp declares. Run both at
|
||||||
|
# configure time (so the file exists when add_executable names it) and on every
|
||||||
|
# build (so the commit and the timestamp are the ones that produced the binary).
|
||||||
|
#
|
||||||
|
# Expects: SRC (the source tree), OUT (the .cpp to write), VERSION.
|
||||||
|
|
||||||
|
set(COMMIT "unknown")
|
||||||
|
find_package(Git QUIET)
|
||||||
|
if(GIT_FOUND AND EXISTS "${SRC}/.git")
|
||||||
|
execute_process(
|
||||||
|
COMMAND ${GIT_EXECUTABLE} describe --always --dirty
|
||||||
|
WORKING_DIRECTORY "${SRC}"
|
||||||
|
OUTPUT_VARIABLE GIT_OUT
|
||||||
|
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||||
|
ERROR_QUIET
|
||||||
|
RESULT_VARIABLE GIT_RC)
|
||||||
|
if(GIT_RC EQUAL 0 AND NOT GIT_OUT STREQUAL "")
|
||||||
|
set(COMMIT "${GIT_OUT}")
|
||||||
|
endif()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
string(TIMESTAMP STAMP "%Y-%m-%d %H:%M UTC" UTC)
|
||||||
|
|
||||||
|
file(WRITE "${OUT}"
|
||||||
|
"// SPDX-License-Identifier: MIT
|
||||||
|
// Generated by cmake/write_build_info.cmake. Do not edit, do not commit.
|
||||||
|
#include \"build_info.hpp\"
|
||||||
|
|
||||||
|
namespace udpscope::build_info {
|
||||||
|
|
||||||
|
const char* const kVersion = \"${VERSION}\";
|
||||||
|
const char* const kCommit = \"${COMMIT}\";
|
||||||
|
const char* const kTimestamp = \"${STAMP}\";
|
||||||
|
|
||||||
|
} // namespace udpscope::build_info
|
||||||
|
")
|
||||||
@@ -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;
|
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) {
|
void mkdirs(const std::string& path) {
|
||||||
// **Every** component, the last one included. The condition used to be "the path has a trailing
|
// **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
|
// 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
|
} // 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) {
|
std::string layout_path(const std::string& host, std::uint16_t port) {
|
||||||
const std::string dir = config_dir();
|
const std::string dir = config_dir();
|
||||||
mkdirs(dir);
|
mkdirs(dir);
|
||||||
|
|||||||
@@ -81,6 +81,10 @@ bool forget_source(const KnownSource& src);
|
|||||||
std::string math_node_text(const MathNode& node);
|
std::string math_node_text(const MathNode& node);
|
||||||
bool math_node_from_text(const std::string& text, MathNode& out);
|
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.
|
/// `~/.config/udpscope/<host>_<port>.toml`, with the directory created.
|
||||||
/// Characters unsafe in a filename are replaced in the host part.
|
/// Characters unsafe in a filename are replaced in the host part.
|
||||||
std::string layout_path(const std::string& host, std::uint16_t port);
|
std::string layout_path(const std::string& host, std::uint16_t port);
|
||||||
|
|||||||
+18
@@ -17,6 +17,7 @@
|
|||||||
#include "acquisition.hpp"
|
#include "acquisition.hpp"
|
||||||
#include "config.hpp"
|
#include "config.hpp"
|
||||||
#include "export.hpp"
|
#include "export.hpp"
|
||||||
|
#include "help_window.hpp"
|
||||||
#include "layout_store.hpp"
|
#include "layout_store.hpp"
|
||||||
#include "measure.hpp"
|
#include "measure.hpp"
|
||||||
#include "model.hpp"
|
#include "model.hpp"
|
||||||
@@ -4083,6 +4084,16 @@ void on_export_clicked(GtkButton*, gpointer user) {
|
|||||||
g_object_unref(dialog);
|
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 -------------------------------------------------------
|
// --- right-click menu -------------------------------------------------------
|
||||||
|
|
||||||
void on_y_auto_clicked(GtkButton* button, gpointer user) {
|
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);
|
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), 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();
|
GtkWidget* popover = gtk_popover_new();
|
||||||
gtk_popover_set_child(GTK_POPOVER(popover), menu_box);
|
gtk_popover_set_child(GTK_POPOVER(popover), menu_box);
|
||||||
app.menu_button = gtk_menu_button_new();
|
app.menu_button = gtk_menu_button_new();
|
||||||
|
|||||||
Reference in new issue
Block a user