docs: the app icon, AdwAboutDialog and the desktop entry
Records the app icons: assertion and why it stops short of claiming the icon renders -- no runtime GTK call can tell a malformed SVG from a good one, which is why the manifest validates through xmllint at build time instead. Scopes the 'filled paths only' authoring rule to symbolic icons, since the colour icon strokes its sine and is never recoloured. Revises the windows invariant to one GtkWindow and two AdwDialogs, and documents the desktop-entry target with the build-path caveat it comes with. Two findings from the regression run are recorded as well. The icons' generated GResource crossed C99's 4095-byte string-literal limit when the app icons joined the action ones, so the build's warning baseline is five, not four. And the app icons: line's has_icon() half is satisfied by an installed theme copy, so on a machine that has run --target desktop-entry it reads ok even with the resource path broken; toolbar icons: is the sensitive line for that. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
f2978fe742
commit
e2be597698
2 files changed
+167
-15
No files matched your search
@@ -13,6 +13,25 @@ cmake --build build
|
||||
./build/udpscope --server 10.0.0.5 --port 8000 --multicast 239.1.2.3 --interface 10.0.0.9
|
||||
```
|
||||
|
||||
The app can be launched from the shell rather than only from a terminal, but only on request:
|
||||
|
||||
```sh
|
||||
cmake --build build --target desktop-entry
|
||||
```
|
||||
|
||||
That writes `org.udpscope.app.desktop` plus both app icons under `$XDG_DATA_HOME` (default
|
||||
`~/.local/share`), runs `desktop-file-validate` as a **hard failure** (when it is on `PATH`;
|
||||
absent, it says so and installs anyway), refreshes `update-desktop-database` and
|
||||
`gtk4-update-icon-cache` when they are on `PATH`, and prints the three paths it wrote — which is
|
||||
the uninstall instructions, since there is no uninstall target. `cmake --build build` alone never
|
||||
touches `$XDG_DATA_HOME`.
|
||||
|
||||
The entry's `Exec` is the built binary's **absolute path in the build tree**, so **moving or
|
||||
deleting the build directory breaks it.** That is the price of having no install step and it is
|
||||
deliberate: the entry then always launches exactly the binary that was last built. No
|
||||
`StartupWMClass` is needed — GTK takes the WM class from the application ID, and the desktop
|
||||
file's basename is that same string, which is what associates a running window with the entry.
|
||||
|
||||
Options: `--server`, `--port`, `--multicast`, `--interface`, `--buffer-mb` (per
|
||||
buffer, default 200), `--max-window`, `--holdoff`, `--recv-buffer`, `--plots` (the row
|
||||
count to *start* with, 1..`RowSet::kMaxRows`; the layout editor owns it afterwards).
|
||||
@@ -76,6 +95,15 @@ xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=1000 \
|
||||
./build/udpscope --server 127.0.0.1 --port 8000
|
||||
```
|
||||
|
||||
Two baselines every run is read against. A clean build emits **five** warnings and no more: two
|
||||
`-Wmissing-field-initializers` on `MathNode::source`, one unused `output_built`, and
|
||||
`-Woverlength-strings` on **both** generated GResource `.c` files — the help pages' and the
|
||||
icons', whose blob crossed C99's 4095-byte string-literal limit when the two app icons joined the
|
||||
four action ones (a note about a generated file, which GCC itself supports). And an exit-dump run
|
||||
emits exactly **two** `Gtk-CRITICAL`s, `gtk_widget_get_height` and `gtk_widget_get_width`, from
|
||||
the dump asking a destroyed widget for its size. A sixth warning, or a third critical, is a
|
||||
finding.
|
||||
|
||||
- `UDPSCOPE_EXIT_AFTER_MS` — run the trigger-on → go-live probe, close the window,
|
||||
then dump the whole pipeline state to stdout (frame/sample counts, per-plot painted
|
||||
frames, trace bindings, panel sizes, status line in both states).
|
||||
@@ -460,6 +488,28 @@ Two lines of the self-check output are assertions in disguise:
|
||||
substitutes a missing-image placeholder, the button still works, and every other line of the
|
||||
dump still passes. Changing an icon name is a one-line change in `build_toolbar()` and this
|
||||
is the only check that it names something that exists.
|
||||
- `app icons: org.udpscope.app(ok 128x128) org.udpscope.app-symbolic(ok 16x16)` (in the exit dump,
|
||||
beside `toolbar icons:`) — the application icon, named and present at the size its own root
|
||||
`<svg>` element declares. **Both entries must read `ok` at those sizes.** `ok` is
|
||||
`gtk_icon_theme_has_icon()` *and* the resource bytes being found under the registered path, so a
|
||||
`MISSING` is a resource path that was never registered or a file that was renamed, and a size
|
||||
other than 128 or 16 is a canvas that drifted off the HIG's. **Read it together with
|
||||
`toolbar icons:`, and trust that line further for the registration**: the resource-path
|
||||
registration is one call in `run_ui()` serving both the window's icon and the toolbar's buttons,
|
||||
so a `MISSING` on either line is the same defect — but on a machine where
|
||||
`--target desktop-entry` has been run, *this* line's `has_icon` half is also satisfied by the
|
||||
**installed** copy under `$XDG_DATA_HOME/icons/hicolor`, so a broken registration still reads
|
||||
`ok` here. That was measured: with the resource path deliberately misspelt, `toolbar icons:`
|
||||
reported four `MISSING` (nothing installs the action icons) while `app icons:` stayed `ok`
|
||||
until the two installed SVGs were moved aside, whereupon it read `MISSING 128x128` /
|
||||
`MISSING 16x16`. The line is still worth its place — the *present* half (the resource bytes) and
|
||||
the declared sizes are read from the binary and nothing else can satisfy them — but a developer
|
||||
machine is the one where it is least sensitive.
|
||||
**It deliberately does not claim the icon renders**, because no runtime GTK call can:
|
||||
`gtk_icon_theme_lookup_icon()` never returns `NULL`, its paintable's intrinsic size is the size
|
||||
it was *asked* for, and a valid icon, a malformed one and a name that does not exist at all all
|
||||
snapshot to the same `GSK_TEXTURE_NODE` of the requested bounds — GTK substitutes
|
||||
`image-missing`. A malformed SVG is a *build* error instead; see the icon-authoring rules.
|
||||
- `[sources] N known: HOST:PORT plots=P signals=S [multicast=G] [interface=I] | ...` (stderr,
|
||||
`UDPSCOPE_PROBE_SOURCES=1`) — the startup chooser's discovery, printed and exited **before
|
||||
`gtk_init()`**, so this one needs no display and no `xvfb-run`. **Only sources with a named
|
||||
@@ -564,7 +614,7 @@ Two lines of the self-check output are assertions in disguise:
|
||||
so the panes the tick rewrites are measured directly. The `[width-probe]` lines are the
|
||||
per-change detail behind the `axes.w changes` count, and a step over 100 px dumps the widget
|
||||
tree's wide subtrees — that is what the documented, intermittent ~320 px step wants read.
|
||||
- `[help] pages=14 parsed=14 shortest='Overview' 1381 B; shortcuts: app=12 plot=13; about version=… commit=… built=… settings=…; closed ok` (stderr, `UDPSCOPE_PROBE_HELP_MS`) — **`parsed` must equal `pages`** and no page may be empty: a missing resource or markup Pango rejects renders a one-line stand-in instead of failing, so the window *looks* fine and only this count notices. `settings=` must be a real path (the same one the About dialog shows). `closed ok` with **no `Gtk-CRITICAL`** is as much a crash test of three windows built, presented and closed in one run as it is a state check. Two of the three are `GtkWindow`s and close with `gtk_window_close()`; the shortcut overview is an `AdwDialog` and closes with `adw_dialog_close()`, cleared on `::closed`. `shortest=` is the page title — a page whose body was never compiled into the binary is absent rather than empty, and a title alone is the stand-in's shape.
|
||||
- `[help] pages=14 parsed=14 shortest='Overview' 1381 B; shortcuts: app=12 plot=13; about version=… commit=… built=… settings=… icon=org.udpscope.app; closed ok` (stderr, `UDPSCOPE_PROBE_HELP_MS`) — **`parsed` must equal `pages`** and no page may be empty: a missing resource or markup Pango rejects renders a one-line stand-in instead of failing, so the window *looks* fine and only this count notices. `settings=` must be a real path (the same one the About dialog shows). **`icon=` must read `org.udpscope.app`** — it is read back through `adw_about_dialog_get_application_icon()` while the dialog is open, so the dialog is asserted to *carry* the icon rather than merely to open (a `set_application_icon()` lost in an edit leaves a dialog that opens, looks almost right, and shows a generic placeholder). `closed ok` with **no `Gtk-CRITICAL`** is as much a crash test of three windows built, presented and closed in one run as it is a state check, and a stronger one than it was: the three are now closed by **two different calls**, so a mismatched pair crashes or leaks here. One of the three is a `GtkWindow` (the help window, `gtk_window_close()`); the About dialog and the shortcut overview are `AdwDialog`s and close with `adw_dialog_close()`, cleared on `::closed`. `shortest=` is the page title — a page whose body was never compiled into the binary is absent rather than empty, and a title alone is the stand-in's shape.
|
||||
- `[keys] a: armed 0->1 | space: paused 0->1 | t: trigger panel: 0->1 | … | editable focused: 'm' refused=1` (stderr, `UDPSCOPE_PROBE_KEYS_MS`) — **every `X->Y` pair must differ** and **`refused` must be 1**. The refusal is the whole reason the controller is in the bubble phase: every bare-letter accel is also a character, so an accel that fires while a `GtkEditable` has focus makes the window entry unusable. A pair reading `0->0` is an action whose dispatch does not reach the handler a click reaches. Note the format is `<key>: <widget-description>: <before>-><after>` — the name passed to `before_after()` includes the widget's description, so `t: trigger panel: 0->1` is the correct form (not `t: trigger panel 0->1` without the colon before the values).
|
||||
- `[zoomkeys] ctrl+plus x=[..]->[..] y_held=1 | alt+plus y=[..]->[..] x_held=1` (stderr, `UDPSCOPE_PROBE_ZOOMKEYS_MS`) — **both `_held` must be 1 and both zooms must still happen.** Run it inside a pinned state, where a frame cannot re-frame the other axis for its own reasons: `UDPSCOPE_PROBE_PAUSE_MS=1500 UDPSCOPE_PROBE_ZOOMKEYS_MS=2200`. A `_held=0` is the modifier not reaching the plotter's controller; an unchanged range on the *zoomed* axis is the opposite failure, the key doing nothing. Ctrl restricts to x and Alt to y; Ctrl+Alt zooms neither (no special case needed — neither flag alone fires).
|
||||
- `status bar: glance=[● live · 1.09 MS/s · 2 s window] max_glance_chars=53 detail_fields=11 popover_rebuilds=0` (exit dump) — **`max_glance_chars` must stay small** (under about 60) and **`popover_rebuilds` must be 0**. The status row is directly above the plot, so this is the "layout must not depend on content" rule read from the bar's own side: the state field is one word from a fixed set, the rate field is `%.3g` (so `10 kS/s`, never `10.0 kS/s`), and the only field trigger mode adds is the elapsed time, which steps units rather than growing digits. `detail_fields` is the popover's label count, a literal, so adding a label without thinking about layout shows up here. The glance rate format matters: `%.3g` drops the trailing zero that a `%.4g` or a fixed-precision format would keep — a rate that reads differently in the dump and in the help page is a format disagreement, and `14-status.txt`'s example must match it.
|
||||
@@ -1247,25 +1297,50 @@ far the largest file.
|
||||
runs after the window is destroyed and asking GTK about a widget then is four criticals, which is
|
||||
how the first version of that line earned them. What stays in the menu is what acts on the *data*
|
||||
(Fit, Export) and the two editors that are not panes (Layout, Signal list). The Arm/Disarm button in the trigger panel still has text.
|
||||
- **Four of the toolbar icons are this app's own, and they are a `GResource` compiled into the
|
||||
- **Six of this app's icons are its own, and they are a `GResource` compiled into the
|
||||
binary.** No icon theme has a glyph for an oscilloscope trigger, a pair of cursors or a
|
||||
measurement ruler, and the stock names those buttons used to carry said something else
|
||||
entirely: a *record dot* for the trigger and three different **magnifiers** for cursors,
|
||||
measurements and math. `src/icons/scalable/actions/udpscope-{trigger,cursors,measure,math}-symbolic.svg`
|
||||
are bundled by `src/icons/udpscope.gresource.xml` (CMake runs `glib-compile-resources`) and
|
||||
reached through the `gtk_icon_theme_add_resource_path()` at the top of `build_toolbar()`.
|
||||
are the four action icons; beside them in `src/icons/scalable/apps/` are the two
|
||||
**application** icons, `org.udpscope.app.svg` (128px colour artwork) and
|
||||
`org.udpscope.app-symbolic.svg` (16px), whose `apps` context is where an icon theme keeps an
|
||||
application's own icon and is what makes `Icon=org.udpscope.app` in the desktop entry resolve.
|
||||
All six are bundled by `src/icons/udpscope.gresource.xml` (CMake runs
|
||||
`glib-compile-resources`) and reached through **one**
|
||||
`gtk_icon_theme_add_resource_path()` in `run_ui()`, right after `adw_init()` — not at the top
|
||||
of `build_toolbar()`, where it used to be: there are two consumers now (the window's own icon,
|
||||
set before the toolbar is built, and the toolbar's buttons) and `run_ui()` is the one place
|
||||
ahead of both. `adw_init()` calls `gtk_init()`, which is what makes
|
||||
`gdk_display_get_default()` valid there.
|
||||
A resource rather than files beside the binary because udpscope is run straight out of the
|
||||
build tree and has no install step. Five authoring rules, each of which fails quietly:
|
||||
* **GTK's symbolic recolour only reaches `fill` on `path`, `rect` and `circle`.** It wraps the
|
||||
SVG in a stylesheet for those selectors, so a `stroke`, a `line` or a `polyline` keeps the
|
||||
colour written in the file and stays dark on a dark theme. Everything is a filled `path`.
|
||||
colour written in the file and stays dark on a dark theme. **In a symbolic icon everything is
|
||||
therefore a filled `path`** — including anything that wants to look stroked, which is drawn as
|
||||
a closed band (two offset curves joined) instead. The rule is scoped to symbolic icons on
|
||||
purpose: `org.udpscope.app.svg` is 128px colour artwork that is never recoloured, so it strokes
|
||||
its sine freely, which is the sane way to draw a smooth one. Do not "fix" one to match the
|
||||
other.
|
||||
* **Whole-pixel coordinates**, because these are drawn at 16 px far more often than not: the
|
||||
first draft placed features on half-pixels and the whole icon was a grey smear at 1x.
|
||||
* `fill-rule="evenodd"` is how the ruler's body is a hole *and* its ticks still fill -- a tick
|
||||
inside the hole is three crossings deep, which is odd. Nonzero would fill the hole back in.
|
||||
* Overlapping subpaths under the default nonzero rule must be **wound the same way** or they
|
||||
cancel instead of uniting. All of them run clockwise.
|
||||
* `--` is illegal inside an XML comment and librsvg refuses the whole file for it.
|
||||
* **A malformed SVG is a build error, and that is the only place it can be caught.** Every
|
||||
`<file>` in `src/icons/udpscope.gresource.xml` carries `preprocess="xml-stripblanks"`, which
|
||||
makes `glib-compile-resources` pipe it through `xmllint` — so `--` inside an XML comment, which
|
||||
is illegal and which librsvg refuses the whole file for, fails the build with a file, a line
|
||||
and a caret under the offending character (`…app-symbolic.svg:5: parser error : Double hyphen
|
||||
within comment`). It has to be here because **no runtime check can see it**: librsvg refuses
|
||||
the file but the icon *name* still resolves, so `gtk_icon_theme_has_icon()` returns true and
|
||||
the icon draws nothing. `gtk_icon_theme_lookup_icon()` cannot tell either (see the `app icons:` line under
|
||||
Verification), `GMarkup` accepts the double hyphen because it is a lenient subset parser, and
|
||||
`gdk_pixbuf` goes through glycin here, which sandboxes a subprocess per load and refuses this
|
||||
project's own icons outright. The cost is that the `xmllint` binary is a build dependency; it
|
||||
ships with libxml2, which GTK already requires.
|
||||
`gtk_icon_theme_has_icon()` is checked for every toolbar button and reported by the dump (see
|
||||
`toolbar icons:` under Verification): a resource path that was never added does not raise an
|
||||
error, it draws `image-missing`.
|
||||
@@ -1319,15 +1394,18 @@ far the largest file.
|
||||
`update_status_popover()` only ever calls `gtk_label_set_text()`, and `popover_rebuilds` in
|
||||
the dump is the assertion.
|
||||
- **The About dialog, the help window and the shortcut overview are opened by `show_about()`,
|
||||
`show_help()` and `show_shortcuts()`, and each closes with the matching call.** The help window
|
||||
and the About dialog are `GtkWindow`s: they close with `gtk_window_close()`, which emits
|
||||
`close-request` so the handler that nulls the file-scope pointer runs. `gtk_window_destroy()`
|
||||
does not emit that signal and must not be used — the math dialog fell into exactly this trap,
|
||||
and the pointer then points at a dead widget until the next attempt to open the same window.
|
||||
The shortcut overview is an `AdwDialog` — it is **not** a `GtkWindow` — closed with
|
||||
`adw_dialog_close()` and cleared on `::closed` (there is no `::close-request` on `AdwDialog`).
|
||||
One `GtkWidget*` (or `AdwDialog*`) per window, nulled in its close handler, is the shape that
|
||||
prevents the double-open defect across all three.
|
||||
`show_help()` and `show_shortcuts()`, and each closes with the matching call.** There is
|
||||
exactly **one `GtkWindow`** among the three — the paged help window. It closes with
|
||||
`gtk_window_close()`, which emits `close-request` so the handler that nulls the file-scope
|
||||
pointer runs. `gtk_window_destroy()` does **not** emit that signal and must not be used — the
|
||||
math dialog fell into exactly this trap, and the pointer then points at a dead widget until the
|
||||
next attempt to open the same window. The other two — the About dialog (an `AdwAboutDialog`)
|
||||
and the shortcut overview (an `AdwShortcutsDialog`) — are **`AdwDialog`s and not `GtkWindow`s**:
|
||||
they close with `adw_dialog_close()` and clear on `::closed`, there being no `::close-request`
|
||||
on an `AdwDialog`. Both share one `on_dialog_closed()` handler keyed by a **pointer to the
|
||||
pointer** it must null (`&g_about`, `&g_shortcuts`), which is what keeps one handler honest for
|
||||
two dialogs. One `GtkWidget*` (or `AdwDialog*`) per window, nulled in its own close handler, is
|
||||
the shape that prevents the double-open defect across all three.
|
||||
- **`build_info.cpp` is generated, never committed.** `cmake/write_build_info.cmake` runs at
|
||||
configure time (so `add_executable` can name the file) and again on every build through
|
||||
`udpscope_build_info` (so the commit and the timestamp belong to the binary that is running).
|
||||
|
||||
@@ -252,6 +252,80 @@ The same reason the window control is a fixed-width label in the header bar
|
||||
not expand: a header-bar child that expands is measured into the title's centre
|
||||
box, so the control stretched to the title and every window resize moved it.
|
||||
|
||||
## The icon, the About dialog and the desktop entry
|
||||
|
||||
**The icon is the instrument face**: a dark panel carrying one bold green trace. That is the
|
||||
clearest metaphor available for this program -- it is what the thing being imitated looks like --
|
||||
and it is the kind of physical object the GNOME HIG's top-and-front depth language was written
|
||||
for, so the guidelines apply as written rather than by analogy. Its silhouette is also what makes
|
||||
it survive being scaled down, which is why two alternatives were dropped: a bare waveform (no
|
||||
silhouette at all, so nothing is left of it in a dock) and a colour version of the trigger glyph
|
||||
the toolbar already carries (a *control*, not the application, and it would mean two icons that
|
||||
have to stay in step).
|
||||
|
||||
It is built to the HIG's own numbers. A 128px canvas the artwork does not fill (it spans
|
||||
x 12..116, y 22..106), every feature on the 2px grid, and **one** depth treatment: a 4px front
|
||||
profile along the bottom, clipped to the chassis silhouette so it does not protrude past the
|
||||
rounded corners. The rest is flat colour -- no gradients, no shadows -- because every surface
|
||||
here is straight, and the palette is GNOME's (Dark 3 chassis, Dark 2 profile, Dark 4 screen,
|
||||
Green 1 trace).
|
||||
|
||||
**Two things were deliberately left out.** There is no graticule: the grid lines a real scope
|
||||
face carries cannot survive 32px, and the trace carries the metaphor without them. And the trace
|
||||
is one cycle -- a single crest, a single trough -- because at 32px the screen is about 22x16 px
|
||||
and anything busier reads as a smudge.
|
||||
|
||||
The two forms are authored differently on purpose. The colour icon **strokes** its sine, which is
|
||||
the sane way to draw a smooth curve; the 16px symbolic one cannot, because GTK's symbolic
|
||||
recolour wraps the file in a stylesheet that reaches `fill` on `path`, `rect` and `circle` only,
|
||||
so a stroked line keeps the colour written in the file and stays dark on a dark theme. Its wave is
|
||||
therefore a closed 2px band -- a sine along the top edge and the same sine 2px lower along the
|
||||
bottom. Do not "fix" one to match the other.
|
||||
|
||||
Both are compiled into the binary as a `GResource` under `scalable/apps/`, for the same reason
|
||||
the four action icons are: udpscope is run straight out of its build tree and has no install step.
|
||||
The one consequence worth knowing is that a **malformed** SVG cannot be caught at runtime -- the
|
||||
icon *name* still resolves, so GTK reports the icon as present and draws nothing -- which is why
|
||||
every `<file>` in the manifest carries `preprocess="xml-stripblanks"` and the build fails with a
|
||||
file, line and column instead.
|
||||
|
||||
**The About dialog is an `AdwAboutDialog`** rather than a `GtkAboutDialog`, and that migration is
|
||||
what the app has an icon *for*: the dialog shows it in its own header, which `GtkAboutDialog`
|
||||
would too but only after being handed one, and nothing else in the program had asked for a
|
||||
128px rendering. Two further gains came with it. The debug information is a **Troubleshooting
|
||||
page with a copy button**, where `system_information` on the old dialog was inert text a user
|
||||
reporting a problem had to retype. And the string it shows is a `std::string`, which removed a
|
||||
`char[1024]` that truncated a long settings path in silence -- the one failure mode of a fixed
|
||||
buffer that no one notices until the path matters.
|
||||
|
||||
What it cost: an `AdwAboutDialog` is an `AdwDialog` and **not** a `GtkWindow`, so its close path
|
||||
differs from the help window sitting beside it -- `adw_dialog_close()` and `::closed`, rather than
|
||||
`gtk_window_close()` and `::close-request`. That is the same trap the shortcut overview already
|
||||
had, and the two now share one `::closed` handler keyed by the pointer it must null. What was
|
||||
deliberately left out: credits (one author, already named), release notes (there are no releases)
|
||||
and an issue-tracker URL (the website link reaches the repository, which carries the tracker).
|
||||
|
||||
**The desktop entry is opt-in.** `cmake --build build` never touches `$XDG_DATA_HOME`; asking for
|
||||
it does:
|
||||
|
||||
```sh
|
||||
cmake --build build --target desktop-entry
|
||||
```
|
||||
|
||||
That writes `org.udpscope.app.desktop` and both app icons under `$XDG_DATA_HOME` (default
|
||||
`~/.local/share`), validates the entry with `desktop-file-validate` as a hard failure, refreshes
|
||||
`update-desktop-database` and `gtk4-update-icon-cache` if they are on `PATH`, and prints the three
|
||||
paths it wrote. Those three lines **are** the uninstall instructions: there is no uninstall
|
||||
target, because three `rm`s the build already told you about are less to maintain than a target
|
||||
that has to guess what a previous version installed.
|
||||
|
||||
The caveat is the entry's `Exec`, which is the built binary's **absolute path in the build tree**:
|
||||
moving or deleting `build/` breaks the launcher. That is the price of having no install step, and
|
||||
it is the right trade here -- the entry then always launches exactly the binary that was last
|
||||
built, which for a program under development is what you want. No `StartupWMClass` is needed:
|
||||
GTK takes the WM class from the application ID, and the desktop file's basename is that same
|
||||
string, which is what associates a running window with the entry.
|
||||
|
||||
## How the spec maps onto the code
|
||||
|
||||
**One buffer of `N+1` slices, plus frozen event snapshots.** `ScopeModel`
|
||||
|
||||
Reference in new issue
Block a user