The hamburger menu's help rows were three flat entries and a window. They are a **`Help` submenu** now, which is where every GNOME menu keeps them, with the four things a first run cannot find: - Keyboard shortcuts Ctrl+/ (the existing overview, `show_shortcuts()`) - Documentation F1 (the existing manual, `show_help()`) - Guided tour Ctrl+T (new) - Enable contextual help (new, a stateful row: its check *is* the setting) The submenu *replaces* the menu's rows rather than floating beside them, the same shape and the same reason as the plot's right-click menu: a submenu torn out of the menu it belongs to is a second thing to dismiss, and the `GtkMenuButton` that owned one toggled itself out of sync the moment anything else closed its popover. It is the pages of a `GtkStack`, not a rebuild of the box and not a swap of the popover's child: the main view holds `app.layout_toggle`, which the tick's `sync_layout_editor()` and the `L` accel read, so rebuilding would leave that pointer on a destroyed widget, and a swapped child would leave the view that is not showing without an owner. **The contextual help is one table and a "?" per surface** (`src/ui_context_help.cpp`): the plot, the trigger, the cursors, the measurements, the math channels, the signals, the event history, the time window, the status line, and the math editor's identity row. Each row names the *heading box* its button joins -- a sibling of the title, never an overlay child on a corner, which is what would cover the switch or the [edit] button already there -- so adding a surface is a row and a widget pointer. Off means *gone*, not greyed: the buttons are removed, and they are found by a data key rather than a list of pointers because the math editor's heading is built afresh on every opening and a list would go stale one dialog later. `contextual_help` and `tour_seen` are keys of `settings.toml`, written through the same `Settings`/`load_settings()`/`save_settings()` the Preferences dialog uses, whose own rows start from `load_settings()` and so carry them through untouched. **The guided tour is five steps on a `GtkOverlay`** above the window's content: the plot, the side panel, the trigger, the signals and the status line. There is no popover in it and therefore no pointer grab to leak -- four dim rectangles around the hole are what make it modal -- and the probe asserts that as a reading (`controllers=` against the count taken before it opened) rather than as a claim. Everything is placed from `gtk_widget_compute_bounds()` re-read on every frame, because a widget's allocation is 0 on the frame it is first shown and a window resize moves it under a one-shot measurement; the hole is kept from the last frame that could measure the target, because a frame that lands between two layout passes would otherwise empty it and make the spotlight jump. `Skip`, `Finish` and `Escape` all hide every child and leave nothing behind. A first run gets the tour once, decided by `tour_seen` and not by "the settings file did not exist" (a user who changed the theme has a file and has still never seen it), and the flag is written when the tour *starts* rather than when it finishes: a user who skips it in the first second has still seen it. `UDPSCOPE_PROBE_HELPUI_MS` drives all of it -- the auto-start, the menu's seven rows and the submenu's five, the toggle's effect read as a count of affordances on either side of it, and the tour started, stepped forwards and back and dismissed -- and the exit dump gains `help layer:` and `help figures:`, the latter being the ten surfaces' window-coordinate rectangles that the manual's annotated screenshots crop their boxes from. Two of them are what the D-Bus-free headless run can still not decide, and both are limitations rather than defects: export carries no "?" (it is a menu action with no surface of its own, and a popover inside the menu's popover is the nested-popover trap the invariants already warn about), and the tour card cannot miss its target when the target is the whole content -- which is what the plot is at the window's 756 px minimum, where the split view overlays the side panel instead of putting it beside the plot. The probe prints both rectangles rather than asserting a placement, so that case reads as the geometry it is. Also fixed while it was found: `gtk_widget_measure()` includes the widget's own margins (measured: a 280 px box with `margin-start 453` reports 733), so a measure that is used to *place* the widget is a feedback loop -- that is what made the tour card walk between the corner and the target from frame to frame.
268 lines
14 KiB
CMake
268 lines
14 KiB
CMake
cmake_minimum_required(VERSION 3.24)
|
|
project(udpscope VERSION 0.1.0 LANGUAGES C CXX)
|
|
|
|
set(CMAKE_CXX_STANDARD 20)
|
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
|
|
|
if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)
|
|
set(CMAKE_BUILD_TYPE Release CACHE STRING "" FORCE)
|
|
endif()
|
|
|
|
find_package(Threads REQUIRED)
|
|
# `CMAKE_INSTALL_DATADIR` (the `share` under the prefix) for the help set's install path.
|
|
include(GNUInstallDirs)
|
|
|
|
# --- libadwaita: the shortcut overview ---------------------------------------
|
|
# GTK4's own `GtkShortcutsWindow` is deprecated as of 4.18 and libadwaita's
|
|
# `AdwShortcutsDialog` (1.8) is its replacement, so the overview is built from the
|
|
# platform widget rather than from a hand-rolled grid. This is the only thing libadwaita
|
|
# is used for; `adw_init()` is called for it, which also means the app picks up
|
|
# libadwaita's stylesheet.
|
|
find_package(PkgConfig REQUIRED)
|
|
pkg_check_modules(ADWAITA REQUIRED IMPORTED_TARGET libadwaita-1>=1.8)
|
|
|
|
# --- deps/plotter: built from the submodule, GTK4 adapter only ----------------
|
|
set(PLOT_BUILD_TESTS OFF CACHE BOOL "" FORCE)
|
|
set(PLOT_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
|
set(PLOT_BUILD_GLFW OFF CACHE BOOL "" FORCE)
|
|
set(PLOT_BUILD_IMGUI OFF CACHE BOOL "" FORCE)
|
|
set(PLOT_BUILD_QT6 OFF CACHE BOOL "" FORCE)
|
|
set(PLOT_BUILD_GTK4 ON CACHE BOOL "" FORCE)
|
|
set(PLOT_INSTALL OFF CACHE BOOL "" FORCE)
|
|
add_subdirectory(deps/plotter EXCLUDE_FROM_ALL)
|
|
|
|
# --- deps/udpsclient: vendored C99 receiver ----------------------------------
|
|
add_library(udpsclient STATIC deps/udpsclient/udps_client.c)
|
|
target_include_directories(udpsclient PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/deps/udpsclient)
|
|
set_target_properties(udpsclient PROPERTIES C_STANDARD 99 C_STANDARD_REQUIRED ON)
|
|
if(CMAKE_C_COMPILER_ID MATCHES "GNU|Clang")
|
|
target_compile_options(udpsclient PRIVATE -Wall -Wextra)
|
|
endif()
|
|
|
|
# --- icons: six SVGs compiled into the binary --------------------------------
|
|
# There is no stock icon for an oscilloscope trigger, a pair of cursors or a measurement
|
|
# ruler, so this app draws its own. They go in a GResource rather than beside the binary
|
|
# because udpscope is run straight out of the build tree and has no install step: a
|
|
# search path would have to be located at runtime, and the failure mode of not locating
|
|
# it is a silent missing-image placeholder.
|
|
find_program(GLIB_COMPILE_RESOURCES NAMES glib-compile-resources REQUIRED)
|
|
set(UDPSCOPE_ICON_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/icons)
|
|
file(GLOB UDPSCOPE_ICON_FILES CONFIGURE_DEPENDS
|
|
${UDPSCOPE_ICON_DIR}/scalable/actions/*.svg
|
|
${UDPSCOPE_ICON_DIR}/scalable/apps/*.svg)
|
|
add_custom_command(
|
|
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/udpscope_resources.c
|
|
COMMAND ${GLIB_COMPILE_RESOURCES}
|
|
--target=${CMAKE_CURRENT_BINARY_DIR}/udpscope_resources.c
|
|
--sourcedir=${UDPSCOPE_ICON_DIR}
|
|
--generate-source
|
|
${UDPSCOPE_ICON_DIR}/udpscope.gresource.xml
|
|
DEPENDS ${UDPSCOPE_ICON_DIR}/udpscope.gresource.xml ${UDPSCOPE_ICON_FILES}
|
|
COMMENT "Compiling udpscope icon resources"
|
|
VERBATIM)
|
|
|
|
# --- the manual: a Mallard help set, installed for the `help:` URI ------------
|
|
# The manual is no longer a GResource compiled into the binary. It is a Mallard set that the
|
|
# desktop help viewer (Yelp on GNOME) reads off disk, because that is the only way `F1` can
|
|
# open a real, searchable, translatable document and the only way the freedesktop Help System
|
|
# Specification's `help:` URI resolves it. The user documentation is then *files*, checked and
|
|
# installed rather than compiled in.
|
|
#
|
|
# The document identifier -- the `<name>` in `help:<name>` -- is derived from the desktop
|
|
# file's own basename (its desktop-file id), not written out twice: the same string is the
|
|
# `.desktop` file, the GApplication id and the installed directory. The specification installs
|
|
# a document at `<datadir>/help/<lang>/<document>/` and resolves `help:<document>` to the first
|
|
# match under $XDG_DATA_HOME/$XDG_DATA_DIRS, so the directory name *is* the identifier.
|
|
# NAME_WE is not used: it strips the *longest* extension, so it would cut `org.udpscope.app`
|
|
# down to `org`. Strip the two suffixes this one file actually carries, in order.
|
|
get_filename_component(_udpscope_desktop
|
|
${CMAKE_CURRENT_SOURCE_DIR}/data/org.udpscope.app.desktop.in NAME)
|
|
string(REGEX REPLACE "\\.desktop\\.in$" "" UDPSCOPE_HELP_NAME "${_udpscope_desktop}")
|
|
unset(_udpscope_desktop)
|
|
set(UDPSCOPE_HELP_SRC ${CMAKE_CURRENT_SOURCE_DIR}/help/C/${UDPSCOPE_HELP_NAME})
|
|
file(GLOB UDPSCOPE_HELP_FILES CONFIGURE_DEPENDS
|
|
${UDPSCOPE_HELP_SRC}/*.page ${UDPSCOPE_HELP_SRC}/figures/*.png)
|
|
|
|
# The install rule the specification asks for. `CMAKE_INSTALL_DATADIR` is `share` by default,
|
|
# so the set lands at `<prefix>/share/help/C/<document>/`, which is exactly where a viewer
|
|
# looks. There is no install step for the binary itself (see the desktop-entry target below),
|
|
# but the manual is *read from a datadir* by another process, so it has to be installable.
|
|
install(DIRECTORY ${UDPSCOPE_HELP_SRC}/
|
|
DESTINATION ${CMAKE_INSTALL_DATADIR}/help/C/${UDPSCOPE_HELP_NAME}
|
|
FILES_MATCHING PATTERN "*.page" PATTERN "*.png" PATTERN "*.jpg")
|
|
|
|
# --- UI files: GtkBuilder XML in a GResource ----------------------------------
|
|
# The third bundle, for the same reason as the other two: `.ui` files are data, so they are compiled
|
|
# in rather than looked for at runtime -- and `preprocess="xml-stripblanks"` makes a malformed one a
|
|
# build failure with a file and line number, instead of a window that silently refuses to open.
|
|
set(UDPSCOPE_UI_DIR ${CMAKE_CURRENT_SOURCE_DIR}/src/ui)
|
|
file(GLOB UDPSCOPE_UI_FILES CONFIGURE_DEPENDS ${UDPSCOPE_UI_DIR}/*.ui)
|
|
add_custom_command(
|
|
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/udpscope_ui_resources.c
|
|
COMMAND ${GLIB_COMPILE_RESOURCES}
|
|
--target=${CMAKE_CURRENT_BINARY_DIR}/udpscope_ui_resources.c
|
|
--sourcedir=${UDPSCOPE_UI_DIR}
|
|
--c-name=udpscope_ui
|
|
--generate-source
|
|
${UDPSCOPE_UI_DIR}/udpscope-ui.gresource.xml
|
|
DEPENDS ${UDPSCOPE_UI_DIR}/udpscope-ui.gresource.xml ${UDPSCOPE_UI_FILES}
|
|
COMMENT "Compiling udpscope UI resources"
|
|
VERBATIM)
|
|
# The generated source carries each file as one C string literal, and a `.ui` is big enough to pass the
|
|
# 4095-character minimum the standard requires a compiler to support: `-Woverlength-strings` fires on
|
|
# generated *data*, not on anything anyone wrote. Silenced for this one generated file (the icon bundle
|
|
# is small enough per file never to need it), so the build stays at its three warnings. The manual is
|
|
# no longer compiled in, so the help bundle's own warning is gone with it.
|
|
set_source_files_properties(${CMAKE_CURRENT_BINARY_DIR}/udpscope_ui_resources.c
|
|
PROPERTIES COMPILE_OPTIONS "-Wno-overlength-strings")
|
|
|
|
# --- the UI files: an opt-in check, never part of `cmake --build` ---------------
|
|
# The interface is hand-written GtkBuilder XML, and nothing in the build sees a mistake in it: a
|
|
# property a class does not have is a `Gtk-ERROR` when the window is built, and `Gtk-ERROR` *aborts
|
|
# the process* -- so a typo takes the application down rather than failing to open a dialog.
|
|
# `xml-stripblanks` catches malformed XML and nothing else, and the probes catch the rest only for the
|
|
# windows they open.
|
|
#
|
|
# blueprint-compiler type-checks every property and child type against the GIR, and it reads XML as
|
|
# well as blueprint (`decompile` turns one into the other), so `tools/check_ui.py` round-trips each
|
|
# file through it. The XML stays the source of truth and stays hand-written; nothing here is needed to
|
|
# build the application, which is the whole point of not converting the files.
|
|
#
|
|
# No ALL: this runs only when it is named, so `cmake --build build` is unaffected and
|
|
# blueprint-compiler is not a build dependency.
|
|
find_program(UDPSCOPE_PYTHON NAMES python3)
|
|
if(UDPSCOPE_PYTHON)
|
|
add_custom_target(test-ui
|
|
COMMAND ${UDPSCOPE_PYTHON} ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_ui.py ${UDPSCOPE_UI_DIR}
|
|
DEPENDS ${UDPSCOPE_UI_FILES} ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_ui.py
|
|
COMMENT "Checking the UI files with blueprint-compiler"
|
|
VERBATIM)
|
|
else()
|
|
message(WARNING "python3 was not found: the test-ui target is not available")
|
|
endif()
|
|
|
|
# --- the manual: an opt-in check, never part of `cmake --build` -----------------
|
|
# The manual is hand-written Mallard, and `cmake --build` never reads it -- a broken xref or a
|
|
# missing figure is a page in the viewer with no content or no image, not a build failure.
|
|
# `tools/check_help.sh` runs yelp-check's four offline subcommands over the set (links, media,
|
|
# orphans, ids) and renders the whole set to HTML with yelp-build, so the manual is checked the
|
|
# way the `.ui` files are. `yelp-check validate` is *not* used: it fetches the Mallard RelaxNG
|
|
# schema over the network, which is not available here and would make the check depend on it.
|
|
#
|
|
# No ALL: this runs only when it is named, so `cmake --build build` is unaffected and
|
|
# yelp-tools is not a build dependency.
|
|
find_program(UDPSCOPE_YELP_CHECK NAMES yelp-check)
|
|
find_program(UDPSCOPE_YELP_BUILD NAMES yelp-build)
|
|
if(UDPSCOPE_YELP_CHECK AND UDPSCOPE_YELP_BUILD)
|
|
add_custom_target(test-help
|
|
COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_help.sh ${UDPSCOPE_HELP_SRC}
|
|
DEPENDS ${UDPSCOPE_HELP_FILES} ${CMAKE_CURRENT_SOURCE_DIR}/tools/check_help.sh
|
|
COMMENT "Checking the Mallard help set with yelp-check and yelp-build"
|
|
VERBATIM)
|
|
else()
|
|
message(WARNING "yelp-check/yelp-build were not found: the test-help target is not available")
|
|
endif()
|
|
|
|
# --- 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 ----------------------------------------------------------
|
|
add_executable(udpscope
|
|
${CMAKE_CURRENT_BINARY_DIR}/udpscope_resources.c
|
|
${CMAKE_CURRENT_BINARY_DIR}/udpscope_ui_resources.c
|
|
${UDPSCOPE_BUILD_INFO}
|
|
src/main.cpp
|
|
src/config.cpp
|
|
src/channel.cpp
|
|
src/model.cpp
|
|
src/condition.cpp
|
|
src/trigger.cpp
|
|
src/capture.cpp
|
|
src/waveform.cpp
|
|
src/event_store.cpp
|
|
src/acquisition.cpp
|
|
src/plot_host.cpp
|
|
src/export.cpp
|
|
src/search.cpp
|
|
src/layout_store.cpp
|
|
src/source_dialog.cpp
|
|
src/measure.cpp
|
|
src/math_channel.cpp
|
|
src/status_text.cpp
|
|
src/shortcuts.cpp
|
|
src/help_window.cpp
|
|
src/settings.cpp
|
|
src/ui_prefs.cpp
|
|
src/ui.cpp
|
|
src/ui_probe.cpp
|
|
src/ui_widgets.cpp
|
|
src/ui_events.cpp
|
|
src/ui_status.cpp
|
|
src/ui_measure.cpp
|
|
src/ui_cursors.cpp
|
|
src/ui_math.cpp
|
|
src/ui_layout.cpp
|
|
src/ui_trigger.cpp
|
|
src/ui_signals.cpp
|
|
src/ui_context_help.cpp
|
|
src/ui_tour.cpp
|
|
)
|
|
|
|
target_include_directories(udpscope PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src)
|
|
# The manual's identifier (`show_help()` builds `help:<id>` from it) and its directory in the
|
|
# source tree (what `help_self_check()` scans). Both come from the block near the top, so the
|
|
# identifier is the desktop file's basename and nothing is written out twice.
|
|
target_compile_definitions(udpscope PRIVATE
|
|
UDPSCOPE_HELP_NAME="${UDPSCOPE_HELP_NAME}"
|
|
UDPSCOPE_HELP_DIR="${UDPSCOPE_HELP_SRC}")
|
|
target_link_libraries(udpscope PRIVATE plot::gtk4 udpsclient Threads::Threads
|
|
PkgConfig::ADWAITA)
|
|
add_dependencies(udpscope udpscope_build_info)
|
|
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
|
target_compile_options(udpscope PRIVATE -Wall -Wextra -Wpedantic)
|
|
endif()
|
|
|
|
# --- the desktop entry: opt-in, never part of `cmake --build` ------------------
|
|
# `Exec` is the built binary's absolute path in the build tree, so the entry always launches
|
|
# exactly the binary that was last built -- and moving or deleting the build directory breaks
|
|
# it. That is the price of having no install step, and it is the right one here: an install()
|
|
# rule would create a second copy of the app that can diverge from the build tree, and a
|
|
# documented `cp` would need the binary on PATH, which it is not.
|
|
#
|
|
# configure_file() cannot evaluate a generator expression and file(GENERATE) cannot substitute
|
|
# an @VAR@, so the two run in sequence: the first writes the genex out literally, the second
|
|
# evaluates it. This is the documented idiom.
|
|
set(UDPSCOPE_EXEC "$<TARGET_FILE:udpscope>")
|
|
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/data/org.udpscope.app.desktop.in
|
|
${CMAKE_CURRENT_BINARY_DIR}/desktop-entry.in @ONLY)
|
|
file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/org.udpscope.app.desktop
|
|
INPUT ${CMAKE_CURRENT_BINARY_DIR}/desktop-entry.in)
|
|
|
|
# No ALL: this runs only when it is named, so `cmake --build build` is unaffected.
|
|
add_custom_target(desktop-entry
|
|
COMMAND ${CMAKE_COMMAND}
|
|
-DENTRY=${CMAKE_CURRENT_BINARY_DIR}/org.udpscope.app.desktop
|
|
-DICON=${UDPSCOPE_ICON_DIR}/scalable/apps/org.udpscope.app.svg
|
|
-DSYMBOLIC=${UDPSCOPE_ICON_DIR}/scalable/apps/org.udpscope.app-symbolic.svg
|
|
-P ${CMAKE_CURRENT_SOURCE_DIR}/cmake/install_desktop_entry.cmake
|
|
COMMENT "Installing the udpscope desktop entry into XDG_DATA_HOME"
|
|
VERBATIM)
|
|
add_dependencies(desktop-entry udpscope)
|