Commit Graph
7 Commits
Author SHA1 Message Date
Martino Ferrari fe499bfe41 help: the search block's (i) on its header, a 12 px mark, and a real search page 2026-10-14 23:29:00 +02:00
Martino Ferrari 829176248a help: a tinier (i), hints beside their controls, and the tour starting in the chooser
The contextual help becomes the affordance the ask names: a plain (i)
(`dialog-information-symbolic`) at **16x16**, and one that sits beside the
control it explains rather than only in a pane's heading.

- The mark: `GtkMenuButton` ignores `min-width` on the widget and a size
  request, so the size is made in `install_css()` on all three nodes (outer
  `menubutton`, its `button` subnode, the glyph). Measured 16x16 against the
  default 34x34; the dump prints it as `hint=WxH`, measured with
  `gtk_widget_compute_bounds()` on the first *visible* hint.
- More sites: `events_record` (the "record events" switch in the trigger
  panel), `trigger_advanced` (the pre-trigger share row), `signals_config`
  (the signals pane's hint line, wrapped in a box so a hint can join it).
  Hints join as siblings, so none can cover a switch, pencil or dropdown.
- The source chooser gets its own layer -- `chooser_sources` (a new heading
  above the saved-source list) and `chooser_waveform` (the waveforms action
  row) -- built with the one `context_help_button()` both windows use and
  re-synced by its Help menu's toggle. The form's `AdwEntryRow`s cannot host a
  sibling hint, so none is invented for the host/port fields.
- The tour is one implementation over two windows: `ui_impl::TourView` plus a
  list of `TourStop`s; each window keeps its own table. A walk that passes its
  last stop calls `on_done`, and the chooser's sets `Config::tour_pending`, so
  a first run is one tour that crosses from the chooser into the app. The
  chooser's tour auto-starts from `settings.tour_seen` exactly as the window's
  does, and its "Guided tour" row now starts *it* instead of only requesting
  the window's.
- Probes: `UDPSCOPE_PROBE_CHOOSER_TOUR=1` / `UDPSCOPE_PROBE_CHOOSER_TOUR_MS`,
  the hint size in the `help layer:`/`[helpui] contextual on:` lines, and the
  chooser's hints in its `chooser ui:` line. Docs updated in
  invariants-gtk-and-chrome.md, probe-readings.md and verification.md.
2026-10-14 22:42:38 +02:00
Martino Ferrari d8a79b96ba 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.
2026-10-14 17:22:03 +02:00
Martino Ferrari 1baa58b8fd 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.
2026-10-14 17:02:29 +02:00
Martino Ferrari ed82abd405 feat: the cursors panel is a UI file
src/ui/cursors-panel.ui holds the panel's shape -- the header with the mode dropdown and the reset
button, and the grid the two cursor times are read in. What is not in it is the grid's contents,
which follow the cursor mode and the rows and are rebuilt whenever either changes.

Panels need a builder that outlives them, unlike the dialogs, which are rebuilt on every opening: a
widget belongs to the builder that built it, so App::ui_builders holds each panel's for the life of
the window. ui_load() is that, in ui_widgets.cpp.

make test-ui also checks that udpscope-ui.gresource.xml lists every .ui file now. That was the third
trap of this refactor -- a file on disk but not in the resource is a Gtk-ERROR at load, which aborts
-- and it is a compile-time mistake that nothing else in the build could see. Verified by unlisting
the new file and watching the check fail, then restoring it.

[cursors-probe] reads as it did: all four modes, pane=1, the timings and the drag cases.
2026-10-12 10:47:34 +02:00
Martino Ferrari 0e256764d5 udpscope: an oscilloscope view for a UDPStreamer source
GTK4 and libadwaita. It receives one UDP stream through the vendored
udpsclient, buffers it in a large ring, and draws it with the vendored
plotter: a trigger with a pre/post event window, a pause that freezes the
window, cursors, measurements, math channels in a node editor, an event cache,
named configurations, preferences, and an export of the visible window to
.npy/.csv. Waveform files can be opened and analysed in place.

The interface is two threads -- a producer that owns the socket and the GTK
main thread that owns every widget and the GL context -- sharing one model
under one mutex, with samples crossing through lock-free rings.

deps/plotter is a submodule pinned to the commit this tree builds against;
deps/udpsclient is vendored in place. docs/claude/ documents the rules that
cost a defect to learn, and SPECS.md is the requirement document.
2026-10-11 15:21:40 +02:00
Martino Ferrari 8f54841c2c working on todo.md features 2026-10-10 22:52:12 +02:00