Feature/source plugins #1

Merged
martino merged 130 commits from feature/source-plugins into main 2026-11-04 07:35:21 +01:00
5 changed files with 254 additions and 119 deletions
Showing only changes of commit 2826042156 - Show all commits

No files matched your search

+60 -38
View File
@@ -489,7 +489,7 @@ Two lines of the self-check output are assertions in disguise:
column and says there is one scroller and no stack. These are captured on the tick or at the exit probe: column and says there is one scroller and no stack. These are captured on the tick or at the exit probe:
the dump runs after the window is destroyed. A source with extra panels adds *no* dock section: its panels the dump runs after the window is destroyed. A source with extra panels adds *no* dock section: its panels
are slide-over pages (`source-<id>`), so the line reads the same on the E-1608 and the probe kind. are slide-over pages (`source-<id>`), so the line reads the same on the E-1608 and the probe kind.
- `help layer: contextual=on hints=12 ids={events plot status cursors measures trigger trigger_advanced events_record trigger_search signals signals_config window} icon=dialog-information-symbolic(ok) hint=12x12; tour=idle steps=5 step=0 auto=1 requested=0` (in the exit - `help layer: contextual=on hints=12 ids={events plot status cursors measures trigger trigger_advanced events_record trigger_search signals signals_config window} icon=dialog-information-symbolic(ok) hint=12x12; tour=idle steps=6 step=0 auto=1 requested=0` (in the exit
dump, beside `dock:`) — the help affordance layer as the last tick found it. **`hints=` must equal dump, beside `dock:`) — 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: eleven of the window the number of ids**, and the ids are the surfaces the contextual help is on: eleven of the window
itself, and `math` only while the math editor is open (its heading lives in a builder that is rebuilt itself, and `math` only while the math editor is open (its heading lives in a builder that is rebuilt
@@ -534,14 +534,18 @@ Two lines of the self-check output are assertions in disguise:
[invariants-gtk-and-chrome.md](invariants-gtk-and-chrome.md)). The writer chains to [invariants-gtk-and-chrome.md](invariants-gtk-and-chrome.md)). The writer chains to
`g_log_writer_default()`, so every message still reaches stderr exactly as before and the documented `g_log_writer_default()`, so every message still reaches stderr exactly as before and the documented
baselines are read the same way. baselines are read the same way.
- `help figures: trigger_head=X,Y,W,H trigger_summary=… trigger_panel=… signal_list=… side_stack=… window_button=… status_label=… header_bar=… plot=…` (in the exit - `help figures: trigger_head=X,Y,W,H trigger_summary=… trigger_panel=… signal_list=… badge_bar=… dock=… 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 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 figures label, `x,y,width,height`, or `-1,-1,0,0` for a surface that is **not on screen**. It is
measured on the tick while the window is alive (nothing in the dump may call `gtk_widget_*` — see measured on the tick while the window is alive (nothing in the dump may call `gtk_widget_*` — see
[verification.md](verification.md)) and with `gtk_widget_is_visible()`, not `get_visible()`: a widget [verification.md](verification.md)) and with `gtk_widget_is_visible()` **and** `gtk_widget_get_mapped()`,
under a hidden parent — `trigger_summary` while the pane is in *configuration* mode, the Measure page not `get_visible()`: a widget under a hidden parent — `trigger_summary` while the pane is in
while the Signals tab is showing — keeps its own flag true and reads `0x0`, and a crop box drawn from *configuration* mode, and **every `trigger_*` surface while the slide-over is closed** (a revealer
that is a box over the wrong thing. The figures are produced by cropping a real screenshot, so these unmaps its child without touching its visible flag) — keeps its own flag true and reads `0x0`, and a crop
box drawn from that is a box over the wrong thing. So the three `trigger_*` rectangles read real numbers
only while the `trigger` page is open (the exit probe has it open; `UDPSCOPE_PROBE_HELPUI_MS` opens it for
its figures legs), and `-1,-1,0,0` otherwise. `badge_bar` is the badge row under the plot and `dock` the
right-hand column (these replaced `side_stack`, the old sidebar stack). The figures are produced by cropping a real screenshot, so these
numbers **are** the crop boxes; their values depend on the window's size, which under the documented numbers **are** the crop boxes; their values depend on the window's size, which under the documented
`xvfb-run` is the window's 756x894 minimum (the 640x480 screen is smaller than the content), so a run `xvfb-run` is the window's 756x894 minimum (the 640x480 screen is smaller than the content), so a run
on a larger screen reads different numbers for the same panels. `trigger_summary` reading on a larger screen reads different numbers for the same panels. `trigger_summary` reading
@@ -878,7 +882,7 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
every row then reports `h=0`. It turns autohide off for its own measurement — a popover dismisses every row then reports `h=0`. It turns autohide off for its own measurement — a popover dismisses
itself when it loses the grab, and `xvfb-run` has no window manager to hand it one — and restores itself when it loses the grab, and `xvfb-run` has no window manager to hand it one — and restores
it afterwards. it afterwards.
- `[helpui] auto_started=1 visible=1 steps=5 step=0 (first run)` (stderr, `UDPSCOPE_PROBE_HELPUI_MS`) — - `[helpui] auto_started=1 visible=1 steps=6 step=0 (first run)` (stderr, `UDPSCOPE_PROBE_HELPUI_MS`) —
the help affordance layer end to end, in ten steps 300-400 ms apart (every reading is an the help affordance layer end to end, in ten steps 300-400 ms apart (every reading is an
*allocation*, and a step that read its own predecessor's geometry would read a rectangle from before *allocation*, and a step that read its own predecessor's geometry would read a rectangle from before
the frame that placed it). The line above is the **first-run auto-start**, read before the probe the frame that placed it). The line above is the **first-run auto-start**, read before the probe
@@ -910,21 +914,25 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
affordances are *gone*, not greyed, so anything else is a row that hid its buttons instead of affordances are *gone*, not greyed, so anything else is a row that hid its buttons instead of
removing them. The count comes back to the same twelve, which is the other half — a toggle that removing them. The count comes back to the same twelve, which is the other half — a toggle that
removed and could not restore is a one-way switch. removed and could not restore is a one-way switch.
- `[helpui] tour started from the probe: visible=1 steps=5 step=0` — the tour started the way - `[helpui] tour started from the probe: visible=1 steps=6 step=0` — the tour started the way
`Ctrl+T` starts it. `steps=5` is the plan: the table's five steps whose surface is on screen or `Ctrl+T` starts it. `steps=6` is the plan: the table's six stops (plot, badges, trigger, dock,
selectable into it (`gtk_widget_get_mapped()` -- a wrong *page* is kept, because selecting its signals, status) whose surface is on screen or can be opened into view. A stop that names a
page reveals it, while a *collapsed pane* is still dropped; see the surface bullets in slide-over page is always kept (the `on_page` hook opens it whatever the dock is doing), a target
[invariants-gtk-and-chrome.md](invariants-gtk-and-chrome.md)). that is mapped is kept, and a stop in a *collapsed dock* is dropped (`gtk_widget_get_mapped()`; see
- `[helpui] step=1/5 visible=1 slideover=none spot=[x,y WxH] target=[…] contains=1 card=[…] clear=C` the surface bullets in [invariants-gtk-and-chrome.md](invariants-gtk-and-chrome.md)).
- `[helpui] step=1/6 visible=1 slideover=none spot=[x,y WxH] target=[…] contains=1 card=[…] clear=C`
— **in window coordinates**, the frame a screenshot is cropped in, with the slide-over's open page — **in window coordinates**, the frame a screenshot is cropped in, with the slide-over's open page
(`none`, or a page id) beside it; see `[toursurface]` below. (`none`, or a page id) beside it; see `[toursurface]` below. The `target=` rectangle is the part of the
target that is *on screen* (intersected with the scrolled windows above it, `tour_target_bounds()`): a
form taller than the panel's scroller on a short window is spotlit down to the scroller's edge and no
further.
**`contains` must be 1** on every step: the spotlight is the target's own rectangle grown three **`contains` must be 1** on every step: the spotlight is the target's own rectangle grown three
pixels. `clear` is the card missing the target, and it is 1 wherever the window has room for the pixels. `clear` is the card missing the target, and it is 1 wherever the window has room for the
card beside or above/below the hole — at the documented run's window (see `help figures:` below) card beside or above/below the hole — at the documented run's window (see `help figures:` below)
the plot's step is *the whole content*, because at 756 px the split view overlays the side panel the plot's step is *the whole content*, because at 756 px the split view overlays the side panel
rather than putting it beside the plot, so `clear=0` there is the geometry and not a placement rather than putting it beside the plot, so `clear=0` there is the geometry and not a placement
bug. The side panel's step reads `clear=1`. bug. The badges step reads `clear=1`.
- `[helpui] after Next: step=2/5 visible=1`, `[helpui] after Back: step=1/5 visible=1`, - `[helpui] after Next: step=2/6 visible=1`, `[helpui] after Back: step=1/6 visible=1`,
`[helpui] after Skip: visible=0 dims_hidden=1 spot_hidden=1 card_hidden=1 controllers=12 (was 12) `[helpui] after Skip: visible=0 dims_hidden=1 spot_hidden=1 card_hidden=1 controllers=12 (was 12)
click_gestures=2` — the three controls. **`controllers=` equal to `(was N)` is the "no lingering click_gestures=2` — the three controls. **`controllers=` equal to `(was N)` is the "no lingering
input grab" reading**: a tour of ordinary overlay widgets takes no grab at all, and a count that input grab" reading**: a tour of ordinary overlay widgets takes no grab at all, and a count that
@@ -935,7 +943,9 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
trigger pane's modes, by clicking its [edit] button. This exists because one of the help's figures trigger pane's modes, by clicking its [edit] button. This exists because one of the help's figures
is the *locked* pane's summary grid (`trigger_summary`), which in configuration mode is not on is the *locked* pane's summary grid (`trigger_summary`), which in configuration mode is not on
screen at all and reads `-1,-1,0,0`; the two lines give the geometry agent both states' numbers screen at all and reads `-1,-1,0,0`; the two lines give the geometry agent both states' numbers
rather than leaving them to find the mode. `trigger_panel`'s height differs between them (508 px in rather than leaving them to find the mode. The probe opens the slide-over's `trigger` page for these
legs (and for the collapsed-search leg after them) and closes it at the end: with the panel shut
every `trigger_*` reads `-1,-1,0,0`. `trigger_panel`'s height differs between them (532 px in
configuration mode against 72 locked — the body is replaced by the summary), which is the check configuration mode against 72 locked — the body is replaced by the summary), which is the check
that the mode really moved. that the mode really moved.
- `[helpui] collapsed search: expanded=0 mapped=1 mapped_header=1 hint=12x12` (the last step, in - `[helpui] collapsed search: expanded=0 mapped=1 mapped_header=1 hint=12x12` (the last step, in
@@ -945,28 +955,40 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
read `mapped=0` (and `0x0`), which is exactly why the header box exists (`search_head` in read `mapped=0` (and `0x0`), which is exactly why the header box exists (`search_head` in
`src/ui/trigger-panel.ui`) and why the hint joins it as its last child. `mapped_header=` is the `src/ui/trigger-panel.ui`) and why the hint joins it as its last child. `mapped_header=` is the
header box beside it, and `hint=WxH` is the same 12x12 the layer's own line reads. header box beside it, and `hint=WxH` is the same 12x12 the layer's own line reads.
- `[toursurface] …` (stderr, `UDPSCOPE_PROBE_TOUR_SURFACE_MS`) -- a stop that stands on a surface it is - `[toursurface] …` (stderr, `UDPSCOPE_PROBE_TOUR_SURFACE_MS`) -- a stop that opens a surface it is not
not on, entered from the state it is not in. Since the sidebar's tabs became the dock the probe opens the on, entered from the state it is not in. The probe opens the `trigger` slide-over page, starts the
`trigger` slide-over page instead of switching a tab, starts the window's tour and walks it a stop per tick window's tour from there and walks the whole table a stop per tick (400 ms, past the panel's 120 ms
(400 ms). **The tour's own rework for the slide-over is Task 5's**: until its page hook exists the tour slide); the `on_page` hook is what opens the page for the trigger stop and closes the panel for every
has no page stack, so a stop whose target is on a slide-over page is dropped, and the lines below read the stop that names none. The lines:
dock only. The lines:
- `[toursurface] before: slideover=none signal_list_mapped=1 running=1` -- the state a first run starts in - `[toursurface] before: slideover=none signal_list_mapped=1 running=1` -- the state a first run starts
(the dock is on screen), then the auto-started walk is dismissed. in (panel closed, dock on screen); the auto-started walk is dismissed.
- `[toursurface] on trigger: slideover=trigger signal_list_mapped=1 side_box_mapped=1` -- a frame after the - `[toursurface] on trigger: slideover=trigger trigger_body_mapped=1 side_box_mapped=1` -- a frame after
page was opened: the dock is still on screen under the slide-over (they are not alternatives). the page was opened: the dock is still on screen under the panel (they are not alternatives).
- `[toursurface] tour: visible=1 steps=N step=0 signals_stop=1` -- **`signals_stop=1` is the filter's - `[toursurface] tour: visible=1 steps=6 step=0 trigger_stop=1 signals_stop=1` -- the plan: **six
reading**: the signal list is in the dock, which is mapped, so its stop is in the plan. stops, the trigger stop and the signals stop both in it**.
- `[toursurface] step=N/M slideover=<page> target_mapped=D[ target=signal_list]` -- one per stop; - `[toursurface] step=N/6 title='…' slideover=<page> target_mapped=D` -- one per stop, in order: *The
**the `signals` stop reads `target_mapped=1`**. plot*, *The badges*, *The trigger*, *The dock*, *The signals*, *The status line*. **Step 1 reads
- `[toursurface] signals stop=N/M visible=1 slideover=… spot=[…] target=[…] contains=1 card=[…] clear=1` -- `slideover=none` although the probe started with the panel open** (a stop with no page closes it),
the `tour_geometry_line()` the helpui probe uses: **`contains=1` is the assertion** that the spotlight **step 3 reads `slideover=trigger`, and step 4 reads `slideover=none` again**; every `target_mapped`
and the card are on the list. is 1.
- `[toursurface] collapsed: side_box_mapped=0 steps=2 signals_stop=0 side_stop=0` -- the other half of - `[toursurface] stop=N/6 visible=1 slideover=… spot=[…] target=[…] contains=1 card=[…] clear=C` -- the
the filter, read: with `sidebar_wanted` off (`update_side_visibility()` re-asserts it, so this is how a `tour_geometry_line()` the helpui probe uses, one per stop, read a frame after the stop was entered
probe puts the sidebar away for good) the split view unmaps the column and the stops that need the (the spotlight is withheld until the panel has finished sliding in, `page_settled`): **`contains=1` on
dock are dropped while the page-less ones stay. This is what says a *collapsed pane* is dropped. every stop is the assertion** that the spotlight is on the target. `clear=0` is read as geometry, not
as a placement bug, on the plot stop (the target is the whole content) and on the trigger stop at the
documented 756 px window (the panel is 360 px of it, so the card has no side wide enough and goes flush
left over the plot, overlapping the panel's edge by a few pixels).
- `[toursurface] finished: running=0 slideover=none` -- the last Next ended the walk and **the panel is
closed with it**.
- `[toursurface] restarted at trigger: running=1 slideover=trigger` then
`[toursurface] skipped: running=0 slideover=none` -- the walk parked on the trigger stop and dismissed
(the Skip/Esc route): **`slideover=none` after the dismissal** is the tour not leaving a panel behind.
- `[toursurface] collapsed: side_box_mapped=0 steps=4 trigger_stop=1 signals_stop=0 side_stop=0` -- the
filter with the dock put away (`sidebar_wanted` off; `update_side_visibility()` re-asserts it, so this
is how a probe puts the dock away for good): the dock's two stops are dropped, **and the trigger stop is
kept** because the slide-over does not depend on the dock -- four stops remain (plot, badges, trigger,
status).
- `[keys] a: armed 0->1 | space: paused 0->1 | t: trigger panel: 0->1 | c: cursors: 0->1 | m: dock 0->243 (cursors_y=243 reach=2256 at_cursors=1) | g: dock 243->0 at_top=1 | … | editable focused: 'm' refused=1` (stderr, `UDPSCOPE_PROBE_KEYS_MS`) — **every `X->Y` pair must differ** (`g` returns, so its pair reads `243->0`) and **`refused` must be 1**. - `[keys] a: armed 0->1 | space: paused 0->1 | t: trigger panel: 0->1 | c: cursors: 0->1 | m: dock 0->243 (cursors_y=243 reach=2256 at_cursors=1) | g: dock 243->0 at_top=1 | … | editable focused: 'm' refused=1` (stderr, `UDPSCOPE_PROBE_KEYS_MS`) — **every `X->Y` pair must differ** (`g` returns, so its pair reads `243->0`) and **`refused` must be 1**.
The `m`/`g` readings are the dock scroller's vertical adjustment before and after (the probe pads the dock to 3000 px so there is something to scroll, and undoes it at the end): **`m` must land on `cursors_y`, the cursors panel's y in the dock (`at_cursors=1`), and `g` must return to 0 (`at_top=1`)**, which is what says those two keys scroll to a section rather than toggling a pane; a `reach` of 0 would mean the dock fit its viewport and the probe's padding was lost; `t`/`c` read the toolbar toggle's own state, because those two enable a feature and the button is where that state is visible. The refusal is the whole reason the controller is in the bubble phase: every bare-letter accel is also a character, so an accel that fires while a `GtkEditable` has focus makes the window entry unusable. A pair reading `0->0` is an action whose dispatch does not reach the handler a click reaches. Note the format is `<key>: <widget-description>: <before>-><after>` — the name passed to `before_after()` includes the widget's description, so `t: trigger panel: 0->1` is the correct form (not `t: trigger panel 0->1` without the colon before the values). The `m`/`g` readings are the dock scroller's vertical adjustment before and after (the probe pads the dock to 3000 px so there is something to scroll, and undoes it at the end): **`m` must land on `cursors_y`, the cursors panel's y in the dock (`at_cursors=1`), and `g` must return to 0 (`at_top=1`)**, which is what says those two keys scroll to a section rather than toggling a pane; a `reach` of 0 would mean the dock fit its viewport and the probe's padding was lost; `t`/`c` read the toolbar toggle's own state, because those two enable a feature and the button is where that state is visible. The refusal is the whole reason the controller is in the bubble phase: every bare-letter accel is also a character, so an accel that fires while a `GtkEditable` has focus makes the window entry unusable. A pair reading `0->0` is an action whose dispatch does not reach the handler a click reaches. Note the format is `<key>: <widget-description>: <before>-><after>` — the name passed to `before_after()` includes the widget's description, so `t: trigger panel: 0->1` is the correct form (not `t: trigger panel 0->1` without the colon before the values).
- `[color] '<name>' at row R slot S: trace=#RRGGBB chip_agrees=1 (want 1)`, then `[color] override: trace=#123456 chip_agrees=1`, `[color] session: entries=1 rgb=#123456` and `[color] default: trace=#RRGGBB chip_agrees=1` (stderr, `UDPSCOPE_PROBE_COLOR_MS`) — **all three `chip_agrees` must be 1**, the override line's `trace` must be `#123456`, the session line must read `entries=1 rgb=#123456`, and the default line's `trace` must be back to the first line's. Run it with `UDPSCOPE_ENABLE_ALL_MS` ahead of it, or there is no bound trace and the probe prints `no bound trace to recolour` instead of asserting anything; on the standard mock the first line reads `'sine' at row 0 slot 0: trace=#4FA3FF`. Two things make this an assertion rather than a tautology. The colours are read out of **`plot::Plot::trace().color`** — what the renderer will stroke, reached through `Row::plot` *after* `relayout_areas()`, since a new colour is a new figure — and out of the chip's own `<span foreground="…">` markup, not by asking `RowSet::signal_color()` twice, which would pass against a chip that never reached the trace. And the override is `#123456`, deliberately in no palette slot: an override equal to the entry it replaces passes against an implementation that ignores overrides entirely. The session leg round-trips `collect_layout()` → `save_layout()` → `load_layout()` on a temp path and deletes it, because a probe must not leave a colour in the real session file for a later run to inherit. - `[color] '<name>' at row R slot S: trace=#RRGGBB chip_agrees=1 (want 1)`, then `[color] override: trace=#123456 chip_agrees=1`, `[color] session: entries=1 rgb=#123456` and `[color] default: trace=#RRGGBB chip_agrees=1` (stderr, `UDPSCOPE_PROBE_COLOR_MS`) — **all three `chip_agrees` must be 1**, the override line's `trace` must be `#123456`, the session line must read `entries=1 rgb=#123456`, and the default line's `trace` must be back to the first line's. Run it with `UDPSCOPE_ENABLE_ALL_MS` ahead of it, or there is no bound trace and the probe prints `no bound trace to recolour` instead of asserting anything; on the standard mock the first line reads `'sine' at row 0 slot 0: trace=#4FA3FF`. Two things make this an assertion rather than a tautology. The colours are read out of **`plot::Plot::trace().color`** — what the renderer will stroke, reached through `Row::plot` *after* `relayout_areas()`, since a new colour is a new figure — and out of the chip's own `<span foreground="…">` markup, not by asking `RowSet::signal_color()` twice, which would pass against a chip that never reached the trace. And the override is `#123456`, deliberately in no palette slot: an override equal to the entry it replaces passes against an implementation that ignores overrides entirely. The session leg round-trips `collect_layout()` → `save_layout()` → `load_layout()` on a temp path and deletes it, because a probe must not leave a colour in the real session file for a later run to inherit.
+7 -5
View File
@@ -639,11 +639,13 @@ as distinct from the fault injection below, which exercises ingest:
second and a half, and that start *writes* `tour_seen` to whichever config directory the run is second and a half, and that start *writes* `tour_seen` to whichever config directory the run is
using, which is why the second run against the same one reads `auto=0`. using, which is why the second run against the same one reads `auto=0`.
- `UDPSCOPE_PROBE_TOUR_SURFACE_MS` — the tour's *surface* switch: it opens a slide-over page (`trigger`), - `UDPSCOPE_PROBE_TOUR_SURFACE_MS` — the tour's *surface* switch: it opens a slide-over page (`trigger`),
starts the window's tour, and walks it a stop per tick to the signals stop, reading the sidebar's starts the window's tour from that state and walks all six stops a stop per tick, reading the
page and the spotlight/card where the walk arrives. It then puts the sidebar *away* and starts the slide-over's open page and the spotlight/card at each. The trigger stop must read `slideover=trigger` and
tour again, which is the same filter's other case (a collapsed pane drops the stops a page switch the next `slideover=none`; the first stop closes the panel the probe left open, and the end of the walk
could have revealed). `[toursurface] signals stop=4/5 … slideover=none … contains=1` and (and a dismissal on the trigger stop) leave none. It then puts the dock *away* and starts the tour again:
`[toursurface] collapsed: side_box_mapped=0 steps=2 signals_stop=0` are the two assertions; its the dock's stops are dropped while the trigger stop, which does not depend on the dock, stays.
`[toursurface] stop=3/6 … slideover=trigger … contains=1` and
`[toursurface] collapsed: side_box_mapped=0 steps=4 trigger_stop=1 signals_stop=0` are the assertions; its
lines are in [probe-readings.md](probe-readings.md). Run it alone with lines are in [probe-readings.md](probe-readings.md). Run it alone with
`UDPSCOPE_EXIT_AFTER_MS=9000 UDPSCOPE_EXIT_DUMP=1` against a live source. `UDPSCOPE_EXIT_AFTER_MS=9000 UDPSCOPE_EXIT_DUMP=1` against a live source.
- `UDPSCOPE_PROBE_DEFAULTS_MS` — opens **Calibrate** and reads every spin button in it and in the - `UDPSCOPE_PROBE_DEFAULTS_MS` — opens **Calibrate** and reads every spin button in it and in the
+19 -3
View File
@@ -73,9 +73,11 @@ enum class EventGroupKind {
/// do not exist yet. /// do not exist yet.
struct TourStop { struct TourStop {
GtkWidget* target; GtkWidget* target;
/// The named page of the view's `page_stack` the target lives on, or nullptr when the stop needs no /// The page the target lives on, or nullptr when the stop needs none. What a page *is* depends on
/// page. The tour selects it as the stop is entered, so a target on a page the user has left is /// the view: a named page of its `page_stack` (the chooser), or whatever its `on_page` hook opens
/// made visible instead of being walked over or spotlighted where it is not. /// (the main window: a slide-over page id). The tour selects it as the stop is entered, so a target
/// on a page the user has left is made visible instead of being walked over or spotlighted where it
/// is not. A stop with no page *closes* what an `on_page` hook opened.
const char* page; const char* page;
const char* title; const char* title;
const char* body; const char* body;
@@ -103,6 +105,16 @@ struct TourView {
/// sidebar; the chooser's two pages), so it is the view's and not each stop's. Set where the walk /// sidebar; the chooser's two pages), so it is the view's and not each stop's. Set where the walk
/// begins, when the widgets exist; null leaves every stop on the page it finds. /// begins, when the widgets exist; null leaves every stop on the page it finds.
GtkWidget* page_stack = nullptr; GtkWidget* page_stack = nullptr;
/// The alternative to `page_stack` for a surface that is not a stack: when set it *replaces* the
/// stack mechanism for this view, and is called with each stop's `page` as the stop is entered --
/// `nullptr` meaning "no page needed" -- and with `nullptr` as the walk ends. The main window's opens
/// and closes the slide-over through it; the chooser keeps `page_stack`.
void (*on_page)(void* user, const char* page) = nullptr;
void* page_user = nullptr;
/// Optional, with `on_page`: whether the surface `on_page` opened has finished appearing. A stop that
/// names a page is not spotlit while this answers false -- a panel that is still sliding in is a
/// target that moves, and a spotlight that follows it for 120 ms reads as a glitch. Null means "ready".
bool (*page_settled)(void* user) = nullptr;
/// Runs when the walk passes its last stop (Next/Finish, never Skip): the chooser's hands the tour /// Runs when the walk passes its last stop (Next/Finish, never Skip): the chooser's hands the tour
/// on to the application through `Config::tour_pending`. /// on to the application through `Config::tour_pending`.
void (*on_done)(void* user) = nullptr; void (*on_done)(void* user) = nullptr;
@@ -2114,6 +2126,10 @@ void tour_end(TourView& view);
void tour_update(TourView& view); void tour_update(TourView& view);
/// The widget the current stop points at, or nullptr when the walk is not on a stop. /// The widget the current stop points at, or nullptr when the walk is not on a stop.
GtkWidget* tour_target(TourView& view); GtkWidget* tour_target(TourView& view);
/// The part of `target` that is on screen, in `overlay`'s coordinates: its bounds intersected with every
/// scrolled window above it. False when it cannot be measured yet or is scrolled out of sight entirely.
/// The spotlight is drawn from it, and the probe reads the spotlight against it.
bool tour_target_bounds(GtkWidget* target, GtkWidget* overlay, graphene_rect_t* out);
/// Builds the *window's* tour inside `app.tour.overlay`. Called once, as the overlay is inserted. /// Builds the *window's* tour inside `app.tour.overlay`. Called once, as the overlay is inserted.
void build_tour(App& app); void build_tour(App& app);
+67 -45
View File
@@ -6657,8 +6657,8 @@ GtkWidget* helpui_menu_view(App* a) {
} }
/// One line: the spotlight, the widget it spotlights, and the card, all in **window coordinates** -- /// One line: the spotlight, the widget it spotlights, and the card, all in **window coordinates** --
/// the frame a screenshot is cropped in -- with the window's own record of the sidebar page beside /// the frame a screenshot is cropped in -- with the slide-over's open page beside them (`none` when it
/// them, so a stop that had to change the page to find its target is readable as such. `contains` is /// is closed), so a stop that had to open a page to find its target is readable as such. `contains` is
/// the spotlight covering its target (it is the target's own rectangle, grown three pixels) and /// the spotlight covering its target (it is the target's own rectangle, grown three pixels) and
/// `clear` is the card missing the target. `probe` is the tag it is printed under, so a window with /// `clear` is the card missing the target. `probe` is the tag it is printed under, so a window with
/// more than one tour probe can tell its lines apart. /// more than one tour probe can tell its lines apart.
@@ -6670,7 +6670,7 @@ void tour_geometry_line(App& a, const char* probe, const char* what) {
const bool have_spot = const bool have_spot =
a.tour.spot != nullptr && gtk_widget_compute_bounds(a.tour.spot, a.window, &spot); a.tour.spot != nullptr && gtk_widget_compute_bounds(a.tour.spot, a.window, &spot);
const bool have_target = t != nullptr && gtk_widget_get_mapped(t) && const bool have_target = t != nullptr && gtk_widget_get_mapped(t) &&
gtk_widget_compute_bounds(t, a.window, &target); tour_target_bounds(t, a.window, &target);
const bool have_card = const bool have_card =
a.tour.card != nullptr && gtk_widget_compute_bounds(a.tour.card, a.window, &card); a.tour.card != nullptr && gtk_widget_compute_bounds(a.tour.card, a.window, &card);
const auto contains = [](const graphene_rect_t& outer, const graphene_rect_t& inner) { const auto contains = [](const graphene_rect_t& outer, const graphene_rect_t& inner) {
@@ -6859,6 +6859,10 @@ gboolean helpui_probe_step(gpointer user) {
// The trigger pane's summary grid is on screen only in *locked* mode, and one of the help's // The trigger pane's summary grid is on screen only in *locked* mode, and one of the help's
// figures is that content: the probe reads `help figures:` in both states, so the geometry agent // figures is that content: the probe reads `help figures:` in both states, so the geometry agent
// has the locked pane's numbers without having to guess how to get there. // has the locked pane's numbers without having to guess how to get there.
// The trigger form is a slide-over page: it is open for both readings (and for the collapsed-search
// one after them), because a closed panel leaves every one of its surfaces unmapped and reads
// `-1,-1,0,0`. The 400 ms of the step is past the 120 ms reveal.
if (step == 6) slideover_open(*a, "trigger");
if (a->trigger_edit != nullptr) g_signal_emit_by_name(a->trigger_edit, "clicked"); if (a->trigger_edit != nullptr) g_signal_emit_by_name(a->trigger_edit, "clicked");
helpui_probe_next(*a, step == 6 ? 7 : 9, 400); helpui_probe_next(*a, step == 6 ? 7 : 9, 400);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
@@ -6891,6 +6895,7 @@ gboolean helpui_probe_step(gpointer user) {
hint != nullptr && gtk_widget_get_mapped(hint) ? 1 : 0, hint != nullptr && gtk_widget_get_mapped(hint) ? 1 : 0,
gtk_widget_get_mapped(a->trigger_search) ? 1 : 0, sw, sh); gtk_widget_get_mapped(a->trigger_search) ? 1 : 0, sw, sh);
} }
slideover_close(*a); // opened by step 6
std::fflush(stderr); std::fflush(stderr);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
@@ -6898,7 +6903,7 @@ gboolean helpui_probe_step(gpointer user) {
// --- the tour's surface switch: a stop that stands on another stack page ------- // --- the tour's surface switch: a stop that stands on another stack page -------
/// Whether the walk's plan holds `target`. `steps=` alone cannot say a stop was *filtered out* rather /// Whether the walk's plan holds `target`. `steps=` alone cannot say a stop was *filtered out* rather
/// than merely reordered, so the two cases the filter tells apart are read against the plan itself. /// than merely reordered, so the cases the filter tells apart are read against the plan itself.
bool tour_plan_holds(const App& a, GtkWidget* target) { bool tour_plan_holds(const App& a, GtkWidget* target) {
for (const TourStop& stop : a.tour.plan) { for (const TourStop& stop : a.tour.plan) {
if (stop.target == target) return true; if (stop.target == target) return true;
@@ -6912,27 +6917,34 @@ void tour_surface_probe_next(App& a, guint delay_ms) {
g_timeout_add(delay_ms, tour_surface_probe_step, &a); g_timeout_add(delay_ms, tour_surface_probe_step, &a);
} }
/// `UDPSCOPE_PROBE_TOUR_SURFACE_MS`: the stop that stands on a page it is not on. It puts the sidebar /// The slide-over's open page for a reading: its id, or `none`.
/// on **Measure**, starts the window's tour, and walks it a stop per tick -- a tick apart on purpose, const char* slideover_name(const App& a) {
/// because a stack's new page is allocated on the frame *after* the switch, so a reading taken in the return slideover_page(a).empty() ? "none" : slideover_page(a).c_str();
/// switch's own tick would be of the page the walk has just left (and `update_tour()` places the hole }
/// from the frame that *has* been laid out). Every step prints the sidebar's page and whether the
/// stop's target is mapped, and the stop whose target is the signal list prints the same geometry /// `UDPSCOPE_PROBE_TOUR_SURFACE_MS`: the stop that opens a surface it is not on. It opens the
/// `[helpui]` does: **`side_tab=signals` with `target=signal_list` and `contains=1`** is the reading /// slide-over's `trigger` page, starts the window's tour from that state, and walks the whole table a
/// that the stop was not merely kept but *made* visible. It then puts the sidebar away -- /// stop per tick -- a tick apart on purpose (400 ms, longer than the panel's 120 ms slide), because a
/// `sidebar_wanted` is the wish the tick re-asserts -- and starts the tour again: a *collapsed* pane /// reading taken in the switch's own tick would be of the surface the walk has just left (and
/// leaves the stack unmapped, so the stops that need it are dropped and `steps=` falls. That pair is /// `update_tour()` places the hole from the frame that *has* been laid out). Every stop prints its
/// the filter's two cases, read rather than assumed. /// title, the slide-over's open page and whether the target is mapped, with the spotlight geometry
/// `[helpui]` prints: **`slideover=trigger` at the trigger stop and `slideover=none` at the one after
/// it, with `contains=1`**, is the reading that the walk opened the panel and then closed it. The walk
/// then runs again and is dismissed on the trigger stop (the Skip/Esc route), which must close the
/// panel too. It finally puts the dock *away* -- `sidebar_wanted` is the wish the tick re-asserts --
/// and starts the tour once more: the dock's stops are dropped (a collapsed pane is not walked over)
/// while the trigger stop, which does not depend on the dock, is kept.
gboolean tour_surface_probe_step(gpointer user) { gboolean tour_surface_probe_step(gpointer user) {
App* a = static_cast<App*>(user); App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE; if (a->window == nullptr) return G_SOURCE_REMOVE;
const auto mapped = [](GtkWidget* w) { return w != nullptr && gtk_widget_get_mapped(w) ? 1 : 0; }; const auto mapped = [](GtkWidget* w) { return w != nullptr && gtk_widget_get_mapped(w) ? 1 : 0; };
switch (a->dbg_tour_surface_step) { switch (a->dbg_tour_surface_step) {
case 0: { case 0: {
// Where the run starts: a fresh config's sidebar is on Signals, so this is a *first* run's // Where the run starts: the panel closed and the dock on screen. The auto-start's walk (if one
// state. The auto-start's walk (if one began) would only confuse the reading; it goes first. // began) would only confuse the reading; it goes first. The panel is then opened, so the walk's
// first stop (which needs no page) has something to close.
std::fprintf(stderr, "[toursurface] before: slideover=%s signal_list_mapped=%d running=%d\n", std::fprintf(stderr, "[toursurface] before: slideover=%s signal_list_mapped=%d running=%d\n",
slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str(), mapped(a->signal_list), tour_active(*a) ? 1 : 0); slideover_name(*a), mapped(a->signal_list), tour_active(*a) ? 1 : 0);
tour_dismiss(*a); tour_dismiss(*a);
slideover_open(*a, "trigger"); slideover_open(*a, "trigger");
a->dbg_tour_surface_step = 1; a->dbg_tour_surface_step = 1;
@@ -6940,15 +6952,16 @@ gboolean tour_surface_probe_step(gpointer user) {
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
case 1: { case 1: {
// A frame after the switch, so the Measure page has been laid out and the list really is off // A frame after the page opened, so the panel has slid in. The tour is started *here*, from this
// screen. The tour is started *here*, from this state. // state.
std::fprintf(stderr, std::fprintf(stderr,
"[toursurface] on trigger: slideover=%s signal_list_mapped=%d side_box_mapped=%d\n", "[toursurface] on trigger: slideover=%s trigger_body_mapped=%d side_box_mapped=%d\n",
slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str(), slideover_name(*a), mapped(a->trigger_body), mapped(a->side_box));
mapped(a->signal_list), mapped(a->side_box));
start_tour(*a); start_tour(*a);
std::fprintf(stderr, "[toursurface] tour: visible=%d steps=%d step=%d signals_stop=%d\n", std::fprintf(stderr,
"[toursurface] tour: visible=%d steps=%d step=%d trigger_stop=%d signals_stop=%d\n",
tour_active(*a) ? 1 : 0, tour_step_count(*a), tour_step_index(*a), tour_active(*a) ? 1 : 0, tour_step_count(*a), tour_step_index(*a),
tour_plan_holds(*a, a->trigger_body) ? 1 : 0,
tour_plan_holds(*a, a->signal_list) ? 1 : 0); tour_plan_holds(*a, a->signal_list) ? 1 : 0);
a->dbg_tour_surface_step = 2; a->dbg_tour_surface_step = 2;
tour_surface_probe_next(*a, 400); tour_surface_probe_next(*a, 400);
@@ -6956,44 +6969,53 @@ gboolean tour_surface_probe_step(gpointer user) {
} }
case 2: { case 2: {
if (!tour_active(*a)) { if (!tour_active(*a)) {
std::fprintf(stderr, "[toursurface] walk ended before the signals stop\n"); // The last Next ended the walk: the panel must have gone with it.
std::fflush(stderr); std::fprintf(stderr, "[toursurface] finished: running=0 slideover=%s\n", slideover_name(*a));
return G_SOURCE_REMOVE; start_tour(*a);
} while (tour_active(*a) && tour_target(*a) != a->trigger_body) tour_step(*a, 1);
GtkWidget* target = tour_target(*a);
const bool is_list = target != nullptr && target == a->signal_list;
std::fprintf(stderr, "[toursurface] step=%d/%d slideover=%s target_mapped=%d%s\n",
tour_step_index(*a) + 1, tour_step_count(*a),
slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str(), mapped(target),
is_list ? " target=signal_list" : "");
if (is_list) {
// The frame after the switch: the page has been laid out and `update_tour()` has placed the
// hole from it, so this is the reading that the spotlight is on the list.
tour_geometry_line(*a, "toursurface", "signals stop");
tour_dismiss(*a);
a->dbg_tour_surface_step = 3; a->dbg_tour_surface_step = 3;
tour_surface_probe_next(*a, 400); tour_surface_probe_next(*a, 400);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
GtkWidget* target = tour_target(*a);
const TourStop& stop = a->tour.plan[static_cast<std::size_t>(tour_step_index(*a))];
std::fprintf(stderr, "[toursurface] step=%d/%d title='%s' slideover=%s target_mapped=%d\n",
tour_step_index(*a) + 1, tour_step_count(*a), stop.title, slideover_name(*a),
mapped(target));
// The frame after the switch: the page has been laid out and `update_tour()` has placed the
// hole from it, so this is the reading that the spotlight is on the stop's target.
tour_geometry_line(*a, "toursurface", "stop");
tour_step(*a, 1); tour_step(*a, 1);
tour_surface_probe_next(*a, 400); tour_surface_probe_next(*a, 400);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
case 3: { case 3: {
// The other half of the filter. `sidebar_wanted` is the user's wish that the tick re-asserts // The walk again, parked on the trigger stop with the panel open: Skip (or Esc) closes it.
// (`update_side_visibility`), so setting it here is how a probe puts the sidebar away for good; std::fprintf(stderr, "[toursurface] restarted at trigger: running=%d slideover=%s\n",
// the delay leaves the split view time to animate the column shut and unmap it. tour_active(*a) ? 1 : 0, slideover_name(*a));
a->sidebar_wanted = false; tour_dismiss(*a);
std::fprintf(stderr, "[toursurface] skipped: running=%d slideover=%s\n",
tour_active(*a) ? 1 : 0, slideover_name(*a));
a->dbg_tour_surface_step = 4; a->dbg_tour_surface_step = 4;
tour_surface_probe_next(*a, 100);
return G_SOURCE_REMOVE;
}
case 4: {
// The other half of the filter. `sidebar_wanted` is the user's wish that the tick re-asserts
// (`update_side_visibility`), so setting it here is how a probe puts the dock away for good; the
// delay leaves the split view time to animate the column shut and unmap it.
a->sidebar_wanted = false;
a->dbg_tour_surface_step = 5;
tour_surface_probe_next(*a, 700); tour_surface_probe_next(*a, 700);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
default: { default: {
start_tour(*a); start_tour(*a);
std::fprintf(stderr, std::fprintf(stderr,
"[toursurface] collapsed: side_box_mapped=%d steps=%d signals_stop=%d " "[toursurface] collapsed: side_box_mapped=%d steps=%d trigger_stop=%d "
"side_stop=%d\n", "signals_stop=%d side_stop=%d\n",
mapped(a->side_box), tour_step_count(*a), mapped(a->side_box), tour_step_count(*a),
tour_plan_holds(*a, a->trigger_body) ? 1 : 0,
tour_plan_holds(*a, a->signal_list) ? 1 : 0, tour_plan_holds(*a, a->signal_list) ? 1 : 0,
tour_plan_holds(*a, a->side_box) ? 1 : 0); tour_plan_holds(*a, a->side_box) ? 1 : 0);
tour_dismiss(*a); tour_dismiss(*a);
+101 -28
View File
@@ -16,9 +16,9 @@
// (see the allocation bullets in `docs/claude/invariants-trigger-and-pinning.md`), and a window // (see the allocation bullets in `docs/claude/invariants-trigger-and-pinning.md`), and a window
// resize moves it under a spotlight that a one-shot measurement would leave behind. // resize moves it under a spotlight that a one-shot measurement would leave behind.
// //
// There are *two* tours and one implementation. The main window's walks the plot, the side panel, // There are *two* tours and one implementation. The main window's walks the plot, the badge bar, the
// the trigger and the like; the source chooser's -- which appears *before* `App` exists -- walks the // trigger (in the slide-over), the dock and the like; the source chooser's -- which appears *before*
// chooser's own widgets and then hands the tour on to the window through `Config::tour_pending`. The // `App` exists -- walks the chooser's own widgets and then hands the tour on to the window through `Config::tour_pending`. The
// machinery below is written against a `TourView` and a list of `TourStop`s, so the difference // machinery below is written against a `TourView` and a list of `TourStop`s, so the difference
// between the two is only which widgets they name: the chooser keeps its table in // between the two is only which widgets they name: the chooser keeps its table in
// `source_dialog.cpp`, where its widgets live. // `source_dialog.cpp`, where its widgets live.
@@ -76,12 +76,18 @@ const TourStop* current_stop(const TourView& view) {
return &view.plan[static_cast<std::size_t>(view.index)]; return &view.plan[static_cast<std::size_t>(view.index)];
} }
/// Selects a stop's page on the view's stack, before anything is measured against the target. Both /// Selects a stop's page before anything is measured against the target. Two mechanisms, one per
/// windows' stacks are `AdwViewStack`s and their page names are the ones the same tables name their /// kind of view: a view with an `on_page` hook (the main window: its pages are slide-over pages, and
/// `.ui` pages with, so the machinery can do this itself; the `get_child_by_name()` guard keeps a /// a stop with no page *closes* the panel) hands the page to the hook; a view with a `page_stack` (the
/// stop that names a page a later `.ui` dropped from blanking the stack. The stack's own /// source chooser: an `AdwViewStack` whose page names are the ones its `.ui` names its pages with)
/// `notify::visible-child-name` is what tells the window which page it is now on. /// has the machinery select it. The `get_child_by_name()` guard keeps a stop that names a page a
/// later `.ui` dropped from blanking the stack. The stack's own `notify::visible-child-name` is what
/// tells the window which page it is now on.
void select_view_page(TourView& view, const char* page) { void select_view_page(TourView& view, const char* page) {
if (view.on_page != nullptr) {
view.on_page(view.page_user, page);
return;
}
if (view.page_stack == nullptr || page == nullptr) return; if (view.page_stack == nullptr || page == nullptr) return;
if (adw_view_stack_get_child_by_name(ADW_VIEW_STACK(view.page_stack), page) == nullptr) return; if (adw_view_stack_get_child_by_name(ADW_VIEW_STACK(view.page_stack), page) == nullptr) return;
adw_view_stack_set_visible_child_name(ADW_VIEW_STACK(view.page_stack), page); adw_view_stack_set_visible_child_name(ADW_VIEW_STACK(view.page_stack), page);
@@ -91,13 +97,16 @@ void select_view_page(TourView& view, const char* page) {
/// pages, and the split view its sidebar, with `gtk_widget_set_child_visible()`, which leaves the /// pages, and the split view its sidebar, with `gtk_widget_set_child_visible()`, which leaves the
/// widget's own visible flag -- the one `is_visible()` walks up the parents -- **true**. So the only /// widget's own visible flag -- the one `is_visible()` walks up the parents -- **true**. So the only
/// honest "is this on screen" test is whether it is mapped, and it is the one that keeps the filter's /// honest "is this on screen" test is whether it is mapped, and it is the one that keeps the filter's
/// two cases apart: /// cases apart:
/// * a stop that names a page of a view with an `on_page` hook is always walkable: the hook opens
/// the page whatever the rest of the window is doing (the slide-over does not depend on the dock);
/// * a target not mapped because it is on *another page* is walked over when the stop names that /// * a target not mapped because it is on *another page* is walked over when the stop names that
/// page and the stack itself is on screen -- selecting the page is what puts the target there; /// page and the stack itself is on screen -- selecting the page is what puts the target there;
/// * a target not mapped because its *pane is collapsed* leaves the stack unmapped too, so no page /// * a target not mapped because its *pane is collapsed* leaves the stack unmapped too, so no page
/// can reveal it and the stop is still dropped. /// can reveal it and the stop is still dropped.
bool stop_is_walkable(const TourView& view, const TourStop& stop) { bool stop_is_walkable(const TourView& view, const TourStop& stop) {
if (stop.target == nullptr) return false; if (stop.target == nullptr) return false;
if (stop.page != nullptr && view.on_page != nullptr) return true;
if (gtk_widget_get_mapped(stop.target)) return true; if (gtk_widget_get_mapped(stop.target)) return true;
return stop.page != nullptr && view.page_stack != nullptr && return stop.page != nullptr && view.page_stack != nullptr &&
gtk_widget_get_mapped(view.page_stack); gtk_widget_get_mapped(view.page_stack);
@@ -130,6 +139,25 @@ void show_step(TourView& view) {
} // namespace } // namespace
bool tour_target_bounds(GtkWidget* target, GtkWidget* overlay, graphene_rect_t* out) {
if (target == nullptr || overlay == nullptr || out == nullptr) return false;
if (!gtk_widget_compute_bounds(target, overlay, out)) return false;
// A target taller than the scroller it stands in (a form on a short window) is clipped by that
// scroller: the part below its edge is not on screen, and a hole that kept going would spotlight
// whatever is under the scroller instead -- the badge bar, the status line. So the hole is the
// *visible* part, intersected with every scrolled window above the target.
for (GtkWidget* parent = gtk_widget_get_parent(target); parent != nullptr;
parent = gtk_widget_get_parent(parent)) {
if (!GTK_IS_SCROLLED_WINDOW(parent)) continue;
graphene_rect_t clip;
graphene_rect_t cut;
if (!gtk_widget_compute_bounds(parent, overlay, &clip)) continue;
if (!graphene_rect_intersection(out, &clip, &cut)) return false;
*out = cut;
}
return true;
}
// --- the machinery, shared by both windows ---------------------------------- // --- the machinery, shared by both windows ----------------------------------
void tour_build(TourView& view, GtkWidget* overlay) { void tour_build(TourView& view, GtkWidget* overlay) {
@@ -241,6 +269,10 @@ void tour_advance(TourView& view, int delta) {
} }
void tour_end(TourView& view) { void tour_end(TourView& view) {
// A page the walk opened is the walk's to close: the panel must not be left standing over a window
// whose tour has ended (Skip, Esc, or the last Next). Only when a walk was actually running --
// dismissing an idle tour must not close a panel the user opened.
if (view.running && view.on_page != nullptr) view.on_page(view.page_user, nullptr);
view.running = false; view.running = false;
view.index = 0; view.index = 0;
view.plan.clear(); view.plan.clear();
@@ -271,8 +303,15 @@ void tour_update(TourView& view) {
// Mapped, not `is_visible()`: a widget on another page of a stack, or in a collapsed pane, keeps // Mapped, not `is_visible()`: a widget on another page of a stack, or in a collapsed pane, keeps
// its own visible flag -- a stack hides its pages with `set_child_visible()` -- and a widget that // its own visible flag -- a stack hides its pages with `set_child_visible()` -- and a widget that
// is not on screen is not one to leave the spotlight standing on. // is not on screen is not one to leave the spotlight standing on.
const bool target_present = target != nullptr && gtk_widget_get_mapped(target); bool target_present = target != nullptr && gtk_widget_get_mapped(target);
if (target_present && gtk_widget_compute_bounds(target, view.overlay, &bounds)) { // A stop on a page waits for the page to finish appearing: until then the target is moving, and
// the frame is drawn as a stop with nothing to point at (everything dim, the hole empty).
const TourStop* stop = current_stop(view);
if (target_present && stop != nullptr && stop->page != nullptr && view.page_settled != nullptr &&
!view.page_settled(view.page_user)) {
target_present = false;
}
if (target_present && tour_target_bounds(target, view.overlay, &bounds)) {
// The hole: the target, grown by three pixels so the outline sits *around* the widget rather than // The hole: the target, grown by three pixels so the outline sits *around* the widget rather than
// on it, and clamped to the overlay -- a target partly off screen is an ordinary case (a sidebar // on it, and clamped to the overlay -- a target partly off screen is an ordinary case (a sidebar
// dragged wide), and a negative rectangle is not something a margin can express. // dragged wide), and a negative rectangle is not something a margin can express.
@@ -354,6 +393,13 @@ void tour_update(TourView& view) {
} else if (hx >= cw + 2 * kGap) { } else if (hx >= cw + 2 * kGap) {
cx = hx - cw - kGap; cx = hx - cw - kGap;
cy = std::clamp(hy, kGap, std::max(kGap, h - ch - kGap)); cy = std::clamp(hy, kGap, std::max(kGap, h - ch - kGap));
} else if (hx > w - (hx + hw) + kGap) {
// No side wide enough for the card, but the hole is at the right edge (the slide-over's trigger
// form on a narrow window: 276 px to its left against a 289 px card): the card goes flush left,
// overlapping the plot and as little of the target as there is no way to avoid, instead of
// sitting on the target's own corner.
cx = kGap;
cy = std::clamp(hy, kGap, std::max(kGap, h - ch - kGap));
} else { } else {
// No clear side at all -- the target is nearly the whole window, which is the plot's step in a // No clear side at all -- the target is nearly the whole window, which is the plot's step in a
// window narrow enough for the split view to *overlay* the side panel rather than put it beside // window narrow enough for the split view to *overlay* the side panel rather than put it beside
@@ -374,9 +420,9 @@ namespace {
/// point of re-measuring every frame is that nothing here knows where the widget is. /// point of re-measuring every frame is that nothing here knows where the widget is.
struct TourStep { struct TourStep {
const char* id; const char* id;
/// The sidebar page the target lives on, or nullptr when the stop is not on either page. The /// The slide-over page the target lives on, or nullptr when the stop needs none. A stop with a page
/// trigger and the channel list are both on the Signals page, so a stop about either has to put /// opens it; a stop without one closes the panel, so the card never points at the status line over
/// the sidebar there before it can spotlight one. /// an open panel. The page ids are the slide-over's (`trigger`, `source-<id>`, ...).
const char* page; const char* page;
const char* title; const char* title;
const char* body; const char* body;
@@ -388,15 +434,20 @@ constexpr TourStep kTourSteps[] = {
"Every signal you have switched on is drawn here, all rows sharing one time axis. The wheel " "Every signal you have switched on is drawn here, all rows sharing one time axis. The wheel "
"zooms, a drag pans, and a right-click on a row opens that plot's own menu.", "zooms, a drag pans, and a right-click on a row opens that plot's own menu.",
&App::area_overlay}, &App::area_overlay},
{"side", nullptr, "The side panel", {"badges", nullptr, "The badges",
"The working surface beside the plot. Measure holds the cursors and the measurements; Signals " "Setup is reached by the thing it configures. The Trigger badge opens the trigger form; the "
"holds the trigger and the list of signals. The buttons in the bar above show and hide it.", "Source badge opens the source's own controls. Click it again, press Esc or click the plot to "
&App::side_box}, "close the panel.",
{"trigger", "signals", "The trigger", &App::badge_bar},
{"trigger", "trigger", "The trigger",
"Describe an event -- a signal, a condition, a level and the window around it -- and arm it: " "Describe an event -- a signal, a condition, a level and the window around it -- and arm it: "
"the view then freezes on the first event that matches, however rare it is.", "the view then freezes on the first event that matches, however rare it is.",
&App::trigger_body}, &App::trigger_body},
{"signals", "signals", "The signals", {"side", nullptr, "The dock",
"The dock beside the plot: the list of signals, then the cursors and the measurements. The "
"buttons in the bar above show and hide it.",
&App::side_box},
{"signals", nullptr, "The signals",
"Every signal the source declares, each with the switch that acquires it. A signal has to be " "Every signal the source declares, each with the switch that acquires it. A signal has to be "
"switched on before it can be plotted, measured or searched.", "switched on before it can be plotted, measured or searched.",
&App::signal_list}, &App::signal_list},
@@ -407,6 +458,23 @@ constexpr TourStep kTourSteps[] = {
}; };
constexpr int kTourStepCount = static_cast<int>(sizeof(kTourSteps) / sizeof(kTourSteps[0])); constexpr int kTourStepCount = static_cast<int>(sizeof(kTourSteps) / sizeof(kTourSteps[0]));
/// The tour's page hook: a page is a slide-over page, and no page closes the panel.
void tour_open_page(void* user, const char* page) {
App& app = *static_cast<App*>(user);
if (page != nullptr) {
slideover_open(app, page);
} else {
slideover_close(app);
}
}
/// Whether the slide-over has finished sliding in. A closed panel has nothing to wait for.
bool tour_page_settled(void* user) {
const App& app = *static_cast<App*>(user);
if (app.slideover.revealer == nullptr || app.slideover.page.empty()) return true;
return gtk_revealer_get_child_revealed(GTK_REVEALER(app.slideover.revealer)) != FALSE;
}
} // namespace } // namespace
void build_tour(App& app) { void build_tour(App& app) {
@@ -422,9 +490,12 @@ void start_tour(App& app) {
app.settings.tour_seen = true; app.settings.tour_seen = true;
(void)save_settings(app.settings); (void)save_settings(app.settings);
} }
// The main window has no page stack any more (the dock is one scroller): `page_stack` stays null, so // The main window has no page stack (the dock is one scroller): its pages are the slide-over's, and
// a stop's `page` selects nothing and a stop whose target is unmapped is dropped. The slide-over hook // the view's `on_page` hook opens one (or closes the panel for a stop with no page), replacing the
// that lets a stop open the trigger page is the next step's. // `page_stack` mechanism the chooser keeps.
app.tour.on_page = tour_open_page;
app.tour.page_settled = tour_page_settled;
app.tour.page_user = &app;
// The whole table, in walk order: `tour_begin()` drops the stops whose surface is not on screen. // The whole table, in walk order: `tour_begin()` drops the stops whose surface is not on screen.
std::vector<TourStop> stops; std::vector<TourStop> stops;
stops.reserve(kTourStepCount); stops.reserve(kTourStepCount);
@@ -484,7 +555,8 @@ constexpr FigureSurface kFigureSurfaces[] = {
{"trigger_summary", &App::trigger_summary}, {"trigger_summary", &App::trigger_summary},
{"trigger_panel", &App::trigger_panel}, {"trigger_panel", &App::trigger_panel},
{"signal_list", &App::signal_list}, {"signal_list", &App::signal_list},
{"side_stack", &App::side_box}, {"badge_bar", &App::badge_bar},
{"dock", &App::side_box},
{"window_button", &App::window_button}, {"window_button", &App::window_button},
{"status_label", &App::status_label}, {"status_label", &App::status_label},
{"header_bar", &App::toolbar}, {"header_bar", &App::toolbar},
@@ -526,10 +598,11 @@ void capture_help_state(App& app) {
GtkWidget* widget = app.*(surface.widget); GtkWidget* widget = app.*(surface.widget);
graphene_rect_t r = GRAPHENE_RECT_INIT(0.0f, 0.0f, 0.0f, 0.0f); graphene_rect_t r = GRAPHENE_RECT_INIT(0.0f, 0.0f, 0.0f, 0.0f);
int x = -1, y = -1, width = 0, height = 0; int x = -1, y = -1, width = 0, height = 0;
// `is_visible()`, not `get_visible()`: a widget under a hidden parent -- the trigger summary // `is_visible()` and `get_mapped()`, not `get_visible()`: a widget under a hidden parent -- the
// while the pane is in configuration mode, the Measure page while the Signals tab is showing -- // trigger summary while the pane is in configuration mode -- keeps its own flag true and reads
// keeps its own flag true and reads 0x0, which a crop box would take for a real rectangle. // 0x0, and so does everything on the slide-over while the panel is closed (a revealer unmaps its
if (widget != nullptr && gtk_widget_is_visible(widget) && // child without touching its visible flag); a crop box would take either for a real rectangle.
if (widget != nullptr && gtk_widget_is_visible(widget) && gtk_widget_get_mapped(widget) &&
gtk_widget_compute_bounds(widget, app.window, &r)) { gtk_widget_compute_bounds(widget, app.window, &r)) {
x = static_cast<int>(std::lround(r.origin.x)); x = static_cast<int>(std::lround(r.origin.x));
y = static_cast<int>(std::lround(r.origin.y)); y = static_cast<int>(std::lround(r.origin.y));