help: default the contextual layer off, and carry the help into the chooser
The contextual help was on by default, so a first run met a window with a "?" beside every surface it had not asked to have explained; `contextual_help` now defaults to false and the layer is asked for from the Help menu, from the chooser's new help menu, or with Ctrl+H. The source chooser runs before `App` exists, which makes it the window a user who does not know how to start is looking at, so it now carries the same three help affordances the window's Help view does: Documentation (the manual is another process and needs nothing of the app), Enable contextual help (a row whose check *is* the setting, written to the file the app reads a moment later), and Guided tour. The tour is drawn on the app's own window, so the chooser can only record the wish: it sets `Config::tour_pending` -- the value `main()` owns and hands to both `choose_source()` and `run_ui()` -- and `on_activate()` starts it once the window is up and laid out, through its own callback so the readings can tell a request from the first-run auto-start. The rows are the window's own `menu_button()`/`menu_toggle()`; `install_css()` therefore runs from `choose_source()` (it is idempotent now) so the popover gets the `.udpscope-menu` rules before it is shown. Opening the manual could also fail with a message that blamed the handler whatever was missing. `help_installed_path()` now resolves `help:<id>` the way the specification does and the failure alert says which of the two it is: with the set installed it names the viewer, without it it names the identifier, the expected installed path and the install command. `show_help()` gained an optional reporter, which `UDPSCOPE_PROBE_HELP_MS` uses for a new reading -- `[help] launch attempted= done= ok= error='' installed=` -- since a successful launch leaves no window of ours to observe. Tested: clean --clean-first build, three warnings as documented; test-ui 13/13 and test-help pass; no Gtk-CRITICAL in the help, helpui and chooser runs.
This commit is contained in:
1 parent
e140642ed0
commit
d8a79b96ba
15 files changed
+461
-66
No files matched your search
@@ -730,7 +730,11 @@ same session twice). Connector settings are stored even for a configuration with
|
||||
which is why those rows exist at all. The app's
|
||||
menu has **Configurations…**: the same list for the running source, with *Save this session* —
|
||||
a name and a note — and a delete button per row. Opening one saves the current session first, so
|
||||
switching is never the gesture that loses what it switches from.
|
||||
switching is never the gesture that loses what it switches from. Beside **Preferences…** the chooser
|
||||
also carries a **help menu** with the window's three help entries — the manual, the contextual-help
|
||||
toggle and the guided tour — because the chooser is shown to exactly the user who has not started yet
|
||||
and does not know how to; the tour is started by the app once its window is up (see
|
||||
[the help section](#the-help-submenu-the-contextual-help-and-the-guided-tour)).
|
||||
|
||||
A session written before configurations existed is imported on first use as the configuration
|
||||
named `default`: copied into the directory, remembered as active, and the old file renamed to
|
||||
@@ -901,14 +905,32 @@ button is a sibling of the heading's title, not an overlay on its corner, for th
|
||||
toolbar's icons are not overlaid either: an overlay child sits *on* whatever is under it, and the
|
||||
one surface whose corner is free (the plot) is the one that uses one.
|
||||
|
||||
**Off means gone, not greyed.** The toggle removes the buttons rather than disabling them, and
|
||||
which buttons exist is decided by a data key rather than a list of pointers, so the math editor --
|
||||
whose heading is created afresh every time it opens -- cannot leave a stale pointer behind. The
|
||||
setting is `contextual_help` in `settings.toml`, written by
|
||||
**Off means gone, not greyed, and off is the default.** The toggle removes the buttons rather than
|
||||
disabling them, and which buttons exist is decided by a data key rather than a list of pointers, so
|
||||
the math editor -- whose heading is created afresh every time it opens -- cannot leave a stale
|
||||
pointer behind. The setting is `contextual_help` in `settings.toml`, written by
|
||||
`Settings`/`load_settings()`/`save_settings()`: the same file, the same three functions and the same
|
||||
`~/.config/udpscope/settings.toml` the Preferences dialog writes. The dialog's own rows start from
|
||||
`load_settings()`, so a save from there carries the two help keys through untouched; the Help menu's
|
||||
row is the only place that states `contextual_help`, which is why the row's check *is* the setting.
|
||||
`~/.config/udpscope/settings.toml` the Preferences dialog writes. **A fresh install has no such key
|
||||
and the default is `false`**, so a first run shows the window as anyone would expect it and the layer
|
||||
is asked for -- from the Help menu, from the source chooser's own help menu, or with `Ctrl+H`. The
|
||||
dialog's own rows start from `load_settings()`, so a save from there carries the two help keys
|
||||
through untouched; the Help menu's row is the only place in the *window* that states `contextual_help`
|
||||
(which is why the row's check *is* the setting), and the chooser's row states it too, before any
|
||||
window exists.
|
||||
|
||||
**The source chooser carries the same three affordances, because it is shown to exactly the user who
|
||||
has not started yet.** Its header bar has a help `GtkMenuButton` beside *Preferences*, whose rows are
|
||||
the window's own `menu_button()`/`menu_toggle()` -- the same shape, and the same `.udpscope-menu`
|
||||
stylesheet, whose rules `choose_source()` installs before building them since the app installs them
|
||||
later. *Documentation* opens the manual directly (it is another process and needs nothing of the app),
|
||||
*Enable contextual help* writes the settings file the app reads a moment later, and *Guided tour*
|
||||
cannot start anything -- the tour is drawn on the app's window, which does not exist yet -- so it sets
|
||||
**`Config::tour_pending`**, the value `main()` owns and hands to both the chooser and the front end
|
||||
(`open_waveform` crosses the same way). Half a second and a half after the window is up, the same
|
||||
delay the first-run auto-start uses, the pending request starts the tour; it sets `tour_requested`
|
||||
rather than `tour_auto_started`, so the readings say which of the two it was. The chooser's rows show
|
||||
**no accelerators** -- the window prints `F1`/`Ctrl+T` because the window binds them, and this dialog
|
||||
binds none.
|
||||
|
||||
**The guided tour is five steps on a `GtkOverlay`** inserted above the window's content: the plot,
|
||||
the side panel, the trigger, the signals and the status line. There is no popover anywhere in it and
|
||||
@@ -2439,7 +2461,14 @@ identifier `org.udpscope.app` is the desktop file's own basename, so it is the `
|
||||
GApplication id and the install directory at once. The set is installed by an `install()` rule to
|
||||
`share/help/C/org.udpscope.app/` — the path the freedesktop Help System Specification's `help:` URI
|
||||
resolves — and `show_help()` hands `help:org.udpscope.app` to whatever the desktop registered for the
|
||||
scheme (Yelp on GNOME), reporting a launch failure with an alert rather than failing silently.
|
||||
scheme (Yelp on GNOME), reporting a launch failure with an alert rather than failing silently. That
|
||||
alert says **which** thing is missing, because the two have different fixes: `help_installed_path()`
|
||||
looks for the document under `$XDG_DATA_HOME/help/<lang>/org.udpscope.app/` then
|
||||
`$XDG_DATA_DIRS/help/<lang>/org.udpscope.app/`, so with the set installed the alert names the missing
|
||||
*viewer* (`yelp`), and without it the alert names the identifier, the expected installed path and the
|
||||
install command — `cmake --install build --prefix ~/.local`, which writes
|
||||
`~/.local/share/help/C/org.udpscope.app/`. A build tree has neither the installed copy nor a
|
||||
`help:` handler, and **either is enough to make `F1` do nothing**; that is the failure this names.
|
||||
`cmake --build build --target test-help` checks the set the way `test-ui` checks the `.ui` files:
|
||||
`yelp-check`'s offline subcommands (links, media, orphans, ids) and a `yelp-build html` render;
|
||||
`yelp-check validate` is left out because it fetches the Mallard RelaxNG schema over the network.
|
||||
@@ -2447,7 +2476,11 @@ scheme (Yelp on GNOME), reporting a launch failure with an alert rather than fai
|
||||
(GTK4 has to be forced to the Cairo renderer or X11 reads the surface back blank).
|
||||
`UDPSCOPE_PROBE_HELP_MS` asserts the set is *present in the tree* — its
|
||||
`[help] topics=… index=… figures=…` reading — rather than parsing pages, since they are files now
|
||||
and not resources. Mallard is translatable with `itstool`, but only the `C` set is authored here.
|
||||
and not resources, and it **launches the manual too**: `[help] launch attempted=1 done=1 ok=1
|
||||
error='' installed=/home/…/.local/share/help/C/org.udpscope.app` is the reading, `ok=1` meaning the
|
||||
`help:` URI reached a registered handler and not that a window appeared (Yelp is another process and
|
||||
cannot map one headlessly). Mallard is translatable with `itstool`, but only the `C` set is authored
|
||||
here.
|
||||
|
||||
**The status bar is two tiers.** The bar itself shows one glance line: the activity badge, the
|
||||
display state (one word from a fixed set — `connecting`, `live`, `paused`, `armed`, `capturing`,
|
||||
|
||||
@@ -127,7 +127,10 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
honest without knowing which dialog it is. One `GtkWidget*` (or `AdwDialog*`) per window, nulled
|
||||
in its own close handler, is the shape that prevents the double-open defect. `close_help_windows()`
|
||||
(misnamed now) closes just the two dialogs; a failed *launch* is reported by the launcher's own
|
||||
callback with a `GtkAlertDialog`, so a click that cannot open the manual says so.
|
||||
callback with a `GtkAlertDialog`, so a click that cannot open the manual says so. That callback is
|
||||
also `show_help()`'s **optional reporter** (`std::function<void(bool ok, const std::string& error)>`)
|
||||
— the one hook that runs on the success path, where no window of ours is left to observe; the probe
|
||||
uses it and nothing else does.
|
||||
- **`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).
|
||||
@@ -149,7 +152,21 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
→ `org.udpscope.app`), so it is the `.desktop` name, the GApplication id and the install
|
||||
directory at once; `UDPSCOPE_HELP_NAME` and `UDPSCOPE_HELP_DIR` are compiled in from that one
|
||||
place. `show_help()` opens `help:<id>` with `GtkUriLauncher` (Yelp runs in its own process, so
|
||||
nothing here holds a pointer to it), and its failure callback shows a `GtkAlertDialog`.
|
||||
nothing here holds a pointer to it), and its failure callback shows a `GtkAlertDialog`. That
|
||||
failure is **careful about which thing is missing**: `gtk_uri_launcher_launch_finish()` reports a
|
||||
launch that could not be handed over — the *handler's* absence — and cannot tell that from a
|
||||
document that was never installed, so `help_installed_path()` walks the specification's own
|
||||
`$XDG_DATA_HOME/help/<lang>/<id>/` then `$XDG_DATA_DIRS/help/<lang>/<id>/` for an `index.page` and
|
||||
the dialog says which one it is: with the manual there, install a viewer (for example `yelp`); with
|
||||
it absent, the identifier, the expected installed path and the install command
|
||||
(`cmake --install build --prefix ~/.local`, which writes `~/.local/share/help/C/<id>/`) — and a
|
||||
viewer as well, the second possibility the error alone cannot rule out. Reporting a missing
|
||||
*document* as a missing *handler* is the defect that shape avoids.
|
||||
|
||||
`UDPSCOPE_PROBE_HELP_MS` launches it too, and its `[help] launch attempted= done= ok= error=''
|
||||
installed=` line is the reading — `ok=1` is the URI handed to a viewer and not a window on screen,
|
||||
since Yelp is another process and cannot map one headlessly; `installed=` sits beside it because a
|
||||
failing `ok=0` has to be read against whether the document was there at all.
|
||||
|
||||
`UDPSCOPE_PROBE_HELP_MS` no longer parses pages — they are files, not markup in a resource — so
|
||||
its `[help]` line asserts the set is *there*: `topics` (the `.page` files other than
|
||||
@@ -163,6 +180,25 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
(GTK4 must be forced to the Cairo renderer, `GSK_RENDERER=cairo`, or the X11 surface reads back
|
||||
blank). Mallard is translatable with `itstool`, but there is no i18n set-up here: only the `C`
|
||||
set is authored.
|
||||
- **The chooser carries the same help affordances as the window, and the tour request crosses as a
|
||||
value — not a global.** The source chooser runs *before* `App` exists, so a user who has not started
|
||||
yet is the one least able to find the help; its header bar therefore has a help `GtkMenuButton`
|
||||
(the `.ui` holds the button, code holds the rows) whose three entries are the window's Help view's
|
||||
the same way — `ui_impl::menu_button()`/`menu_toggle()`, the app's own row shape — with the
|
||||
`.udpscope-menu` class on the popover. That class's rules only exist once `install_css()` has run, so
|
||||
`choose_source()` calls it before building the rows (`install_css()` is idempotent precisely because
|
||||
it now has two callers; the app's `on_activate()` still calls it and is a no-op by then). The rows
|
||||
show **no accelerators**: the window's Help view prints `F1`/`Ctrl+T` because the window binds them,
|
||||
and this dialog binds none, so a key printed here would be documentation that does nothing.
|
||||
*Documentation* calls `show_help()` straight from the chooser (the manual is another process and
|
||||
needs nothing of the app); *Enable contextual help* writes the settings file, which is what the app
|
||||
reads when it starts a moment later; *Guided tour* cannot start anything — the tour is drawn on the
|
||||
app's own window — so it sets **`Config::tour_pending`**, the one value `main()` owns and hands to
|
||||
both `choose_source()` and `run_ui()` (`open_waveform` and the preferences' relative time cross the
|
||||
same way). `on_activate()` schedules `tour_start_requested()` 1.5 s later, the same delay the
|
||||
first-run auto-start uses, and **that callback is separate from `tour_autostart()` on purpose**: it
|
||||
sets `tour_requested`, not `tour_auto_started`, so `help layer:` says which of the two started the
|
||||
tour instead of conflating a user's request with a first run.
|
||||
- **A `GtkPaned` hands its start child at least its minimum unless the position says otherwise.**
|
||||
Putting the events pane in a paned is what makes its width the user's to drag -- but with the
|
||||
position left alone, `shrink-start-child` or not, the pane's minimum is reserved as an empty column
|
||||
|
||||
@@ -395,17 +395,21 @@ Two lines of the self-check output are assertions in disguise:
|
||||
on the first tick for the same reason the toolbar's are. **Both must read `(ok)`**: the page titles are
|
||||
empty on purpose, so an icon that does not resolve leaves a *blank* pill, and a blank pill looks
|
||||
exactly like a page with nothing in it.
|
||||
- `help layer: contextual=on hints=8 ids={events plot status cursors measures trigger signals window} icon=dialog-question-symbolic(ok); tour=idle steps=5 step=0 auto=1` (in the exit
|
||||
- `help layer: contextual=on hints=8 ids={events plot status cursors measures trigger signals window} icon=dialog-question-symbolic(ok); tour=idle steps=5 step=0 auto=1 requested=0` (in the exit
|
||||
dump, beside `switcher:`) — the help affordance layer as the last tick found it. **`hints=` must equal
|
||||
the number of ids**, and the ids are the surfaces the contextual help is on: the eight of the window
|
||||
itself, and `math` only while the math editor is open (its heading lives in a builder that is rebuilt
|
||||
on every opening, so it is not one of the window's own). `contextual=off` beside `hints=0` is the
|
||||
toggle's other state — **off is *gone*, not greyed**, so a non-zero count with `off` is a row that hid
|
||||
its buttons instead of removing them. **`icon=` must read `(ok)`**: a `?"` whose name Adwaita lacks is a
|
||||
on every opening, so it is not one of the window's own). The layer is **off by default**
|
||||
(`contextual_help` in `settings.toml` is false for a missing file), so a fresh-config run reads
|
||||
`contextual=off hints=0` and that line above is the state *after* the Help row was switched on;
|
||||
`contextual=off` beside a non-zero count is the toggle not having removed its buttons — **off is
|
||||
*gone*, not greyed**. **`icon=` must read `(ok)`**: a `?"` whose name Adwaita lacks is a
|
||||
*blank* control that still works, the failure the pane [edit] buttons already paid for once. `tour=`
|
||||
is `running`/`idle` with the plan's `steps=` and the current `step=`, and `auto=1` is the first-run
|
||||
is `running`/`idle` with the plan's `steps=` and the current `step=`; `auto=1` is the first-run
|
||||
auto-start having fired *in this process* — the flag is `tour_seen` in `settings.toml`, so a second run
|
||||
against the same config directory reads `auto=0`.
|
||||
against the same config directory reads `auto=0` — and `requested=1` is the **chooser's** "Guided tour"
|
||||
(`Config::tour_pending`) having been carried into this run instead, which is `auto=0` because it is not
|
||||
the auto-start.
|
||||
- `help figures: trigger_head=X,Y,W,H trigger_summary=… trigger_panel=… signal_list=… side_switcher=… side_stack=… window_button=… status_label=… header_bar=… plot=…` (in the exit
|
||||
dump, beside `help layer:`) — the **window-coordinate** rectangle of each surface the help's annotated
|
||||
figures label, `x,y,width,height`, or `-1,-1,0,0` for a surface that is **not on screen**. It is
|
||||
@@ -634,7 +638,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] topics=14 index=1 figures=3 dir=/…/help/C/org.udpscope.app; shortcuts: app=14 plot=14 tables=28 ui=28 ui_accel=21 ui_icon=7 ui_bare=0; about version=… commit=… built=… settings=… icon=org.udpscope.app; closed ok` (stderr, `UDPSCOPE_PROBE_HELP_MS`) — the manual is a Mallard set on disk now, not a GResource in the binary, so this is a **file-system reading**: `topics` is the `.page` files other than `index.page`, `index` must be 1, and `figures` counts the screenshots under `figures/`. **`topics=0 index=0` means the manual is missing** from `dir` (the source directory the build was configured from, `UDPSCOPE_HELP_DIR`) — the finding the old `parsed != pages` used to name. The manual itself opens in Yelp, another process, which this probe does not and cannot exercise. The `shortcuts:` numbers are the shortcut overview four ways.
|
||||
- `[help] topics=14 index=1 figures=3 dir=/…/help/C/org.udpscope.app; shortcuts: app=14 plot=14 tables=28 ui=28 ui_accel=21 ui_icon=7 ui_bare=0; about version=… commit=… built=… settings=… icon=org.udpscope.app; closed ok` (stderr, `UDPSCOPE_PROBE_HELP_MS`) — the manual is a Mallard set on disk now, not a GResource in the binary, so this is a **file-system reading**: `topics` is the `.page` files other than `index.page`, `index` must be 1, and `figures` counts the screenshots under `figures/`. **`topics=0 index=0` means the manual is missing** from `dir` (the source directory the build was configured from, `UDPSCOPE_HELP_DIR`) — the finding the old `parsed != pages` used to name. The manual itself opens in Yelp, another process, which the probe now launches and reports (the `[help] launch` line below). The `shortcuts:` numbers are the shortcut overview four ways.
|
||||
`app` and `plot` are the rows in the C tables -- what the application actually *handles* -- and `tables`
|
||||
is their sum. `ui`, `ui_accel`, `ui_icon` and `ui_bare` are what `src/ui/shortcuts.ui` *declares*, read
|
||||
out of the resource with a markup parse rather than off the built widgets, because a tree walk counts
|
||||
@@ -642,7 +646,8 @@ every row twice: `GtkShortcutsWindow` reaches each one from more than one place
|
||||
**`ui` must equal `tables`** -- two lists of one thing, and a row in one but not the other is a gesture
|
||||
that works and is not documented, or is documented and does nothing -- and **`ui_bare` must be zero**: a
|
||||
row with no accelerator and no icon is a cell reading "No Shortcut". `ui_accel + ui_icon == ui`, so those
|
||||
two also catch a row whose accelerator went missing. `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 the two dialogs built, presented and closed in one run as it is a state check: they are closed by **two different calls**, so a mismatched pair crashes or leaks here. One is a `GtkWindow` (the shortcut overview, `gtk_window_close()`/`::close-request`); the About dialog is an `AdwDialog` and closes with `adw_dialog_close()`, cleared on `::closed`. The manual is *not* one of them — it opens in Yelp, a separate process, so there is nothing here to open, read back or close.
|
||||
two also catch a row whose accelerator went missing. `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 the two dialogs built, presented and closed in one run as it is a state check: they are closed by **two different calls**, so a mismatched pair crashes or leaks here. One is a `GtkWindow` (the shortcut overview, `gtk_window_close()`/`::close-request`); the About dialog is an `AdwDialog` and closes with `adw_dialog_close()`, cleared on `::closed`. The manual is *not* one of them — it opens in Yelp, a separate process, so there is no window of ours to read back or close; the line below is the reading of that path instead.
|
||||
- `[help] launch attempted=1 done=1 ok=1 error='' installed=/home/…/.local/share/help/C/org.udpscope.app` (stderr, `UDPSCOPE_PROBE_HELP_MS`, a moment after the line above) — the **launch of the manual**, the one part of the help path no other reading reaches: a successful launch leaves no window of ours, so nothing else can say whether `show_help()` did anything at all. `attempted=` is set at the call (`0` would be a probe that never got as far as calling it); `done=` says the asynchronous callback ran, and **`done=0` is the reading for a launch that never called back**; `ok=1` is the `help:` URI handed to a viewer, `ok=0` with the launcher's `error=` text is a launch that failed. **`ok` is not "a window appeared"**: Yelp is another process, and under a headless X server it cannot map one — the launch is *this* process's half (the URI reached a registered handler), which is exactly what the desktop does when a user presses F1, and what an absent handler or an uninstalled viewer breaks. `installed=` is `help_installed_path()`'s answer — the `$XDG_DATA_HOME`/`$XDG_DATA_DIRS` directory the document was found in, or `(not found)` — and it is what a failing `ok=0` should be read beside, because the launch error alone cannot tell a missing document from a missing handler. On a machine with the set installed by `cmake --install build --prefix ~/.local` and `yelp` present, the line reads `ok=1 … installed=~/.local/share/help/C/org.udpscope.app`. A **failure** leaves the launcher's `GtkAlertDialog` up on purpose — that is what a user gets, and the probe must not report a failed path as a silence.
|
||||
- `[menu] N 'label' accel='key' h=P label_x=X accel_right=R/W order=D ok=O`, one line per row,
|
||||
then `[menu] rows=R expected=E order=D rows_bad=B popover_visible=V mapped=M button_mapped=B`
|
||||
and `[menu] status pos=top bubble=WxH button_top=T button_right=R fits=F` (stderr,
|
||||
@@ -671,7 +676,10 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
|
||||
run against the same config directory, which is the assertion that the flag persisted. Then:
|
||||
- `[helpui] contextual on: hints=8 ids={events plot status cursors measures trigger signals window}` —
|
||||
the eight "?" affordances, by id. The layer's ninth surface, `math`, is not here: its host exists
|
||||
only while the math editor window is open.
|
||||
only while the math editor window is open. **On a fresh config this reads `hints=0 ids={}`**, because
|
||||
the layer is off by default; the example is the state after the row below was switched on, and a
|
||||
fresh run's `hints=0` beside a `settings.toml` with no `contextual_help` key is the default being
|
||||
read rather than assumed.
|
||||
- `[helpui] main view: rows=7 help_row=1` and one `[helpui] entry N '<label>' accel='<key>' order=O`
|
||||
per row of the **Help view** — `entry 0 'Back'` with no accelerator and then the four the submenu
|
||||
promises: `entry 1 'Keyboard shortcuts' accel='Ctrl+/'`, `entry 2 'Documentation' accel='F1'`,
|
||||
@@ -679,7 +687,9 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
|
||||
every row** is the assertion; the view is entered by emitting `::clicked` on the menu's own `Help`
|
||||
row, so the row's handler is exercised and not just the stack.
|
||||
- `[helpui] help view: rows=5 expected=5 rows_bad=0 back=1 toggle=1 entries='…'` — the summary, with
|
||||
`toggle=1` being the contextual-help row's check, which *is* `settings.contextual_help`.
|
||||
`toggle=` being the contextual-help row's check, which *is* `settings.contextual_help`. It reads `0`
|
||||
on a fresh config (the default) and `1` on one where the setting is on; the probe's next step drives
|
||||
it both ways, which is why the example here is the `1` side.
|
||||
- `[helpui] toggle: off -> hints=0 ids={}; on -> hints=8 ids={…}; setting=on` — the toggle's effect
|
||||
read both ways through the row itself. **`hints=0` with `off` is the assertion**: off means the
|
||||
affordances are *gone*, not greyed, so anything else is a row that hid its buttons instead of
|
||||
@@ -1088,7 +1098,10 @@ configuration that carries settings, is the defect this exists for — a saved h
|
||||
## `[sources] waveforms` and the waveform load
|
||||
|
||||
```
|
||||
[sources] chooser ui: page='sources' title='Sources' icon=yes page='waveforms' title='Waveforms' icon=yes visible='sources' switcher_wired=1 buttons=[prefs=1 new_host=1 open=1 cancel=1]
|
||||
[sources] chooser ui: page='sources' title='Sources' icon=yes page='waveforms' title='Waveforms' icon=yes visible='sources' switcher_wired=1 buttons=[prefs=1 new_host=1 open=1 cancel=1] help=[button=1 docs=1 tour=1 toggle=1 on=0]
|
||||
[sources] chooser help: tour requested
|
||||
[sources] chooser help: tour_pending=1
|
||||
[sources] chooser help: toggle=1 setting=1 file=/tmp/…/udpscope/settings.toml
|
||||
[sources] waveforms: 1 recent /tmp/rt.npy(ok) page=waveforms
|
||||
[sources] picked waveform /tmp/rt.npy
|
||||
[udpscope] waveform loaded: /tmp/rt.npy signals=4 samples=20001 rows=20001 decim=1 span=2.000000s rate=10000 time=[-2.000000..0.000000]
|
||||
@@ -1097,7 +1110,7 @@ configuration that carries settings, is the defect this exists for — a saved h
|
||||
|
||||
| field | value it must read | the defect a wrong reading names |
|
||||
| --- | --- | --- |
|
||||
| `chooser ui:` | both pages with a `title` and an `icon=yes`, `switcher_wired=1`, and all four buttons `1` | the chooser's `.ui` losing an id: the code names the missing one and refuses the window, and this line is the frame that *did* build -- a page with `icon=NO` is one the switcher cannot draw |
|
||||
| `chooser ui:` | both pages with a `title` and an `icon=yes`, `switcher_wired=1`, all four buttons `1`, and `help=[button=1 docs=1 tour=1 toggle=1 on=0]` | the chooser's `.ui` losing an id: the code names the missing one and refuses the window, and this line is the frame that *did* build -- a page with `icon=NO` is one the switcher cannot draw, and a `help=` field at `0` is a help row (or the button that opens it) the dialog no longer carries |
|
||||
| `waveforms:` | one entry per remembered export, with `(ok)` or `(missing)`, and `page=waveforms` once the page is shown | the Waveforms page not built, or a stale path offered as if it were openable |
|
||||
| `picked waveform` | the selected path | a pick that carries no file: `--open` is what the mode reads |
|
||||
| `signals=`, `rows=` | the export's `cols=` and `rows=` | a reader mis-slicing its records, or the columns bound to the wrong channels |
|
||||
@@ -1106,6 +1119,21 @@ configuration that carries settings, is the defect this exists for — a saved h
|
||||
| `span=`, `rate=`, `time=[…]` | the exported window and rate, `[-window, 0]` | the plan not covering the whole file (the tail arrives as the whole picture) or the display epoch not pinned to the file's last stamp |
|
||||
| `frames=0 packets=0` beside `channels=N` | 0 — no acquisition | a socket opened for a file, which would also make the starvation notice live |
|
||||
|
||||
The chooser's **help menu** — `Documentation`, `Guided tour` and `Enable contextual help`, the three
|
||||
affordances the window's Help view carries, moved in front of a user who has not started yet — has two
|
||||
probe legs, both of which then fall through to the normal first-row pick so the run they affect is the
|
||||
run they report:
|
||||
|
||||
- `UDPSCOPE_PROBE_CHOOSER_TOUR=1` emits `::clicked` on `Guided tour`, and `[sources] chooser help:
|
||||
tour_pending=1` reads the request back out of the `Config` the run will be handed. **`tour_pending=1`
|
||||
is the assertion**: the tour is drawn on a window that does not exist yet, so the entry can only work
|
||||
by setting that field, and the reading it *started* is the exit dump's `help layer:` — `tour=running
|
||||
… auto=0 requested=1`, `requested=1` being this path (see `help layer:` above).
|
||||
- `UDPSCOPE_PROBE_CHOOSER_HELP=1` sets the `Enable contextual help` row active and reads the settings
|
||||
file back: `toggle=1 setting=1 file=<settings.toml>`. **`setting=1` beside `toggle=1`** is the pair
|
||||
that matters — the row *is* the setting, and the app is built from the file and not from the dialog,
|
||||
so the file is where it has to have landed.
|
||||
|
||||
## `[drop-probe]`
|
||||
|
||||
```
|
||||
|
||||
@@ -59,6 +59,13 @@ struct Config {
|
||||
/// live source. File mode and source mode are exclusive -- there is no acquisition behind a file,
|
||||
/// and the chooser's Waveforms page sets this rather than the connection fields.
|
||||
std::string open_waveform;
|
||||
|
||||
/// The source chooser's "Guided tour" entry. The tour is drawn on the application's own window,
|
||||
/// which does not exist while the chooser runs, so the request rides on the one value `main()`
|
||||
/// owns and hands to both the chooser and the front end (`open_waveform` and the preferences'
|
||||
/// relative time cross the same way) and the window starts the tour once it is up and laid out.
|
||||
/// It is *not* a preference: it describes this request and nothing else, so nothing persists it.
|
||||
bool tour_pending = false;
|
||||
};
|
||||
|
||||
/// Parses argv. On `--help` prints usage to stdout and exits(0); on a bad
|
||||
|
||||
+84
-21
@@ -3,7 +3,9 @@
|
||||
|
||||
#include <adwaita.h>
|
||||
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "build_info.hpp"
|
||||
#include "layout_store.hpp"
|
||||
@@ -44,27 +46,55 @@ void on_dialog_closed(AdwDialog* dialog, gpointer slot) {
|
||||
*static_cast<AdwDialog**>(slot) = nullptr;
|
||||
}
|
||||
|
||||
/// The launch is asynchronous and can fail -- no help viewer installed, no handler for the
|
||||
/// `help:` scheme, or a document no viewer can find -- and it runs in another process, so
|
||||
/// nothing raises a window of ours. Report the failure here rather than leaving a click that
|
||||
/// silently did nothing; the detail names the identifier a viewer looks for.
|
||||
/// What an asynchronous launch needs to survive the call: the window it is attributed to (held,
|
||||
/// because the call outlives the stack frame that made it) and the caller's reporter, if it asked
|
||||
/// for one. One small struct rather than two `gpointer`s, because a callback that takes both through
|
||||
/// one `user_data` still has to get them there.
|
||||
struct HelpLaunch {
|
||||
GtkWindow* parent = nullptr;
|
||||
std::function<void(bool, const std::string&)> done;
|
||||
};
|
||||
|
||||
/// The launch failed. Report it rather than leaving a click that silently did nothing -- and say
|
||||
/// *which* thing is missing: `gtk_uri_launcher_launch_finish()` reports that the URI could not be
|
||||
/// handed over, which is the handler's absence, while a document that is not installed is a
|
||||
/// different failure the launcher cannot tell it from. `help_installed_path()` settles that from the
|
||||
/// file system, so a missing *document* is not reported as a missing *handler*: with the manual
|
||||
/// installed the message names the viewer, and without it the message names the document, its
|
||||
/// expected path and the install command -- and a viewer as well, because that second possibility
|
||||
/// cannot be ruled out from here.
|
||||
void on_help_launched(GObject* source, GAsyncResult* result, gpointer user) {
|
||||
GtkWindow* parent = user != nullptr ? GTK_WINDOW(user) : nullptr;
|
||||
auto* launch = static_cast<HelpLaunch*>(user);
|
||||
GError* error = nullptr;
|
||||
if (!gtk_uri_launcher_launch_finish(GTK_URI_LAUNCHER(source), result, &error)) {
|
||||
const bool ok = gtk_uri_launcher_launch_finish(GTK_URI_LAUNCHER(source), result, &error);
|
||||
if (!ok) {
|
||||
const std::string installed = help_installed_path();
|
||||
GtkAlertDialog* alert = gtk_alert_dialog_new("Could not open the help manual");
|
||||
const std::string detail =
|
||||
std::string(error->message) +
|
||||
"\n\nThe manual is installed under $XDG_DATA_DIRS/help/C/help:" UDPSCOPE_HELP_NAME
|
||||
"; a desktop help viewer (for example `yelp`) that handles the `help:` URI is needed "
|
||||
"to read it.";
|
||||
std::string detail = error != nullptr ? error->message : std::string("the launch failed");
|
||||
detail += "\n\n";
|
||||
if (!installed.empty()) {
|
||||
detail += "The manual is installed at " + installed +
|
||||
", so what is missing is a viewer for the `help:` URI it is opened with: install "
|
||||
"one (for example `yelp`) or make it the desktop's default for that scheme.";
|
||||
} else {
|
||||
detail += "The manual does not look installed: nothing named \"help:" UDPSCOPE_HELP_NAME
|
||||
"\" was found under $XDG_DATA_HOME/help/<lang>/" UDPSCOPE_HELP_NAME "/ or "
|
||||
"$XDG_DATA_DIRS/help/<lang>/" UDPSCOPE_HELP_NAME "/. Install it with\n\n"
|
||||
" cmake --install build --prefix ~/.local\n\n"
|
||||
"which writes ~/.local/share/help/C/" UDPSCOPE_HELP_NAME "/ -- the directory "
|
||||
"`help:" UDPSCOPE_HELP_NAME "` resolves to. If there is no `help:` viewer either, "
|
||||
"install one as well (for example `yelp`).";
|
||||
}
|
||||
gtk_alert_dialog_set_detail(alert, detail.c_str());
|
||||
gtk_alert_dialog_show(alert, parent);
|
||||
gtk_alert_dialog_show(alert, launch->parent);
|
||||
g_object_unref(alert);
|
||||
g_error_free(error);
|
||||
}
|
||||
// `parent` was referenced for the duration of the async call.
|
||||
if (parent != nullptr) g_object_unref(parent);
|
||||
if (launch->done) {
|
||||
launch->done(ok, ok || error == nullptr ? std::string() : std::string(error->message));
|
||||
}
|
||||
if (launch->parent != nullptr) g_object_unref(launch->parent);
|
||||
delete launch;
|
||||
if (error != nullptr) g_error_free(error);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -108,6 +138,37 @@ const char* about_icon_name() {
|
||||
return adw_about_dialog_get_application_icon(ADW_ABOUT_DIALOG(g_about));
|
||||
}
|
||||
|
||||
std::string help_installed_path() {
|
||||
// The freedesktop Help System Specification's own search order: `$XDG_DATA_HOME` first, then every
|
||||
// `$XDG_DATA_DIRS` entry in order. Yelp resolves `help:<id>` the same way, so this is a reading of
|
||||
// where the document *would* be found rather than a second, private convention.
|
||||
std::vector<std::string> roots;
|
||||
roots.emplace_back(g_get_user_data_dir() != nullptr ? g_get_user_data_dir() : "");
|
||||
const char* const* dirs = g_get_system_data_dirs();
|
||||
for (int i = 0; dirs != nullptr && dirs[i] != nullptr; ++i) roots.emplace_back(dirs[i]);
|
||||
for (const std::string& root : roots) {
|
||||
if (root.empty()) continue;
|
||||
const std::string base = root + "/help";
|
||||
GError* error = nullptr;
|
||||
GDir* dir = g_dir_open(base.c_str(), 0, &error);
|
||||
if (dir == nullptr) {
|
||||
if (error != nullptr) g_error_free(error);
|
||||
continue;
|
||||
}
|
||||
// `<lang>` is a directory of its own (`C`, `en`, `it`, ...), so the document is looked for one
|
||||
// level below the `help` root rather than straight under it.
|
||||
while (const char* lang = g_dir_read_name(dir)) {
|
||||
const std::string candidate = base + "/" + lang + "/" UDPSCOPE_HELP_NAME;
|
||||
if (g_file_test((candidate + "/index.page").c_str(), G_FILE_TEST_IS_REGULAR)) {
|
||||
g_dir_close(dir);
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
g_dir_close(dir);
|
||||
}
|
||||
return std::string();
|
||||
}
|
||||
|
||||
HelpReport help_self_check() {
|
||||
HelpReport report;
|
||||
|
||||
@@ -141,14 +202,16 @@ HelpReport help_self_check() {
|
||||
return report;
|
||||
}
|
||||
|
||||
void show_help(GtkWindow* parent) {
|
||||
void show_help(GtkWindow* parent, std::function<void(bool, const std::string&)> done) {
|
||||
// The freedesktop `help:` URI, handed to whatever the desktop registered for it -- Yelp on
|
||||
// GNOME. The launch is asynchronous, so the parent is referenced across it and released in
|
||||
// the callback; `parent` may legitimately be nullptr (the chooser runs before any window
|
||||
// exists).
|
||||
// GNOME. The launch is asynchronous, so both the parent and the reporter are carried across it in
|
||||
// a heap `HelpLaunch` and released in the callback; `parent` may legitimately be nullptr (the
|
||||
// chooser runs before any window exists).
|
||||
auto* launch = new HelpLaunch;
|
||||
launch->parent = parent != nullptr ? GTK_WINDOW(g_object_ref(parent)) : nullptr;
|
||||
launch->done = std::move(done);
|
||||
GtkUriLauncher* launcher = gtk_uri_launcher_new("help:" UDPSCOPE_HELP_NAME);
|
||||
gtk_uri_launcher_launch(launcher, parent, nullptr, on_help_launched,
|
||||
parent != nullptr ? g_object_ref(parent) : nullptr);
|
||||
gtk_uri_launcher_launch(launcher, parent, nullptr, on_help_launched, launch);
|
||||
g_object_unref(launcher);
|
||||
}
|
||||
|
||||
|
||||
+15
-1
@@ -4,6 +4,7 @@
|
||||
#include <gtk/gtk.h>
|
||||
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
namespace udpscope {
|
||||
@@ -23,7 +24,20 @@ const char* about_icon_name();
|
||||
/// window of our own. `parent` is the window the launch is attributed to (and the one an
|
||||
/// error is reported on); the viewer is a separate process, so nothing here can be closed
|
||||
/// with `close_help_windows()`.
|
||||
void show_help(GtkWindow* parent);
|
||||
///
|
||||
/// `done`, when the caller gives one, is called with the *launch*'s outcome: whether the
|
||||
/// `help:` URI was handed to a viewer, and the error text when it was not. It says nothing
|
||||
/// about the viewer itself — that process may fail for its own reasons and this side cannot
|
||||
/// see them — so it is the self-check's reading of the path and not a promise that a window
|
||||
/// appeared. The failure alert is shown either way; the callback is an extra, not instead.
|
||||
void show_help(GtkWindow* parent, std::function<void(bool ok, const std::string& error)> done = {});
|
||||
|
||||
/// Where `help:<id>` resolves to on this system: the first of `$XDG_DATA_HOME/help/<lang>/<id>/`
|
||||
/// and `$XDG_DATA_DIRS/help/<lang>/<id>/` that holds an `index.page`, or an empty string when no
|
||||
/// such directory does. This is the specification's own resolution, and the distinction a launch
|
||||
/// failure turns on: a missing *document* (install it) and a missing *handler* (install a viewer)
|
||||
/// are different problems with different fixes, and only the file system can tell them apart.
|
||||
std::string help_installed_path();
|
||||
|
||||
/// What UDPSCOPE_PROBE_HELP_MS reports about the manual *in the tree*. `topics` is the
|
||||
/// number of `<id>.page` files other than `index.page`, and `index` says whether
|
||||
|
||||
+4
-3
@@ -84,9 +84,10 @@ bool save_settings(const Settings& settings) {
|
||||
out << "event_cache_max = " << settings.event_cache_max << "\n";
|
||||
out << "relative_time = " << (settings.relative_time ? "true" : "false") << "\n";
|
||||
out << "theme = \"" << settings.theme << "\"\n";
|
||||
// The two help keys are the *app's* memory rather than the dialog's: the contextual-help toggle in
|
||||
// the Help menu writes the first, and the first-run tour writes the second. `read_rows()` in the
|
||||
// preferences starts from `load_settings()`, so a dialog save carries them through untouched.
|
||||
// The two help keys are the *app's* memory rather than the dialog's: the contextual-help rows --
|
||||
// the Help menu's and the source chooser's own -- write the first, and the first-run tour writes the
|
||||
// second. `read_rows()` in the preferences starts from `load_settings()`, so a dialog save carries
|
||||
// them through untouched.
|
||||
out << "contextual_help = " << (settings.contextual_help ? "true" : "false") << "\n";
|
||||
out << "tour_seen = " << (settings.tour_seen ? "true" : "false") << "\n";
|
||||
out.flush();
|
||||
|
||||
+5
-3
@@ -33,9 +33,11 @@ struct Settings {
|
||||
/// "auto", "light" or "dark": what the interface should look like. Kept as the three names libadwaita
|
||||
/// uses, so the file says what the setting is rather than what an enum happens to be numbered.
|
||||
std::string theme = "auto";
|
||||
/// The contextual-help layer: a small "?" beside each surface it can explain. On by default -- an
|
||||
/// affordance nobody can find is not help -- and off is a decision that survives a restart.
|
||||
bool contextual_help = true;
|
||||
/// The contextual-help layer: a small "?" beside each surface it can explain. **Off by default** --
|
||||
/// a first run shows the interface as a user expects it and the layer is asked for, from the Help
|
||||
/// menu or from the chooser's own help menu, by whoever wants it; on is then a decision that
|
||||
/// survives a restart.
|
||||
bool contextual_help = false;
|
||||
/// Whether the guided tour has been offered. A first run has no file carrying this key, so the tour
|
||||
/// starts itself once; the flag is written when it *starts*, not when it finishes, because a user who
|
||||
/// skips it has still seen it and a second launch with the same tour would be a nuisance.
|
||||
|
||||
+132
-6
@@ -5,8 +5,13 @@
|
||||
|
||||
#include "waveform.hpp"
|
||||
|
||||
#include "help_window.hpp"
|
||||
#include "layout_store.hpp"
|
||||
#include "settings.hpp"
|
||||
// The app's shared chrome -- `ui_impl::menu_button()`/`menu_toggle()` and `install_css()` -- so the
|
||||
// chooser's help menu is the *same* menu the window's Help view is, not a second one that merely
|
||||
// looks like it. Heavy header, but a row shape copied here would be a row shape that drifts.
|
||||
#include "ui_app.hpp"
|
||||
#include "ui_prefs.hpp"
|
||||
|
||||
#include <adwaita.h>
|
||||
@@ -127,6 +132,15 @@ struct Chooser {
|
||||
/// The preferences, for the button below. Same reasoning as `form` above: a member of the chooser
|
||||
/// rather than a heap struct, so nothing can outlive the stack frame that owns it.
|
||||
ui_impl::PrefsForm prefs;
|
||||
/// The help menu: the `.ui`'s own `GtkMenuButton` and the three rows built onto it. The rows are
|
||||
/// code for the same reason every other row here is -- each carries a handler, and the toggle
|
||||
/// carries the value the settings file holds -- while the button that opens them is the file's, so
|
||||
/// a `.ui` that lost it is caught by the null check below rather than by an empty popover.
|
||||
GtkWidget* help_button = nullptr;
|
||||
GtkWidget* help_docs = nullptr;
|
||||
GtkWidget* help_tour = nullptr;
|
||||
GtkWidget* help_toggle = nullptr;
|
||||
GtkWidget* help_popover = nullptr;
|
||||
|
||||
};
|
||||
|
||||
@@ -596,14 +610,90 @@ void open_chooser_preferences(Chooser& c) {
|
||||
});
|
||||
}
|
||||
|
||||
/// The chooser's help menu: the same three affordances the window's Help view carries, because a
|
||||
/// user who has not started yet is exactly the one who does not know how to start. The rows are
|
||||
/// `ui_impl::menu_button()`/`menu_toggle()` -- the app's own menu-row shape, so the two menus read
|
||||
/// as one program -- and the popover carries the same `.udpscope-menu` class, which is what trims
|
||||
/// the rows; `install_css()` therefore runs before this, in `choose_source()`.
|
||||
///
|
||||
/// No row shows an accelerator. The window's Help rows show "F1"/"Ctrl+T" because the window handles
|
||||
/// those keys; this dialog binds none of them, and a shortcut printed beside an action that does not
|
||||
/// answer to it is the "documented and does nothing" failure the shortcut tables are checked for.
|
||||
void build_chooser_help(Chooser& c) {
|
||||
c.help_popover = gtk_popover_new();
|
||||
gtk_widget_add_css_class(c.help_popover, "udpscope-menu");
|
||||
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_widget_set_margin_top(box, 6);
|
||||
gtk_widget_set_margin_bottom(box, 6);
|
||||
gtk_widget_set_margin_start(box, 6);
|
||||
gtk_widget_set_margin_end(box, 6);
|
||||
|
||||
c.help_docs = ui_impl::menu_button("Documentation", nullptr);
|
||||
gtk_widget_set_tooltip_text(c.help_docs,
|
||||
"the udpscope manual, opened in the desktop help viewer");
|
||||
g_signal_connect(c.help_docs, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
Chooser& ch = *static_cast<Chooser*>(user);
|
||||
gtk_popover_popdown(GTK_POPOVER(ch.help_popover));
|
||||
// No window of the app exists yet: the manual is a separate process reached
|
||||
// through the `help:` URI, so this needs no more of the app than the chooser
|
||||
// itself. The parent may be null, and this dialog is the right attribution.
|
||||
show_help(GTK_WINDOW(ch.window));
|
||||
}),
|
||||
&c);
|
||||
gtk_box_append(GTK_BOX(box), c.help_docs);
|
||||
|
||||
c.help_tour = ui_impl::menu_button("Guided tour", nullptr);
|
||||
gtk_widget_set_tooltip_text(c.help_tour,
|
||||
"walk over the plot, the side panel, the trigger, the signals and "
|
||||
"the status line, once the window is up");
|
||||
g_signal_connect(c.help_tour, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
Chooser& ch = *static_cast<Chooser*>(user);
|
||||
gtk_popover_popdown(GTK_POPOVER(ch.help_popover));
|
||||
// The tour lives on the window the app builds, which does not exist yet, so the
|
||||
// request rides on the `Config` this call was handed and the app starts it once
|
||||
// the window is up -- see `Config::tour_pending`.
|
||||
ch.cfg->tour_pending = true;
|
||||
std::fprintf(stderr, "[sources] chooser help: tour requested\n");
|
||||
std::fflush(stderr);
|
||||
}),
|
||||
&c);
|
||||
gtk_box_append(GTK_BOX(box), c.help_tour);
|
||||
|
||||
// The check *is* the setting, exactly as in the window's menu: the row is stated before the handler
|
||||
// is connected, so a row that opens checked does not write the file while the dialog is still being
|
||||
// built. The chooser cannot re-sync anything (there is no "?" layer here), so the handler only has
|
||||
// to leave the setting where the app will read it.
|
||||
c.help_toggle = ui_impl::menu_toggle("Enable contextual help", nullptr,
|
||||
load_settings().contextual_help);
|
||||
gtk_widget_set_tooltip_text(c.help_toggle,
|
||||
"a small \u201c?\u201d beside each surface of the main window, saying "
|
||||
"what that surface is for");
|
||||
g_signal_connect(c.help_toggle, "toggled", G_CALLBACK(+[](GtkToggleButton* button, gpointer) {
|
||||
Settings s = load_settings();
|
||||
s.contextual_help = gtk_toggle_button_get_active(button) != FALSE;
|
||||
// Written in full, the way the window's toggle writes it: the keys this row does
|
||||
// not own are carried through from the file rather than dropped.
|
||||
if (!save_settings(s)) {
|
||||
std::fprintf(stderr, "[sources] chooser help: could not write %s\n",
|
||||
settings_path().c_str());
|
||||
std::fflush(stderr);
|
||||
}
|
||||
}),
|
||||
&c);
|
||||
gtk_box_append(GTK_BOX(box), c.help_toggle);
|
||||
|
||||
gtk_popover_set_child(GTK_POPOVER(c.help_popover), box);
|
||||
gtk_menu_button_set_popover(GTK_MENU_BUTTON(c.help_button), c.help_popover);
|
||||
}
|
||||
|
||||
/// `UDPSCOPE_PROBE_PICK_MS`: activate the first row, the way a click does.
|
||||
/// `UDPSCOPE_PROBE_NEWHOST=host:port` drives the form instead of a row. The crash surface of a
|
||||
/// dialog built, filled and destroyed is as much the point as the values, the same way
|
||||
/// `UDPSCOPE_PROBE_FIELDS_MS` is in `ui.cpp`.
|
||||
/// The chooser's frame, read back from the `.ui` it was built from: the two pages by name and title, the
|
||||
/// switcher wired to the stack, and the four buttons the code resolved. `[help] ui=` does the same against
|
||||
/// its tables -- the frame is what the file holds, so the frame is what is read, while the rows a user sees
|
||||
/// are read by the probes that drive them.
|
||||
/// switcher wired to the stack, the four buttons the code resolved, and the help menu's three rows.
|
||||
/// `[help] ui=` does the same against its tables -- the frame is what the file holds, so the frame is
|
||||
/// what is read, while the rows a user sees are read by the probes that drive them.
|
||||
void report_chooser_ui(const Chooser& c) {
|
||||
std::fprintf(stderr, "[sources] chooser ui:");
|
||||
for (GtkWidget* w = gtk_widget_get_first_child(c.page_stack); w != nullptr;
|
||||
@@ -617,14 +707,21 @@ void report_chooser_ui(const Chooser& c) {
|
||||
adw_view_stack_page_get_icon_name(page) != nullptr ? "yes" : "NO");
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
" visible='%s' switcher_wired=%d buttons=[prefs=%d new_host=%d open=%d cancel=%d]\n",
|
||||
" visible='%s' switcher_wired=%d buttons=[prefs=%d new_host=%d open=%d cancel=%d] "
|
||||
"help=[button=%d docs=%d tour=%d toggle=%d on=%d]\n",
|
||||
adw_view_stack_get_visible_child_name(ADW_VIEW_STACK(c.page_stack)),
|
||||
adw_view_switcher_bar_get_stack(ADW_VIEW_SWITCHER_BAR(c.switcher)) ==
|
||||
ADW_VIEW_STACK(c.page_stack)
|
||||
? 1
|
||||
: 0,
|
||||
c.prefs_button != nullptr ? 1 : 0, c.new_host != nullptr ? 1 : 0,
|
||||
c.waves_open != nullptr ? 1 : 0, c.cancel_button != nullptr ? 1 : 0);
|
||||
c.waves_open != nullptr ? 1 : 0, c.cancel_button != nullptr ? 1 : 0,
|
||||
c.help_button != nullptr ? 1 : 0, c.help_docs != nullptr ? 1 : 0,
|
||||
c.help_tour != nullptr ? 1 : 0, c.help_toggle != nullptr ? 1 : 0,
|
||||
c.help_toggle != nullptr &&
|
||||
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(c.help_toggle))
|
||||
? 1
|
||||
: 0);
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
@@ -638,6 +735,26 @@ gboolean pick_probe(gpointer user) {
|
||||
// What the `.ui` contributed, before anything is driven: the pages, the switcher and the buttons.
|
||||
report_chooser_ui(c);
|
||||
|
||||
// `UDPSCOPE_PROBE_CHOOSER_TOUR=1` and `UDPSCOPE_PROBE_CHOOSER_HELP=1` drive the help menu's two
|
||||
// stateful rows the way a click does, and then fall through to the pick below -- the point of the
|
||||
// tour entry is that it crosses *into the run this chooser starts*, so a probe that stopped here
|
||||
// would assert nothing. The tour is read back out of the `Config` the run will be handed, which is
|
||||
// the carrier; the toggle is read back out of the settings file it writes, since the app is built
|
||||
// from that file and not from the dialog.
|
||||
if (g_getenv("UDPSCOPE_PROBE_CHOOSER_TOUR") != nullptr && c.help_tour != nullptr) {
|
||||
g_signal_emit_by_name(c.help_tour, "clicked");
|
||||
std::fprintf(stderr, "[sources] chooser help: tour_pending=%d\n", c.cfg->tour_pending ? 1 : 0);
|
||||
std::fflush(stderr);
|
||||
}
|
||||
if (g_getenv("UDPSCOPE_PROBE_CHOOSER_HELP") != nullptr && c.help_toggle != nullptr) {
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(c.help_toggle), TRUE);
|
||||
const Settings back = load_settings();
|
||||
std::fprintf(stderr, "[sources] chooser help: toggle=%d setting=%d file=%s\n",
|
||||
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(c.help_toggle)) ? 1 : 0,
|
||||
back.contextual_help ? 1 : 0, settings_path().c_str());
|
||||
std::fflush(stderr);
|
||||
}
|
||||
|
||||
// `UDPSCOPE_PROBE_CHOOSER_PREFS=1`: the preferences opened from *here* rather than from the app's
|
||||
// menu -- the entry point that exists because this dialog runs before `App`. It is driven through
|
||||
// the same fill-and-apply the app's probe uses, and reports what the run about to start will get,
|
||||
@@ -906,15 +1023,24 @@ bool choose_source(Config& cfg) {
|
||||
c.new_host = GTK_WIDGET(gtk_builder_get_object(c.ui, "new_host_button"));
|
||||
c.waves_open = GTK_WIDGET(gtk_builder_get_object(c.ui, "waves_open"));
|
||||
c.cancel_button = GTK_WIDGET(gtk_builder_get_object(c.ui, "cancel_button"));
|
||||
c.help_button = GTK_WIDGET(gtk_builder_get_object(c.ui, "help_menu_button"));
|
||||
if (c.window == nullptr || c.page_stack == nullptr || c.switcher == nullptr || c.list == nullptr ||
|
||||
c.wave_list == nullptr || c.prefs_button == nullptr || c.new_host == nullptr ||
|
||||
c.waves_open == nullptr || c.cancel_button == nullptr) {
|
||||
c.waves_open == nullptr || c.cancel_button == nullptr || c.help_button == nullptr) {
|
||||
std::fprintf(stderr, "[sources] chooser ui: source-chooser.ui is missing an id this code reads\n");
|
||||
std::fflush(stderr);
|
||||
return false;
|
||||
}
|
||||
adw_view_switcher_bar_set_stack(ADW_VIEW_SWITCHER_BAR(c.switcher), ADW_VIEW_STACK(c.page_stack));
|
||||
|
||||
// The help menu's rows are the window's own `menu_button()`/`menu_toggle()`, so its popover wants
|
||||
// the same stylesheet the overflow menu is drawn with (`.udpscope-menu`, which trims a row to a
|
||||
// menu row rather than Adwaita's 34 px button). The app installs it in `on_activate()`, which is
|
||||
// *after* this dialog, so it has to be installed here first; `install_css()` is idempotent, so
|
||||
// the later call is the no-op it reads as.
|
||||
ui_impl::install_css();
|
||||
build_chooser_help(c);
|
||||
|
||||
g_signal_connect(c.list, "row-activated", G_CALLBACK(on_row_activated), &c);
|
||||
g_signal_connect(c.wave_list, "row-activated", G_CALLBACK(on_wave_activated), &c);
|
||||
g_signal_connect(c.waves_open, "clicked", G_CALLBACK(on_wave_open_clicked), &c);
|
||||
|
||||
+9
-2
@@ -5142,8 +5142,15 @@ void on_activate(GtkApplication* application, gpointer user) {
|
||||
// A first run is the run whose settings file does not say the tour has been offered -- see
|
||||
// `tour_autostart()`. Delayed rather than fired on the first tick: the window has to be up and
|
||||
// laid out for the tour to have anything to point at, and a tour that opened on the frame the
|
||||
// window appeared would be a spotlight on a widget with no allocation yet.
|
||||
if (!app->settings.tour_seen) g_timeout_add(1500, tour_autostart, app);
|
||||
// window appeared would be a spotlight on a widget with no allocation yet. The source chooser's
|
||||
// own "Guided tour" entry arrives as `Config::tour_pending` (the request has to outlive the dialog
|
||||
// that made it -- the tour is drawn on *this* window) and gets the same delay for the same reason,
|
||||
// through its own callback so the two are told apart in the readings.
|
||||
if (app->model->config().tour_pending) {
|
||||
g_timeout_add(1500, tour_start_requested, app);
|
||||
} else if (!app->settings.tour_seen) {
|
||||
g_timeout_add(1500, tour_autostart, app);
|
||||
}
|
||||
|
||||
// Bubble phase on purpose: a focused GtkEditable or GtkButton wins its own keys, so typing
|
||||
// 'm' into the window entry does not toggle the measurements pane and Space does not
|
||||
|
||||
@@ -33,6 +33,17 @@
|
||||
<property name="tooltip-text" translatable="yes">the event cache, relative time and the interface's theme — the same dialog as the main window's menu, and the only place relative time can still change this run</property>
|
||||
</object>
|
||||
</child>
|
||||
<!-- The help menu: the same three affordances the window's Help view carries, because a
|
||||
user who has not started yet is exactly the one who does not know how to start. Only
|
||||
the button is here — its rows carry a handler and a setting, so they are code, the
|
||||
same division every other row in this file follows. Beside `_Preferences`, because
|
||||
both act on the *program* rather than on a source. -->
|
||||
<child type="start">
|
||||
<object class="GtkMenuButton" id="help_menu_button">
|
||||
<property name="icon-name">help-about-symbolic</property>
|
||||
<property name="tooltip-text" translatable="yes">the manual, a guided tour after start-up, and the contextual help</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkButton" id="cancel_button">
|
||||
<property name="label" translatable="yes">_Cancel</property>
|
||||
|
||||
@@ -255,6 +255,18 @@ struct App {
|
||||
/// The first-run auto-start: set by the timer that fires it, and read by the probe and the dump.
|
||||
/// The decision itself is `settings.tour_seen`, which is the whole of "this has happened before".
|
||||
bool tour_auto_started = false;
|
||||
/// The chooser's own "Guided tour" request (`Config::tour_pending`), recorded when its timer runs.
|
||||
/// A separate flag from the auto-start because it is a *different* thing: the user asked for the
|
||||
/// tour before the window existed, rather than never having seen it.
|
||||
bool tour_requested = false;
|
||||
/// The self-check's reading of the manual's launch (`UDPSCOPE_PROBE_HELP_MS`). The launch is
|
||||
/// asynchronous and the viewer is another process, so the outcome arrives in a callback and the
|
||||
/// probe prints it from a timer afterwards; `attempted` is set at the call, because a launch that
|
||||
/// never called back is itself a reading.
|
||||
bool dbg_help_launch_attempted = false;
|
||||
bool dbg_help_launch_done = false;
|
||||
bool dbg_help_launch_ok = false;
|
||||
std::string dbg_help_launch_error;
|
||||
/// The help layer's own dump readings, captured on the tick while the window is alive (nothing
|
||||
/// here may be read in the dump -- see `docs/claude/verification.md`).
|
||||
std::string dbg_context_help;
|
||||
@@ -1743,6 +1755,10 @@ int tour_step_count(const App& app);
|
||||
int tour_step_index(const App& app);
|
||||
/// The first-run auto-start's callback, scheduled only when `settings.tour_seen` is false.
|
||||
gboolean tour_autostart(gpointer user);
|
||||
/// The chooser's "Guided tour" request (`Config::tour_pending`): the same delayed start, but it
|
||||
/// sets `tour_requested` rather than `tour_auto_started`, so the probe and the dump can tell the two
|
||||
/// apart. Scheduled only when the config carries the request.
|
||||
gboolean tour_start_requested(gpointer user);
|
||||
/// Captures the layer's own readings and the bounds of the surfaces the help's figures label, while
|
||||
/// the window is alive -- the dump runs after it is destroyed and may not ask GTK anything.
|
||||
void capture_help_state(App& app);
|
||||
|
||||
+37
-4
@@ -4444,16 +4444,33 @@ UiShortcuts ui_shortcut_rows(const char* path) {
|
||||
return out;
|
||||
}
|
||||
|
||||
/// Prints the manual launch's outcome, which arrives in a callback (the launch is asynchronous and
|
||||
/// the viewer is another process) -- so the line is written from a timer a moment later rather than
|
||||
/// from `help_probe`. `done=0` is a launch that never called back, which is a reading of its own.
|
||||
gboolean help_launch_report(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
const std::string installed = help_installed_path();
|
||||
std::fprintf(stderr, "[help] launch attempted=%d done=%d ok=%d error='%s' installed=%s\n",
|
||||
a->dbg_help_launch_attempted ? 1 : 0, a->dbg_help_launch_done ? 1 : 0,
|
||||
a->dbg_help_launch_ok ? 1 : 0, a->dbg_help_launch_error.c_str(),
|
||||
installed.empty() ? "(not found)" : installed.c_str());
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean help_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
const HelpReport report = help_self_check();
|
||||
const std::string session =
|
||||
layout_path(a->model->config().server, a->model->config().port);
|
||||
|
||||
// The manual is not opened here: it lives in another process (Yelp, through the `help:` URI)
|
||||
// and neither it nor this box can draw a window for it, so there is nothing to open, read
|
||||
// back or close. `help_self_check()` above is the assertion -- the set is on disk where the
|
||||
// build says it is. The two AdwDialogs below are still built, read and closed for real.
|
||||
// The manual is *launched* here and read no further: it opens in another process (Yelp, through
|
||||
// the `help:` URI) and neither it nor this box can draw a window for it, so there is nothing to
|
||||
// read back or close. `help_self_check()` above is the assertion on the *set* being on disk where
|
||||
// the build says it is; the launch is the assertion on the *path*, and its outcome is reported by
|
||||
// `help_launch_report()` once the launch's callback has run. The failure alert the callback shows
|
||||
// is deliberately left up -- it is what a user sees, and the probe must not report a failure as a
|
||||
// silence. The two AdwDialogs below are still built, read and closed for real.
|
||||
show_about(GTK_WINDOW(a->window), session);
|
||||
show_shortcuts(GTK_WINDOW(a->window));
|
||||
// Read while it is open: the icon is the point of the dialog carrying one, and
|
||||
@@ -4461,6 +4478,18 @@ gboolean help_probe(gpointer user) {
|
||||
const std::string about_icon = about_icon_name();
|
||||
close_help_windows();
|
||||
|
||||
// The manual's path: on a successful launch no window is left behind, so this cannot be observed
|
||||
// any other way; `show_help()`'s reporter is the only hook that runs on that path.
|
||||
a->dbg_help_launch_attempted = true;
|
||||
a->dbg_help_launch_done = false;
|
||||
a->dbg_help_launch_ok = false;
|
||||
a->dbg_help_launch_error.clear();
|
||||
show_help(GTK_WINDOW(a->window), [a](bool ok, const std::string& error) {
|
||||
a->dbg_help_launch_done = true;
|
||||
a->dbg_help_launch_ok = ok;
|
||||
a->dbg_help_launch_error = error;
|
||||
});
|
||||
|
||||
int accel_count = 0;
|
||||
int gesture_count = 0;
|
||||
(void)app_accels(&accel_count);
|
||||
@@ -4479,6 +4508,10 @@ gboolean help_probe(gpointer user) {
|
||||
build_info::kCommit, build_info::kTimestamp, config_dir().c_str(),
|
||||
about_icon.c_str());
|
||||
std::fflush(stderr);
|
||||
// The launch's outcome needs its callback to have run; a moment is enough for a local
|
||||
// `g_app_info_launch_default_for_uri()`, and `done=0` in the line is how a launch that never
|
||||
// called back reads.
|
||||
g_timeout_add(600, help_launch_report, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
|
||||
+13
-2
@@ -342,6 +342,16 @@ gboolean tour_autostart(gpointer user) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean tour_start_requested(gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
if (app->window == nullptr || app->tour_overlay == nullptr) return G_SOURCE_REMOVE;
|
||||
// Not `tour_auto_started`: the chooser's entry is the user asking for the tour before the window
|
||||
// existed, and the reading has to say which of the two started it.
|
||||
app->tour_requested = true;
|
||||
start_tour(*app);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
// --- the dump's readings ----------------------------------------------------
|
||||
|
||||
namespace {
|
||||
@@ -381,10 +391,11 @@ void capture_help_state(App& app) {
|
||||
char layer[512];
|
||||
std::snprintf(layer, sizeof(layer),
|
||||
"contextual=%s hints=%d ids={%s} icon=dialog-question-symbolic(%s); tour=%s steps=%d "
|
||||
"step=%d auto=%d",
|
||||
"step=%d auto=%d requested=%d",
|
||||
app.settings.contextual_help ? "on" : "off", hints, ids.empty() ? "" : ids.c_str(),
|
||||
icon_ok ? "ok" : "MISSING", app.tour_running ? "running" : "idle",
|
||||
tour_step_count(app), tour_step_index(app), app.tour_auto_started ? 1 : 0);
|
||||
tour_step_count(app), tour_step_index(app), app.tour_auto_started ? 1 : 0,
|
||||
app.tour_requested ? 1 : 0);
|
||||
app.dbg_context_help = layer;
|
||||
app.dbg_tour_steps = tour_step_count(app);
|
||||
app.dbg_tour_step = tour_step_index(app);
|
||||
|
||||
@@ -37,6 +37,13 @@ GtkBuilder* ui_load(App& app, const char* resource) {
|
||||
/// the whole side panel. Scoped to `.udpscope-compact` so it cannot reach any other
|
||||
/// button in the window.
|
||||
void install_css() {
|
||||
// Idempotent, because it is no longer installed from one place: the source chooser runs before the
|
||||
// application (it draws a menu built from the same `menu_button()` rows) and has to have the
|
||||
// stylesheet before it shows them, while `on_activate()` still installs it for every run. A second
|
||||
// provider would be a second copy of the same rules on the display.
|
||||
static bool installed = false;
|
||||
if (installed) return;
|
||||
installed = true;
|
||||
static const char kCss[] =
|
||||
"row.udpscope-compact { min-height: 0; padding: 1px 0; }\n"
|
||||
// A popover menu row: Adwaita gives a button a 34 px minimum height and 8 px of padding,
|
||||
|
||||
Reference in new issue
Block a user