feat: the help is a submenu, with a contextual-help layer and a guided tour
The hamburger menu's help rows were three flat entries and a window. They are a **`Help` submenu** now, which is where every GNOME menu keeps them, with the four things a first run cannot find: - Keyboard shortcuts Ctrl+/ (the existing overview, `show_shortcuts()`) - Documentation F1 (the existing manual, `show_help()`) - Guided tour Ctrl+T (new) - Enable contextual help (new, a stateful row: its check *is* the setting) The submenu *replaces* the menu's rows rather than floating beside them, the same shape and the same reason as the plot's right-click menu: a submenu torn out of the menu it belongs to is a second thing to dismiss, and the `GtkMenuButton` that owned one toggled itself out of sync the moment anything else closed its popover. It is the pages of a `GtkStack`, not a rebuild of the box and not a swap of the popover's child: the main view holds `app.layout_toggle`, which the tick's `sync_layout_editor()` and the `L` accel read, so rebuilding would leave that pointer on a destroyed widget, and a swapped child would leave the view that is not showing without an owner. **The contextual help is one table and a "?" per surface** (`src/ui_context_help.cpp`): the plot, the trigger, the cursors, the measurements, the math channels, the signals, the event history, the time window, the status line, and the math editor's identity row. Each row names the *heading box* its button joins -- a sibling of the title, never an overlay child on a corner, which is what would cover the switch or the [edit] button already there -- so adding a surface is a row and a widget pointer. Off means *gone*, not greyed: the buttons are removed, and they are found by a data key rather than a list of pointers because the math editor's heading is built afresh on every opening and a list would go stale one dialog later. `contextual_help` and `tour_seen` are keys of `settings.toml`, written through the same `Settings`/`load_settings()`/`save_settings()` the Preferences dialog uses, whose own rows start from `load_settings()` and so carry them through untouched. **The guided tour is five steps on a `GtkOverlay`** above the window's content: the plot, the side panel, the trigger, the signals and the status line. There is no popover in it and therefore no pointer grab to leak -- four dim rectangles around the hole are what make it modal -- and the probe asserts that as a reading (`controllers=` against the count taken before it opened) rather than as a claim. Everything is placed from `gtk_widget_compute_bounds()` re-read on every frame, because a widget's allocation is 0 on the frame it is first shown and a window resize moves it under a one-shot measurement; the hole is kept from the last frame that could measure the target, because a frame that lands between two layout passes would otherwise empty it and make the spotlight jump. `Skip`, `Finish` and `Escape` all hide every child and leave nothing behind. A first run gets the tour once, decided by `tour_seen` and not by "the settings file did not exist" (a user who changed the theme has a file and has still never seen it), and the flag is written when the tour *starts* rather than when it finishes: a user who skips it in the first second has still seen it. `UDPSCOPE_PROBE_HELPUI_MS` drives all of it -- the auto-start, the menu's seven rows and the submenu's five, the toggle's effect read as a count of affordances on either side of it, and the tour started, stepped forwards and back and dismissed -- and the exit dump gains `help layer:` and `help figures:`, the latter being the ten surfaces' window-coordinate rectangles that the manual's annotated screenshots crop their boxes from. Two of them are what the D-Bus-free headless run can still not decide, and both are limitations rather than defects: export carries no "?" (it is a menu action with no surface of its own, and a popover inside the menu's popover is the nested-popover trap the invariants already warn about), and the tour card cannot miss its target when the target is the whole content -- which is what the plot is at the window's 756 px minimum, where the split view overlays the side panel instead of putting it beside the plot. The probe prints both rectangles rather than asserting a placement, so that case reads as the geometry it is. Also fixed while it was found: `gtk_widget_measure()` includes the widget's own margins (measured: a 280 px box with `margin-start 453` reports 733), so a measure that is used to *place* the widget is a feedback loop -- that is what made the tour card walk between the corner and the target from frame to frame.
This commit is contained in:
1 parent
c1460a6977
commit
1baa58b8fd
20 files changed
+1558
-30
No files matched your search
@@ -872,6 +872,87 @@ built, which for a program under development is what you want. No `StartupWMClas
|
||||
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.
|
||||
|
||||
## The Help submenu, the contextual help and the guided tour
|
||||
|
||||
**The hamburger menu's help trio became a `Help` submenu**, which is where every GNOME menu keeps
|
||||
these four: the **Keyboard shortcuts** overview and the **Documentation** window (the two dialogs
|
||||
the program already had, reached through `show_shortcuts()`/`show_help()` exactly as their keys
|
||||
reach them), the **Guided tour**, and a stateful **Enable contextual help** row.
|
||||
|
||||
The submenu *replaces* the menu's rows rather than floating beside them -- the same shape, and the
|
||||
same reason, as the plot's right-click menu: a submenu torn out of the menu it belongs to is a
|
||||
second thing to dismiss, and the `GtkMenuButton` that owned one toggled itself out of sync the
|
||||
moment anything else closed its popover. It is a `GtkStack` whose visible page is the view, and not
|
||||
a rebuild of the box: the main view holds `app.layout_toggle`, which the frame tick reads
|
||||
(`sync_layout_editor()`) and the `L` accel moves, so rebuilding the rows would leave that pointer on
|
||||
a destroyed widget, and *swapping the popover's child* would leave the view that is not showing
|
||||
without an owner. A page swap has neither problem.
|
||||
|
||||
**The accelerators are `Ctrl+/` and `F1` for the two dialogs -- they already were -- plus `Ctrl+T`
|
||||
for the tour and `Ctrl+H` for the contextual help.** Both new keys are rows in
|
||||
`src/shortcuts.cpp` *and* in `src/ui/shortcuts.ui`, because the `[help]` probe checks the two
|
||||
against each other (`ui` must equal `tables`).
|
||||
|
||||
**The contextual help is one table and a "?" per surface.** `src/ui_context_help.cpp` carries the
|
||||
rows -- the plot, the trigger, the cursors, the measurements, the math channels, the signals, the
|
||||
event history, the time window, the status line, and the math editor's identity row -- and each row
|
||||
names the *heading box* its button joins, so adding a surface is a row and a widget pointer. The
|
||||
button is a sibling of the heading's title, not an overlay on its corner, for the reason the
|
||||
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
|
||||
`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.
|
||||
|
||||
**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
|
||||
therefore no pointer grab to lose: what makes the tour modal is four dim rectangles -- above, below
|
||||
and either side of the target -- that swallow the clicks that land on them, and
|
||||
`gtk_widget_is_visible()` decides which steps exist at all (a widget under a hidden parent keeps its
|
||||
own flag true, so `get_visible()` would have walked over surfaces that are not there).
|
||||
|
||||
Everything is placed from `gtk_widget_compute_bounds()`, re-read on **every frame**, because a
|
||||
widget's allocation is 0 on the frame it is first shown and a window resize moves it under a
|
||||
one-shot measurement. The hole is kept from the last frame that could measure the target for the
|
||||
same reason: a frame can arrive between two layout passes, and a hole that emptied there would make
|
||||
the spotlight and the card jump. The card goes under the target, over it, or in the column beside it
|
||||
-- whichever has room -- and only takes the bottom corner when the target *is* the whole content,
|
||||
which is what happens to the plot step in the documented headless run (a 640x480 Xvfb screen gives
|
||||
the window its 756x894 minimum, and at that width the split view overlays the side panel instead of
|
||||
putting it beside the plot). The probe prints the two rectangles (`spot=`, `card=`) rather than
|
||||
asserting a placement, so that case reads as the geometry it is.
|
||||
|
||||
**A first run gets the tour, once.** The decision is `settings.tour_seen`, and it is a key of its
|
||||
own rather than "the settings file did not exist": a user who had already changed a theme, or the
|
||||
cache size, has a settings file and has still never seen the tour. It is written when the tour
|
||||
**starts**, not when it finishes, because a user who skips it in the first second has still seen it
|
||||
and a second launch that offered the whole tour again would be a nuisance. The auto-start is
|
||||
scheduled 1.5 s after the window is built -- the window has to be up and laid out for there to be
|
||||
anything to point at -- and the menu (or `Ctrl+T`) starts the same tour at any time.
|
||||
|
||||
Dismissing it is `Skip`, `Finish` on the last step, or `Escape`; all three hide every child of the
|
||||
overlay. The probe asserts what "left nothing behind" means as a *reading*: four dim rectangles,
|
||||
the outline and the card all off screen, and **the window carrying exactly the event controllers it
|
||||
had before the tour opened**.
|
||||
|
||||
`UDPSCOPE_PROBE_HELPUI_MS` drives all of it: the first-run auto-start, the menu's seven rows and the
|
||||
submenu's five, the toggle and the count of "?" affordances on either side of it, and the tour
|
||||
started, stepped forwards and back, and dismissed. Its readings are in
|
||||
[probe-readings.md](docs/claude/probe-readings.md).
|
||||
|
||||
**The dump also carries the geometry the help's annotated figures crop**, `help figures:`, with each
|
||||
of ten surfaces' `x,y,width,height` in **window coordinates** -- the frame a screenshot is cropped
|
||||
in -- or `-1,-1,0,0` when the surface is not on screen. It is measured on the tick while the window
|
||||
is alive, and it exists because a crop box drawn round a region of a screenshot has to be measured
|
||||
rather than eyeballed: the panels are where `gtk_widget_compute_bounds()` says they are, and a
|
||||
plausible guess is not.
|
||||
|
||||
## How the spec maps onto the code
|
||||
|
||||
**One buffer of `N+1` slices, plus frozen event snapshots.** `ScopeModel`
|
||||
@@ -2541,6 +2622,20 @@ silence timeout and then resumes, which is the EMI case end to end.
|
||||
machine and its integration with the buffers, and rendering under Xvfb
|
||||
(llvmpipe) were exercised; the interactive paths (drag and drop, the dialogs,
|
||||
the right-click menu) were not driven by an automated test.
|
||||
- **The contextual help has no "?" on the export row.** Nine surfaces carry one;
|
||||
export is a *menu action* and has no surface of its own to carry anything, and the
|
||||
obvious alternative -- a "?" on the row inside the menu's popover, opening a
|
||||
popover of its own -- is the nested popover the invariants already warn about: the
|
||||
inner popover takes the grab, the menu closes under the click, and the bubble is
|
||||
left parented to a row in a hidden popover. Export is described in the
|
||||
Documentation instead.
|
||||
- **The tour card can cover its own target, and in the documented headless run it
|
||||
does.** The card is placed under, over or beside the hole whenever one of those has
|
||||
room; when the target covers the whole content -- which is what the plot does at the
|
||||
window's 756 px minimum, because the split view overlays the side panel rather than
|
||||
putting it beside the plot -- no placement can avoid it, and the card takes the
|
||||
bottom corner. The probe prints both rectangles instead of asserting a placement,
|
||||
so the case reads as a measurement.
|
||||
|
||||
**A debug-build assertion in the library's suite.** `deps/plotter`'s debug build prints
|
||||
`TextBatch::add_cell`'s `glyph_is_cell` assertion once while running its tests — from a
|
||||
|
||||
Reference in new issue
Block a user