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.
This commit is contained in:
1 parent
395c9caec3
commit
829176248a
14 files changed
+901
-293
No files matched your search
@@ -193,13 +193,16 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
|||||||
and this dialog binds none, so a key printed here would be documentation that does nothing.
|
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
|
*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
|
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
|
reads when it starts a moment later, and **re-syncs this dialog's own "(i)" rows** through
|
||||||
app's own window — so it sets **`Config::tour_pending`**, the one value `main()` owns and hands to
|
`chooser_help_sync()`; *Guided tour* starts a tour **of this window** -- the chooser's own walk --
|
||||||
|
and that walk's last stop 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
|
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
|
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
|
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
|
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.
|
window's half instead of conflating a user's request with a first run. The chooser's half has its own
|
||||||
|
entry points in `source_dialog.cpp` (`chooser_tour_begin`, `chooser_tour_autostart`), and *that* walk
|
||||||
|
is what a first run meets first -- see the tour bullets in the help-affordance section below.
|
||||||
- **A `GtkPaned` hands its start child at least its minimum unless the position says otherwise.**
|
- **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
|
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
|
position left alone, `shrink-start-child` or not, the pane's minimum is reserved as an empty column
|
||||||
@@ -537,27 +540,69 @@ The `Help` submenu, the contextual help's "?" and the guided tour, and the traps
|
|||||||
stack, the visible child is the view, and `gtk_popover_present()` re-measures a popover that is
|
stack, the visible child is the view, and `gtk_popover_present()` re-measures a popover that is
|
||||||
already up. `UDPSCOPE_PROBE_MENUS_MS` walks the *visible page*, which is the one line the stack cost
|
already up. `UDPSCOPE_PROBE_MENUS_MS` walks the *visible page*, which is the one line the stack cost
|
||||||
the probe.
|
the probe.
|
||||||
- **A "?" joins its surface's heading row as a sibling, not as an overlay child.** An overlay child
|
- **A "(i)" joins its surface's row as a sibling, not as an overlay child.** An overlay child
|
||||||
sits *on* whatever is under it -- and every panel heading already has a switch or an [edit] button in
|
sits *on* whatever is under it -- and every panel heading already has a switch or a pencil in a
|
||||||
a corner. `src/ui_context_help.cpp` is a table of (id, title, body, shortcut, host) where the host is
|
corner. `src/ui_context_help.cpp` is a table of (id, title, body, shortcut, host) where the host is
|
||||||
the *heading box*, so a new surface is a row and nothing else. The one exception is the plot, which
|
the *row* -- a panel's heading usually, and the row of a specific control (the "record events"
|
||||||
has no heading: its "?" is an overlay child of the overlay the layout editor already uses, and its
|
switch, the pre-trigger share) where that is the thing being explained. The one exception is the
|
||||||
corner is free. The icon is `dialog-question-symbolic` -- `help-about-symbolic` is *already* the
|
plot, which has no heading: its "(i)" is an overlay child of the overlay the layout editor already
|
||||||
status bar's information button, and two "help" glyphs meaning different things on the same line is
|
uses, and its corner is free. The icon is **`dialog-information-symbolic`** -- `help-about-symbolic`
|
||||||
the trap the seven app icons exist to avoid; the dump reads the name back through the theme for the
|
is *already* the status bar's information button, and two "help" glyphs meaning different things on
|
||||||
same reason the pane [edit] buttons do.
|
the same line is the trap the seven app icons exist to avoid; the dump reads the name back through
|
||||||
|
the theme for the same reason the pane [edit] buttons do. `math_head` exists only while the math
|
||||||
|
editor is open, so that row is skipped when it is null.
|
||||||
|
- **A `GtkMenuButton` ignores both `min-width` and a size request; its subnodes are where a small
|
||||||
|
one is made.** The "(i)" is meant to be a *mark*, not a button, and the default is 34x34 (a 24 px
|
||||||
|
floor plus 5 px of padding from Adwaita's `button` rule). Measured on libadwaita 1.10: setting
|
||||||
|
`min-width`/`min-height` on the widget changed nothing, and neither did `gtk_widget_set_size_request()`
|
||||||
|
-- the menu button's own measure wins. Three selectors are needed, all in `install_css()`:
|
||||||
|
`menubutton.udpscope-hint` (the outer node), `menubutton.udpscope-hint > button` (its subnode) and
|
||||||
|
`... > button > image` (the glyph), with `min-width: 0; min-height: 0; padding: 0` -- and the result
|
||||||
|
is **16x16**, the glyph's own size, because a `min-width` is a floor and not a cap. (A class on the
|
||||||
|
outer node alone leaves the subnode at 34; a class on the subnode alone shrank the width to 16 and
|
||||||
|
left the height at 26.) The dump's `hint=` reading is that number, and it is measured with
|
||||||
|
`gtk_widget_compute_bounds()` on the first *visible* hint -- a hint inside a collapsed `GtkExpander`
|
||||||
|
is not in the widget-parent chain at all and reads 0x0, which would look like a mark shrunk to
|
||||||
|
nothing.
|
||||||
- **Off is gone, not greyed, and the buttons are found by a data key rather than kept in a list.** The
|
- **Off is gone, not greyed, and the buttons are found by a data key rather than kept in a list.** The
|
||||||
list is what the math editor cannot survive: its heading lives in a builder that is dropped and
|
list is what the math editor cannot survive: its heading lives in a builder that is dropped and
|
||||||
rebuilt on every opening, so a stored pointer into it would go stale one dialog later. A key on the
|
rebuilt on every opening, so a stored pointer into it would go stale one dialog later. A key on the
|
||||||
button (`udpscope-help-hint`, carrying the surface's id) is both the mark that says "this is a hint"
|
button (`udpscope-help-hint`, carrying the surface's id) is both the mark that says "this is a hint"
|
||||||
and the name the probe and the dump report it by, and removing a hint is `gtk_widget_unparent()`,
|
and the name the probe and the dump report it by, and removing a hint is `gtk_widget_unparent()`,
|
||||||
which means the same thing for a box and for the plot's overlay.
|
which means the same thing for a box and for the plot's overlay. The button itself is
|
||||||
|
`ui_impl::hint_button()` reached either by the table's `context_help_sync()` or by
|
||||||
|
`context_help_button()` for a window with no `App` (the source chooser) -- one button, two callers,
|
||||||
|
so the two windows cannot drift into two affordances.
|
||||||
- **The guided tour takes no grab, and that is the whole reason it is an overlay.** Four dim
|
- **The guided tour takes no grab, and that is the whole reason it is an overlay.** Four dim
|
||||||
rectangles around the hole are ordinary widgets that swallow the clicks that land on them and
|
rectangles around the hole are ordinary widgets that swallow the clicks that land on them and
|
||||||
nothing else; `Skip`, `Finish` and `Escape` hide them, and there is no popover, no gesture and no
|
nothing else; `Skip`, `Finish` and `Escape` hide them, and there is no popover, no gesture and no
|
||||||
grab to leak. The probe asserts it as a reading -- `controllers=` against the count taken before the
|
grab to leak. The probe asserts it as a reading -- `controllers=` against the count taken before the
|
||||||
tour opened -- rather than as a claim, because "no lingering grab" is exactly the sort of thing a
|
tour opened -- rather than as a claim, because "no lingering grab" is exactly the sort of thing a
|
||||||
later change can add back without noticing.
|
later change can add back without noticing.
|
||||||
|
- **The tour is one implementation over two windows.** `ui_impl::TourView` holds the overlay children
|
||||||
|
and the walk's place in it (`tour_build`/`tour_begin`/`tour_advance`/`tour_end`/`tour_update`), and
|
||||||
|
what differs between the windows is only the *stops*: a `TourStop` names a widget the owner resolves,
|
||||||
|
so each window keeps its own table -- the main window's against `App` fields,
|
||||||
|
`source_dialog.cpp`'s `kChooserStops` against the `Chooser`'s. A walk that passes its last stop calls
|
||||||
|
the view's `on_done`; the window's passes null, the chooser's sets `Config::tour_pending`, which is
|
||||||
|
how the two halves are one tour. `App` carries its own `TourView` (`app.tour`), so the machinery is
|
||||||
|
compiled once and the probe reads either window's state by the same fields.
|
||||||
|
- **The chooser's tour needs an overlay over its *whole* window, and `AdwWindow`'s content is
|
||||||
|
replaced, not wrapped.** Two of the chooser's stops name header-bar buttons, and an overlay inside
|
||||||
|
the `AdwToolbarView`'s `content` would give them negative coordinates and clamp the hole to the top
|
||||||
|
edge. So the overlay *becomes* the window's content and adopts the toolbar view -- and the order
|
||||||
|
matters: `gtk_overlay_set_child()` refuses a child that still has a parent, so
|
||||||
|
`adw_window_set_content(win, overlay)` goes **first** (that call is what unparents the toolbar view)
|
||||||
|
and `gtk_overlay_set_child(overlay, content)` second. The builder's object list still owns the view,
|
||||||
|
so nothing is finalized in between. Getting the order wrong is a `Gtk-CRITICAL` and an empty
|
||||||
|
chooser, which is exactly what the first version of this did.
|
||||||
|
- **The chooser's hints are its own table because it runs before `App`.** An `AdwEntryRow` is not a
|
||||||
|
box and its editor is internal, so a hint cannot join the host/port fields as a sibling -- appending
|
||||||
|
it to the `AdwPreferencesGroup` would make it a row under the field rather than a mark beside it, so
|
||||||
|
no hint is invented for them and the group's own `description` carries that text. The two rows the
|
||||||
|
dialog *does* have -- the heading above the saved-source list and the waveforms page's action row --
|
||||||
|
carry `chooser_sources` and `chooser_waveform`, added with the same `context_help_button()` the
|
||||||
|
window uses and re-synced by its Help menu's toggle.
|
||||||
- **The hole is kept from the last frame that could measure the target.** `gtk_widget_compute_bounds()`
|
- **The hole is kept from the last frame that could measure the target.** `gtk_widget_compute_bounds()`
|
||||||
answers nothing on a frame that lands between two layout passes, and letting the hole empty there
|
answers nothing on a frame that lands between two layout passes, and letting the hole empty there
|
||||||
made the spotlight and the card jump to the corner and back; a *target that is not visible* is the
|
made the spotlight and the card jump to the corner and back; a *target that is not visible* is the
|
||||||
@@ -567,8 +612,11 @@ The `Help` submenu, the contextual help's "?" and the guided tour, and the traps
|
|||||||
- **The first-run tour is a `settings.toml` key (`tour_seen`), written when the tour *starts*.** Not
|
- **The first-run tour is a `settings.toml` key (`tour_seen`), written when the tour *starts*.** Not
|
||||||
"the settings file did not exist": a user who has already changed the theme has a file and has still
|
"the settings file did not exist": a user who has already changed the theme has a file and has still
|
||||||
never seen the tour. Written on the way *in*, because a user who skips it in the first second has
|
never seen the tour. Written on the way *in*, because a user who skips it in the first second has
|
||||||
still seen it. The auto-start is a 1.5 s timer from `on_activate()`, so the window is up and laid out
|
still seen it. It starts **in the source chooser** when one is shown (the chooser is the surface a
|
||||||
before anything points at it.
|
first run meets first, and the window's own auto-start is unreachable until the chooser has returned
|
||||||
|
with a source) and in the window otherwise; either way it is a 1.5 s timer, so the window is up and
|
||||||
|
laid out before anything points at it. The chooser's walk writes the key itself and hands the second
|
||||||
|
half over by `Config::tour_pending`, so a first run is *one* tour that crosses the two windows.
|
||||||
- **The `help figures:` line is captured on the tick, like every other size the dump prints.** It names
|
- **The `help figures:` line is captured on the tick, like every other size the dump prints.** It names
|
||||||
ten surfaces' window-coordinate rectangles for the help's annotated figures, and the dump runs after
|
ten surfaces' window-coordinate rectangles for the help's annotated figures, and the dump runs after
|
||||||
the window is destroyed -- a `gtk_widget_compute_bounds()` there is the "no `gtk_widget_*` call in the
|
the window is destroyed -- a `gtk_widget_compute_bounds()` there is the "no `gtk_widget_*` call in the
|
||||||
|
|||||||
@@ -395,21 +395,32 @@ 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
|
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
|
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.
|
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 requested=0` (in the exit
|
- `help layer: contextual=on hints=11 ids={events plot status cursors measures trigger trigger_advanced events_record signals signals_config window} icon=dialog-information-symbolic(ok) hint=16x16; 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
|
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
|
the number of ids**, and the ids are the surfaces the contextual help is on: ten 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
|
||||||
on every opening, so it is not one of the window's own). The layer is **off by default**
|
on every opening, so it is not one of the window's own). Three of the ten are not a pane's *heading*
|
||||||
|
but the row of a specific control — `events_record` (the "record events" switch in the trigger
|
||||||
|
panel), `trigger_advanced` (the pre-trigger share row) and `signals_config` (the signals pane's hint
|
||||||
|
line, whose box exists so a hint can join it) — which is the rule the layer follows: a hint is a
|
||||||
|
sibling of the thing it explains and never an overlay on it. The layer is **off by default**
|
||||||
(`contextual_help` in `settings.toml` is false for a missing file), so a fresh-config run reads
|
(`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 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
|
`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
|
*gone*, not greyed**. **`icon=` must read `(ok)`**: an "(i)" whose name Adwaita lacks is a
|
||||||
*blank* control that still works, the failure the pane [edit] buttons already paid for once. `tour=`
|
*blank* control that still works, the failure the pane [edit] buttons already paid for once. The name
|
||||||
is `running`/`idle` with the plan's `steps=` and the current `step=`; `auto=1` is the first-run
|
is `dialog-information-symbolic`, the plain information mark; `help-about-symbolic` is the status
|
||||||
auto-start having fired *in this process* — the flag is `tour_seen` in `settings.toml`, so a second run
|
bar's ⓘ and a second meaning for it is what the app's own icons exist to avoid. `hint=WxH` is the
|
||||||
against the same config directory reads `auto=0` — and `requested=1` is the **chooser's** "Guided tour"
|
size the **first visible** hint was allocated, measured the way every other rectangle here is
|
||||||
(`Config::tour_pending`) having been carried into this run instead, which is `auto=0` because it is not
|
(`gtk_widget_compute_bounds()`), and **16x16 is the reading a "much tinier" mark must keep**: the
|
||||||
the auto-start.
|
class zeroes the outer `menubutton` node, its `button` subnode and the glyph, which is what a
|
||||||
|
`GtkMenuButton` needs (its measure ignores both `min-width` on the widget and a size request) — the
|
||||||
|
default 34x34 is the number to compare against. Hints inside a collapsed expander are skipped, so the
|
||||||
|
number is never a hidden widget's 0x0. `tour=` 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` —
|
||||||
|
and `requested=1` is the **chooser's** tour having handed over (`Config::tour_pending`), which is
|
||||||
|
`auto=0` because the request came from the other window rather than from this process's first run.
|
||||||
- `gtk warnings: N` (in the exit dump, beside `help layer:`) — **GTK's own log at warning or above,
|
- `gtk warnings: N` (in the exit dump, beside `help layer:`) — **GTK's own log at warning or above,
|
||||||
counted for the whole run, and it must read `0`.** It is counted through a log *writer* function
|
counted for the whole run, and it must read `0`.** It is counted through a log *writer* function
|
||||||
because GTK is built with structured logging: its messages go through the writer and never through
|
because GTK is built with structured logging: its messages go through the writer and never through
|
||||||
@@ -472,6 +483,50 @@ Two lines of the self-check output are assertions in disguise:
|
|||||||
connect_sensitive=1` then a `picked` line, with no SIGSEGV, which is as much a crash test of a
|
connect_sensitive=1` then a `picked` line, with no SIGSEGV, which is as much a crash test of a
|
||||||
dialog built and destroyed as it is a state check. An empty list must print `picked nothing:
|
dialog built and destroyed as it is a state check. An empty list must print `picked nothing:
|
||||||
the list is empty` rather than dereferencing a null row.
|
the list is empty` rather than dereferencing a null row.
|
||||||
|
|
||||||
|
**The command that reaches the chooser at all is `UDPSCOPE_PROBE_PICK_MS`, and this is worth
|
||||||
|
saying because three earlier attempts found no chooser and no output:** with no `--server` and no
|
||||||
|
probe, the chooser runs its own nested `GMainLoop` and *blocks* — there is nothing to time it out
|
||||||
|
and no dump runs, so a bare `./build/udpscope` (bare or under a pty) sits there silently. The
|
||||||
|
working form, against the mock:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=6000 UDPSCOPE_PROBE_PICK_MS=900 \
|
||||||
|
./build/udpscope
|
||||||
|
```
|
||||||
|
|
||||||
|
(with `XDG_CONFIG_HOME` pointing at a directory holding at least one saved source, or the pick
|
||||||
|
finds an empty list and the run prints `picked nothing`). It is the same command the docs give
|
||||||
|
elsewhere with `--server` removed and `UDPSCOPE_PROBE_PICK_MS` added.
|
||||||
|
- `[sources] chooser ui: … hints=[N {ids}] hint=WxH` (stderr, `UDPSCOPE_PROBE_PICK_MS`) — the
|
||||||
|
chooser's frame *and* its help layer in one line. `hints=[2 {chooser_sources chooser_waveform}]`
|
||||||
|
is the layer **on** (`contextual_help` in the file the run reads) and `hints=[0 {}] hint=0x0` is
|
||||||
|
it **off**, which is the same "off is gone" rule the window's line reads, in the window that runs
|
||||||
|
before `App` exists. The two ids are the surfaces this dialog has a row for — the heading above
|
||||||
|
the saved-source list and the waveforms page's action row; the form's `AdwEntryRow`s are *not*
|
||||||
|
hosts (a row is not a box and its editor is internal, so a hint cannot join it as a sibling) and
|
||||||
|
the connection fields are explained by their group's `description` instead. `hint=WxH` is the
|
||||||
|
same 16x16 the window's line reads: the button is `ui_impl::context_help_button()`, the one
|
||||||
|
`udpscope-hint` affordance both windows draw.
|
||||||
|
- `[sources] tour: first run -- auto_started=1 in the chooser` then
|
||||||
|
`[sources] chooser tour: visible=1 steps=5 step=0` (stderr) — **a first run's tour starts in the
|
||||||
|
chooser**, on the chooser's own widgets, from the same `settings.tour_seen` the window's
|
||||||
|
auto-start reads and with the same 1.5 s delay. `steps=5` is the chooser's plan (the source list,
|
||||||
|
the two tabs, New host, the help menu and the preferences). Driven by the walk itself:
|
||||||
|
|
||||||
|
- `UDPSCOPE_PROBE_CHOOSER_TOUR=1` emits `::clicked` on the help menu's **Guided tour** row, which
|
||||||
|
now starts *this* walk rather than only requesting the window's: `[sources] chooser help: tour
|
||||||
|
started in the chooser`, then `[sources] chooser tour: visible=1 steps=5 step=0`, then a walking
|
||||||
|
`[sources] chooser tour: next -> step=N/5` per press, then `[sources] chooser tour: finished 5
|
||||||
|
stops -- the window continues it (tour_pending=1)`. **`tour_pending=1` and `visible=0` is the
|
||||||
|
handoff assertion** — the walk is over here and the request rides on the one `Config` value
|
||||||
|
`main()` hands to both windows.
|
||||||
|
- `UDPSCOPE_PROBE_CHOOSER_TOUR_MS=<ms>` schedules the same walk on a timer, for reading a tour
|
||||||
|
**the run itself started** (the first-run auto-start) rather than one the probe started; it must
|
||||||
|
be later than 1500 ms. Paired with `UDPSCOPE_PROBE_PICK_MS` a few hundred ms later, the run then
|
||||||
|
shows the other half: the application's `help layer:` reads `tour=running … auto=0 requested=1`,
|
||||||
|
the window's tour having been started by `tour_start_requested()` rather than by the first-run
|
||||||
|
timer.
|
||||||
- **The session layout round trip** (`~/.config/udpscope/<host>_<port>.toml`) — run once
|
- **The session layout round trip** (`~/.config/udpscope/<host>_<port>.toml`) — run once
|
||||||
with a drop and a window change, then again with no interaction: the second run's dump
|
with a drop and a window change, then again with no interaction: the second run's dump
|
||||||
must read `layout loaded from ...` and `layout: rows=R window_s=W row0=[...]` with the
|
must read `layout loaded from ...` and `layout: rows=R window_s=W row0=[...]` with the
|
||||||
@@ -685,12 +740,16 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
|
|||||||
touches anything: `auto_started=1 visible=1` is the tour that started itself, and the parenthetical is
|
touches anything: `auto_started=1 visible=1` is the tour that started itself, and the parenthetical is
|
||||||
`settings.tour_seen` as the process found it — `tour_seen already set` with `auto=0` is the *second*
|
`settings.tour_seen` as the process found it — `tour_seen already set` with `auto=0` is the *second*
|
||||||
run against the same config directory, which is the assertion that the flag persisted. Then:
|
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}` —
|
- `[helpui] contextual on: hints=11 ids={events plot status cursors measures trigger trigger_advanced events_record signals signals_config window} hint=16x16` —
|
||||||
the eight "?" affordances, by id. The layer's ninth surface, `math`, is not here: its host exists
|
the eleven "(i)" affordances, by id, and the size the first visible one was allocated. Ten are the
|
||||||
only while the math editor window is open. **On a fresh config this reads `hints=0 ids={}`**, because
|
window's own surfaces (three of them a specific control's row rather than a heading — see the
|
||||||
the layer is off by default; the example is the state after the row below was switched on, and a
|
`help layer:` entry) and the eleventh is `math`, which is present only while the math editor
|
||||||
fresh run's `hints=0` beside a `settings.toml` with no `contextual_help` key is the default being
|
window is open; it reads 10 with the editor shut. **On a fresh config this reads `hints=0 ids={}`
|
||||||
read rather than assumed.
|
and `hint=0x0`**, 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. **`hint=16x16` is the "much tinier" assertion**
|
||||||
|
— the default menu-button mark is 34x34 (see `help layer:`), and anything near that is the class
|
||||||
|
not reaching the outer `menubutton` node.
|
||||||
- `[helpui] main view: rows=7 help_row=1` and one `[helpui] entry N '<label>' accel='<key>' order=O`
|
- `[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
|
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'`,
|
promises: `entry 1 'Keyboard shortcuts' accel='Ctrl+/'`, `entry 2 'Documentation' accel='F1'`,
|
||||||
@@ -701,10 +760,10 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
|
|||||||
`toggle=` being the contextual-help row's check, which *is* `settings.contextual_help`. It reads `0`
|
`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
|
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.
|
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
|
- `[helpui] toggle: off -> hints=0 ids={}; on -> hints=11 ids={…}; setting=on` — the toggle's effect
|
||||||
read both ways through the row itself. **`hints=0` with `off` is the assertion**: off means the
|
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
|
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 eight, which is the other half — a toggle that
|
removing them. The count comes back to the same eleven, 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=5 step=0` — the tour started the way
|
||||||
`Ctrl+T` starts it. `steps=5` is the plan: the table's five steps whose target was on screen
|
`Ctrl+T` starts it. `steps=5` is the plan: the table's five steps whose target was on screen
|
||||||
|
|||||||
@@ -309,16 +309,19 @@ as distinct from the fault injection below, which exercises ingest:
|
|||||||
- `UDPSCOPE_PROBE_HELPUI_MS` — the help affordance layer, end to end, in ten steps 300–400 ms apart:
|
- `UDPSCOPE_PROBE_HELPUI_MS` — the help affordance layer, end to end, in ten steps 300–400 ms apart:
|
||||||
the **first-run auto-start** (run it once with a throwaway `XDG_CONFIG_HOME` to see `auto=1`, and
|
the **first-run auto-start** (run it once with a throwaway `XDG_CONFIG_HOME` to see `auto=1`, and
|
||||||
again against the same one to see it not repeat), the menu's seven rows and the **four entries of
|
again against the same one to see it not repeat), the menu's seven rows and the **four entries of
|
||||||
the Help submenu**, the **contextual-help toggle** with the count of "?" affordances on either side
|
the Help submenu**, the **contextual-help toggle** with the count of "(i)" affordances on either side
|
||||||
of it, the tour **started from the probe** and stepped forwards, back and dismissed, and `help
|
of it, the tour **started from the probe** and stepped forwards, back and dismissed, and `help
|
||||||
figures:` read in both of the trigger pane's modes. Its lines and what each asserts are in
|
figures:` read in both of the trigger pane's modes. Its lines and what each asserts are in
|
||||||
[probe-readings.md](probe-readings.md); the two things to read first are **`hints=0`** while the
|
[probe-readings.md](probe-readings.md); the two things to read first are **`hints=0`** while the
|
||||||
toggle is off (off is *gone*, not greyed) and **`controllers=12 (was 12)`** after the dismissal (no
|
toggle is off (off is *gone*, not greyed) and **`controllers=12 (was 12)`** after the dismissal (no
|
||||||
lingering input grab — a tour of ordinary overlay widgets takes none). Give it
|
lingering input grab — a tour of ordinary overlay widgets takes none), and **`hint=16x16`** is the
|
||||||
|
size the shrunk "(i)" must keep (the default menu-button mark is 34x34). The chooser's own tour is
|
||||||
|
the other half and has its own entry points — see the **chooser** bullet below. Give it
|
||||||
`UDPSCOPE_EXIT_AFTER_MS=9500 UDPSCOPE_EXIT_DUMP=1` to get the `help layer:`/`help figures:` dump
|
`UDPSCOPE_EXIT_AFTER_MS=9500 UDPSCOPE_EXIT_DUMP=1` to get the `help layer:`/`help figures:` dump
|
||||||
lines as well, and note the tour **auto-starts** in any run whose settings file does not carry
|
lines as well, and note the tour **auto-starts** in any run whose settings file does not carry
|
||||||
`tour_seen`, so a probe run against a fresh config directory will have it on screen for the first
|
`tour_seen` — **in the source chooser when there is one**, in the window otherwise (see the chooser
|
||||||
second and a half — and that start *writes* `tour_seen` to whichever config directory the run is
|
bullet) — so a probe run against a fresh config directory will have it on screen for the first
|
||||||
|
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_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
|
||||||
trigger panel: `[defaults] calibrate: 1 0 model=1 0 | trigger: 0.001 0.002 0 0.001 0 50 0.01`. A
|
trigger panel: `[defaults] calibrate: 1 0 model=1 0 | trigger: 0.001 0.002 0 0.001 0 50 0.01`. A
|
||||||
@@ -331,7 +334,17 @@ as distinct from the fault injection below, which exercises ingest:
|
|||||||
entry per configuration (`host:port/name … buffer= relative= note='…'`), and
|
entry per configuration (`host:port/name … buffer= relative= note='…'`), and
|
||||||
`UDPSCOPE_PROBE_PICK_MS=800` opens the chooser, picks the first row and prints
|
`UDPSCOPE_PROBE_PICK_MS=800` opens the chooser, picks the first row and prints
|
||||||
`[sources] picked host:port … buffer=64 relative=0 capture=0 config=… note='…'` — the settings the
|
`[sources] picked host:port … buffer=64 relative=0 capture=0 config=… note='…'` — the settings the
|
||||||
configuration carried, into the `Config` the model is built from. A legacy file
|
configuration carried, into the `Config` the model is built from. **With no `--server` and no probe
|
||||||
|
the chooser blocks in its own nested loop** (no frame to end and no dump), so a chooser run is
|
||||||
|
always this probe. It also prints the dialog's frame *and its own help layer*:
|
||||||
|
`[sources] chooser ui: … hints=[2 {chooser_sources chooser_waveform}] hint=16x16` with the setting
|
||||||
|
on, `hints=[0 {}] hint=0x0` with it off. `UDPSCOPE_PROBE_CHOOSER_HELP=1` drives the **Enable
|
||||||
|
contextual help** row and reads the file back; `UDPSCOPE_PROBE_CHOOSER_TOUR=1` drives the **Guided
|
||||||
|
tour** row and walks the chooser's own tour to its last stop, printing `[sources] chooser tour: …`
|
||||||
|
and the handoff (`tour_pending=1`); `UDPSCOPE_PROBE_CHOOSER_TOUR_MS=<ms>` schedules the same walk,
|
||||||
|
which is how the **first-run** auto-start is read (schedule it after 1500 ms, and pair it with
|
||||||
|
`UDPSCOPE_PROBE_PICK_MS` a little later so the application's half follows). Its lines and what each
|
||||||
|
asserts are in [probe-readings.md](probe-readings.md). A legacy file
|
||||||
(`<host>_<port>.toml`) produces `layout loaded from …/<host>_<port>/default.toml`, an `active`
|
(`<host>_<port>.toml`) produces `layout loaded from …/<host>_<port>/default.toml`, an `active`
|
||||||
holding `default`, and the old file renamed `.imported`.
|
holding `default`, and the old file renamed `.imported`.
|
||||||
- **The loading window covers the gap, and only on the path that asked.** The same
|
- **The loading window covers the gap, and only on the path that asked.** The same
|
||||||
|
|||||||
+241
-16
@@ -141,11 +141,40 @@ struct Chooser {
|
|||||||
GtkWidget* help_tour = nullptr;
|
GtkWidget* help_tour = nullptr;
|
||||||
GtkWidget* help_toggle = nullptr;
|
GtkWidget* help_toggle = nullptr;
|
||||||
GtkWidget* help_popover = nullptr;
|
GtkWidget* help_popover = nullptr;
|
||||||
|
/// The contextual help's "(i)" affordances, one per surface *this* window explains. They are not
|
||||||
|
/// the window's table -- `ui_context_help.cpp` names `App` fields, and there is no `App` here yet
|
||||||
|
/// -- so this window holds its own two rows and joins them with `context_help_button()`, which
|
||||||
|
/// draws the *one* "(i)" button both windows use.
|
||||||
|
GtkWidget* sources_head = nullptr;
|
||||||
|
GtkWidget* waves_actions = nullptr;
|
||||||
|
/// The guided tour of *this* window. It walks the chooser's own widgets and, when the walk
|
||||||
|
/// finishes, hands it on to the application through `Config::tour_pending`. The machinery is the
|
||||||
|
/// shared `ui_impl::TourView`; only the stops are this window's, because they name its widgets.
|
||||||
|
ui_impl::TourView tour;
|
||||||
|
/// The timer that auto-starts this tour on a first run, kept so a chooser that closes first can
|
||||||
|
/// cancel it (`&c` is a stack frame, and a timer that outlives it is the defect this project has
|
||||||
|
/// already paid for once).
|
||||||
|
guint tour_source = 0;
|
||||||
|
/// `UDPSCOPE_PROBE_CHOOSER_TOUR_MS`'s own timer, cancelled the same way.
|
||||||
|
guint tour_probe_source = 0;
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
void chooser_finish(Chooser& c, bool chosen) {
|
void chooser_finish(Chooser& c, bool chosen) {
|
||||||
c.chosen = chosen;
|
c.chosen = chosen;
|
||||||
|
// The two timers point into this stack frame (`&c`), so they go before the frame does. Both are
|
||||||
|
// cleared first: `g_source_remove()` on an id whose source already returned is a GLib critical.
|
||||||
|
if (c.tour_source != 0) {
|
||||||
|
g_source_remove(c.tour_source);
|
||||||
|
c.tour_source = 0;
|
||||||
|
}
|
||||||
|
if (c.tour_probe_source != 0) {
|
||||||
|
g_source_remove(c.tour_probe_source);
|
||||||
|
c.tour_probe_source = 0;
|
||||||
|
}
|
||||||
|
// The tour's own widgets go with the window; ending the walk first means the spotlight and the
|
||||||
|
// card are hidden rather than finalized by the destroy while still visible.
|
||||||
|
ui_impl::tour_end(c.tour);
|
||||||
// Destroy the form first: any path that reaches here while the form is up -- a row click, the
|
// Destroy the form first: any path that reaches here while the form is up -- a row click, the
|
||||||
// Escape key -- would otherwise leave a window whose four handlers are bound to a dead stack
|
// Escape key -- would otherwise leave a window whose four handlers are bound to a dead stack
|
||||||
// frame. `gtk_window_destroy()` rather than `gtk_window_close()`: the pointer is already
|
// frame. `gtk_window_destroy()` rather than `gtk_window_close()`: the pointer is already
|
||||||
@@ -176,6 +205,10 @@ void chooser_populate_waves(Chooser& c);
|
|||||||
void report_pick(const Chooser& c);
|
void report_pick(const Chooser& c);
|
||||||
/// Defined below; the row buttons reach it, and the row builder sits above it.
|
/// Defined below; the row buttons reach it, and the row builder sits above it.
|
||||||
void open_form(Chooser& c, int edit_index);
|
void open_form(Chooser& c, int edit_index);
|
||||||
|
/// The chooser's own help surfaces and its own tour, defined below: the help menu's rows and the
|
||||||
|
/// first-run timer call them, and both need the window's widgets resolved first.
|
||||||
|
void chooser_help_sync(Chooser& c);
|
||||||
|
void chooser_tour_begin(Chooser& c);
|
||||||
|
|
||||||
void on_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) {
|
void on_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user) {
|
||||||
Chooser& c = *static_cast<Chooser*>(user);
|
Chooser& c = *static_cast<Chooser*>(user);
|
||||||
@@ -644,16 +677,18 @@ void build_chooser_help(Chooser& c) {
|
|||||||
|
|
||||||
c.help_tour = ui_impl::menu_button("Guided tour", nullptr);
|
c.help_tour = ui_impl::menu_button("Guided tour", nullptr);
|
||||||
gtk_widget_set_tooltip_text(c.help_tour,
|
gtk_widget_set_tooltip_text(c.help_tour,
|
||||||
"walk over the plot, the side panel, the trigger, the signals and "
|
"a walk over this dialog's own widgets, then over the plot, the side "
|
||||||
"the status line, once the window is up");
|
"panel, the trigger, the signals and the status line once a source "
|
||||||
|
"has been opened");
|
||||||
g_signal_connect(c.help_tour, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
g_signal_connect(c.help_tour, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||||
Chooser& ch = *static_cast<Chooser*>(user);
|
Chooser& ch = *static_cast<Chooser*>(user);
|
||||||
gtk_popover_popdown(GTK_POPOVER(ch.help_popover));
|
gtk_popover_popdown(GTK_POPOVER(ch.help_popover));
|
||||||
// The tour lives on the window the app builds, which does not exist yet, so the
|
// The tour *starts here*, on this window: the chooser is what a first run meets
|
||||||
// request rides on the `Config` this call was handed and the app starts it once
|
// first and what it has no way of knowing about. When this walk finishes it sets
|
||||||
// the window is up -- see `Config::tour_pending`.
|
// `Config::tour_pending`, so the application continues it once a source has been
|
||||||
ch.cfg->tour_pending = true;
|
// picked -- see `chooser_tour_begin()` and `chooser_tour_done()`.
|
||||||
std::fprintf(stderr, "[sources] chooser help: tour requested\n");
|
chooser_tour_begin(ch);
|
||||||
|
std::fprintf(stderr, "[sources] chooser help: tour started in the chooser\n");
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
}),
|
}),
|
||||||
&c);
|
&c);
|
||||||
@@ -661,14 +696,14 @@ void build_chooser_help(Chooser& c) {
|
|||||||
|
|
||||||
// The check *is* the setting, exactly as in the window's menu: the row is stated before the handler
|
// 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
|
// 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
|
// built. The chooser re-syncs its own "(i)" layer here, the way the window re-syncs the window's.
|
||||||
// to leave the setting where the app will read it.
|
|
||||||
c.help_toggle = ui_impl::menu_toggle("Enable contextual help", nullptr,
|
c.help_toggle = ui_impl::menu_toggle("Enable contextual help", nullptr,
|
||||||
load_settings().contextual_help);
|
load_settings().contextual_help);
|
||||||
gtk_widget_set_tooltip_text(c.help_toggle,
|
gtk_widget_set_tooltip_text(c.help_toggle,
|
||||||
"a small \u201c?\u201d beside each surface of the main window, saying "
|
"a small \u201c(i)\u201d beside each surface of this dialog and of the "
|
||||||
"what that surface is for");
|
"main window, saying what that surface is for");
|
||||||
g_signal_connect(c.help_toggle, "toggled", G_CALLBACK(+[](GtkToggleButton* button, gpointer) {
|
g_signal_connect(c.help_toggle, "toggled", G_CALLBACK(+[](GtkToggleButton* button, gpointer user) {
|
||||||
|
Chooser& ch = *static_cast<Chooser*>(user);
|
||||||
Settings s = load_settings();
|
Settings s = load_settings();
|
||||||
s.contextual_help = gtk_toggle_button_get_active(button) != FALSE;
|
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
|
// Written in full, the way the window's toggle writes it: the keys this row does
|
||||||
@@ -678,6 +713,7 @@ void build_chooser_help(Chooser& c) {
|
|||||||
settings_path().c_str());
|
settings_path().c_str());
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
}
|
}
|
||||||
|
chooser_help_sync(ch);
|
||||||
}),
|
}),
|
||||||
&c);
|
&c);
|
||||||
gtk_box_append(GTK_BOX(box), c.help_toggle);
|
gtk_box_append(GTK_BOX(box), c.help_toggle);
|
||||||
@@ -686,6 +722,159 @@ void build_chooser_help(Chooser& c) {
|
|||||||
gtk_menu_button_set_popover(GTK_MENU_BUTTON(c.help_button), c.help_popover);
|
gtk_menu_button_set_popover(GTK_MENU_BUTTON(c.help_button), c.help_popover);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The chooser's contextual help: the same "(i)" the window draws, on the two surfaces *this*
|
||||||
|
/// window has a row for -- the list of saved sources and the waveforms page's action row. It is
|
||||||
|
/// called once after the window is built and again by the Help menu's toggle, which is the one place
|
||||||
|
/// the setting can change while the dialog is up.
|
||||||
|
///
|
||||||
|
/// The form's rows are deliberately not a host: an `AdwEntryRow` is not a box and its editing widget
|
||||||
|
/// is internal, so a hint could not join it as a sibling -- it would have to be appended to the whole
|
||||||
|
/// `AdwPreferencesGroup` as a row of its own, which is not a hint *beside* the field but a row under
|
||||||
|
/// it. The connection fields are therefore explained by the group's own `description`, which is
|
||||||
|
/// where libadwaita puts a hint that must survive being typed over.
|
||||||
|
void chooser_help_sync(Chooser& c) {
|
||||||
|
const bool on = load_settings().contextual_help;
|
||||||
|
ui_impl::context_help_button(
|
||||||
|
c.sources_head, "chooser_sources", "The sources you have opened",
|
||||||
|
"One row per saved configuration. Clicking one opens it: it becomes the active configuration "
|
||||||
|
"and the stream starts. The pencil edits a row's name, note, buffer size and clock; the cross "
|
||||||
|
"forgets it and deletes its saved session.",
|
||||||
|
"", on);
|
||||||
|
ui_impl::context_help_button(
|
||||||
|
c.waves_actions, "chooser_waveform", "Opening a recording",
|
||||||
|
"A `.npy` an earlier run exported is opened for viewing and analysis instead of a live source: "
|
||||||
|
"there is no acquisition behind it, so the trigger and the rate counters are not there -- only "
|
||||||
|
"the samples, which can be measured, searched and re-exported.",
|
||||||
|
"", on);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One stop of the *chooser's* tour. Its targets are this window's widgets, which exist before `App`
|
||||||
|
/// does -- which is the whole reason the stops cannot live in the window's table.
|
||||||
|
struct ChooserStop {
|
||||||
|
GtkWidget* Chooser::* target;
|
||||||
|
const char* title;
|
||||||
|
const char* body;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr ChooserStop kChooserStops[] = {
|
||||||
|
{&Chooser::list, "Choosing a source",
|
||||||
|
"Every source you have saved, newest first. Clicking one opens it -- that is the choice this "
|
||||||
|
"dialog exists to make -- and the arrows on a row edit or forget its configuration."},
|
||||||
|
{&Chooser::switcher, "Two kinds of input",
|
||||||
|
"A *source* is a stream to receive; a *waveform* is a recording an earlier run exported. They "
|
||||||
|
"are the two answers to \u201cwhat should be on the screen\u201d, and these are the tabs "
|
||||||
|
"between them."},
|
||||||
|
{&Chooser::new_host, "A source that is not listed",
|
||||||
|
"Opens the form for an address that has no configuration yet: host, port, the settings a source "
|
||||||
|
"can only be given before it is opened, and the name its session will be saved under."},
|
||||||
|
{&Chooser::help_button, "The manual, and this tour",
|
||||||
|
"Documentation opens the manual in the desktop help viewer. This tour walks this window and "
|
||||||
|
"then the application's own window. Contextual help is the switch below it: on, every surface "
|
||||||
|
"carries a small \u201c(i)\u201d saying what it is for."},
|
||||||
|
{&Chooser::prefs_button, "Preferences",
|
||||||
|
"The event cache, relative time and the interface theme. This is the last moment relative time "
|
||||||
|
"can still change this run -- the buffers are planned as soon as a source is picked -- which is "
|
||||||
|
"why the button is here and not only in the window's menu."},
|
||||||
|
};
|
||||||
|
constexpr int kChooserStopCount = static_cast<int>(sizeof(kChooserStops) / sizeof(kChooserStops[0]));
|
||||||
|
|
||||||
|
/// The walk is over: the chooser hands the tour on to the application. This is the second half of the
|
||||||
|
/// two-window tour -- `tour_pending` is the value `main()` owns and hands to both `choose_source()`
|
||||||
|
/// and `run_ui()`, and the window starts its own tour from it once a source has been picked.
|
||||||
|
void chooser_tour_done(void* user) {
|
||||||
|
Chooser& c = *static_cast<Chooser*>(user);
|
||||||
|
c.cfg->tour_pending = true;
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[sources] chooser tour: finished %d stops -- the window continues it "
|
||||||
|
"(tour_pending=1)\n",
|
||||||
|
kChooserStopCount);
|
||||||
|
std::fflush(stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// "The tour has run" is the same `settings.tour_seen` the window's auto-start reads, written when
|
||||||
|
/// the walk *starts*: a user who skips it has still seen it.
|
||||||
|
void chooser_tour_mark_seen() {
|
||||||
|
Settings s = load_settings();
|
||||||
|
if (s.tour_seen) return;
|
||||||
|
s.tour_seen = true;
|
||||||
|
if (!save_settings(s)) {
|
||||||
|
std::fprintf(stderr, "[sources] tour: could not write %s: it will be offered again\n",
|
||||||
|
settings_path().c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void chooser_tour_begin(Chooser& c) {
|
||||||
|
if (c.tour.card == nullptr) return;
|
||||||
|
chooser_tour_mark_seen();
|
||||||
|
// The stops whose target is on screen, in table order, the same filter the window's tour uses: a
|
||||||
|
// surface that is not there is not walked over.
|
||||||
|
std::vector<ui_impl::TourStop> stops;
|
||||||
|
for (int i = 0; i < kChooserStopCount; ++i) {
|
||||||
|
GtkWidget* target = c.*(kChooserStops[i].target);
|
||||||
|
if (target != nullptr && gtk_widget_is_visible(target)) {
|
||||||
|
stops.push_back({target, kChooserStops[i].title, kChooserStops[i].body});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stops.empty()) return;
|
||||||
|
ui_impl::tour_begin(c.tour, std::move(stops), chooser_tour_done, &c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The tour is drawn over the *whole* window, header bar included: two of its stops name the header's
|
||||||
|
/// own buttons, and an overlay placed inside the content would give them negative coordinates and
|
||||||
|
/// clamp its hole to the top edge. `AdwWindow`'s content takes the overlay and the toolbar view
|
||||||
|
/// becomes its child -- the builder still owns the tree, so there is no reference bookkeeping.
|
||||||
|
void build_chooser_tour(Chooser& c) {
|
||||||
|
GtkWidget* content = adw_window_get_content(ADW_WINDOW(c.window));
|
||||||
|
if (content == nullptr) return;
|
||||||
|
// The overlay has to *become* the window's content before the toolbar view can be adopted by it:
|
||||||
|
// `gtk_overlay_set_child()` refuses a child that still has a parent, so the two calls are in this
|
||||||
|
// order -- handing the window the overlay first is what unparents the toolbar view. The view is
|
||||||
|
// held across the swap rather than left to whatever the builder's object list keeps: it has no id
|
||||||
|
// for the builder to name it by, and the window's content was its only owner.
|
||||||
|
GtkWidget* overlay = gtk_overlay_new();
|
||||||
|
g_object_ref(content);
|
||||||
|
adw_window_set_content(ADW_WINDOW(c.window), overlay);
|
||||||
|
gtk_overlay_set_child(GTK_OVERLAY(overlay), content);
|
||||||
|
g_object_unref(content);
|
||||||
|
ui_impl::tour_build(c.tour, overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A first run is the run whose settings file does not say the tour has been offered -- the same key
|
||||||
|
/// the window's own auto-start reads. Delayed 1.5 s for the reason the window's is: the window has to
|
||||||
|
/// be up and laid out before anything points at it.
|
||||||
|
gboolean chooser_tour_autostart(gpointer user) {
|
||||||
|
Chooser& c = *static_cast<Chooser*>(user);
|
||||||
|
c.tour_source = 0;
|
||||||
|
std::fprintf(stderr, "[sources] tour: first run -- auto_started=1 in the chooser\n");
|
||||||
|
std::fflush(stderr);
|
||||||
|
chooser_tour_begin(c);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `UDPSCOPE_PROBE_CHOOSER_TOUR_MS`: the chooser's tour, read and walked the way the window's is --
|
||||||
|
/// where it stands, then every stop, then the handoff. It reports the walk by *stepping* it (the
|
||||||
|
/// button the user presses), so what it reads is what a first run sees, not the state the probe set.
|
||||||
|
gboolean chooser_tour_probe(gpointer user) {
|
||||||
|
Chooser& c = *static_cast<Chooser*>(user);
|
||||||
|
c.tour_probe_source = 0;
|
||||||
|
std::fprintf(stderr, "[sources] chooser tour: visible=%d steps=%d step=%d\n",
|
||||||
|
c.tour.running ? 1 : 0, static_cast<int>(c.tour.plan.size()), c.tour.index);
|
||||||
|
while (c.tour.running) {
|
||||||
|
const int before = c.tour.index;
|
||||||
|
ui_impl::tour_advance(c.tour, 1);
|
||||||
|
if (c.tour.running) {
|
||||||
|
std::fprintf(stderr, "[sources] chooser tour: next -> step=%d/%d\n", c.tour.index,
|
||||||
|
static_cast<int>(c.tour.plan.size()));
|
||||||
|
} else {
|
||||||
|
std::fprintf(stderr, "[sources] chooser tour: Next past stop %d finished the walk\n", before + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::fprintf(stderr, "[sources] chooser tour: after finish visible=%d tour_pending=%d\n",
|
||||||
|
c.tour.running ? 1 : 0, c.cfg->tour_pending ? 1 : 0);
|
||||||
|
std::fflush(stderr);
|
||||||
|
return G_SOURCE_REMOVE;
|
||||||
|
}
|
||||||
|
|
||||||
/// `UDPSCOPE_PROBE_PICK_MS`: activate the first row, the way a click does.
|
/// `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
|
/// `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
|
/// dialog built, filled and destroyed is as much the point as the values, the same way
|
||||||
@@ -706,9 +895,14 @@ void report_chooser_ui(const Chooser& c) {
|
|||||||
adw_view_stack_page_get_title(page),
|
adw_view_stack_page_get_title(page),
|
||||||
adw_view_stack_page_get_icon_name(page) != nullptr ? "yes" : "NO");
|
adw_view_stack_page_get_icon_name(page) != nullptr ? "yes" : "NO");
|
||||||
}
|
}
|
||||||
|
std::string hint_ids;
|
||||||
|
const int hints = ui_impl::context_help_count_in(c.window, &hint_ids);
|
||||||
|
int hint_w = 0;
|
||||||
|
int hint_h = 0;
|
||||||
|
ui_impl::context_help_size_in(c.window, &hint_w, &hint_h);
|
||||||
std::fprintf(stderr,
|
std::fprintf(stderr,
|
||||||
" visible='%s' switcher_wired=%d buttons=[prefs=%d new_host=%d open=%d cancel=%d] "
|
" 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",
|
"help=[button=%d docs=%d tour=%d toggle=%d on=%d] hints=[%d {%s}] hint=%dx%d\n",
|
||||||
adw_view_stack_get_visible_child_name(ADW_VIEW_STACK(c.page_stack)),
|
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_switcher_bar_get_stack(ADW_VIEW_SWITCHER_BAR(c.switcher)) ==
|
||||||
ADW_VIEW_STACK(c.page_stack)
|
ADW_VIEW_STACK(c.page_stack)
|
||||||
@@ -721,7 +915,8 @@ void report_chooser_ui(const Chooser& c) {
|
|||||||
c.help_toggle != nullptr &&
|
c.help_toggle != nullptr &&
|
||||||
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(c.help_toggle))
|
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(c.help_toggle))
|
||||||
? 1
|
? 1
|
||||||
: 0);
|
: 0,
|
||||||
|
hints, hint_ids.c_str(), hint_w, hint_h);
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -741,9 +936,18 @@ gboolean pick_probe(gpointer user) {
|
|||||||
// would assert nothing. The tour is read back out of the `Config` the run will be handed, which is
|
// 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
|
// 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.
|
// from that file and not from the dialog.
|
||||||
|
// `UDPSCOPE_PROBE_CHOOSER_TOUR=1` drives the help menu's Guided tour row the way a click does --
|
||||||
|
// which now starts a tour *in this dialog*, not merely a request for the window's -- then walks it
|
||||||
|
// to its last stop and past it, so the handoff is exercised, and then falls 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 carrier is `Config::tour_pending`.
|
||||||
if (g_getenv("UDPSCOPE_PROBE_CHOOSER_TOUR") != nullptr && c.help_tour != nullptr) {
|
if (g_getenv("UDPSCOPE_PROBE_CHOOSER_TOUR") != nullptr && c.help_tour != nullptr) {
|
||||||
g_signal_emit_by_name(c.help_tour, "clicked");
|
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::fprintf(stderr, "[sources] chooser tour: visible=%d steps=%d step=%d\n",
|
||||||
|
c.tour.running ? 1 : 0, static_cast<int>(c.tour.plan.size()), c.tour.index);
|
||||||
|
while (c.tour.running) ui_impl::tour_advance(c.tour, 1);
|
||||||
|
std::fprintf(stderr, "[sources] chooser tour: after finish visible=%d tour_pending=%d\n",
|
||||||
|
c.tour.running ? 1 : 0, c.cfg->tour_pending ? 1 : 0);
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
}
|
}
|
||||||
if (g_getenv("UDPSCOPE_PROBE_CHOOSER_HELP") != nullptr && c.help_toggle != nullptr) {
|
if (g_getenv("UDPSCOPE_PROBE_CHOOSER_HELP") != nullptr && c.help_toggle != nullptr) {
|
||||||
@@ -1024,9 +1228,12 @@ bool choose_source(Config& cfg) {
|
|||||||
c.waves_open = GTK_WIDGET(gtk_builder_get_object(c.ui, "waves_open"));
|
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.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"));
|
c.help_button = GTK_WIDGET(gtk_builder_get_object(c.ui, "help_menu_button"));
|
||||||
|
c.sources_head = GTK_WIDGET(gtk_builder_get_object(c.ui, "sources_head"));
|
||||||
|
c.waves_actions = GTK_WIDGET(gtk_builder_get_object(c.ui, "waves_actions"));
|
||||||
if (c.window == nullptr || c.page_stack == nullptr || c.switcher == nullptr || c.list == nullptr ||
|
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.wave_list == nullptr || c.prefs_button == nullptr || c.new_host == nullptr ||
|
||||||
c.waves_open == nullptr || c.cancel_button == nullptr || c.help_button == nullptr) {
|
c.waves_open == nullptr || c.cancel_button == nullptr || c.help_button == nullptr ||
|
||||||
|
c.sources_head == nullptr || c.waves_actions == nullptr) {
|
||||||
std::fprintf(stderr, "[sources] chooser ui: source-chooser.ui is missing an id this code reads\n");
|
std::fprintf(stderr, "[sources] chooser ui: source-chooser.ui is missing an id this code reads\n");
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
return false;
|
return false;
|
||||||
@@ -1041,6 +1248,11 @@ bool choose_source(Config& cfg) {
|
|||||||
ui_impl::install_css();
|
ui_impl::install_css();
|
||||||
build_chooser_help(c);
|
build_chooser_help(c);
|
||||||
|
|
||||||
|
// The chooser's own "(i)" layer and its own tour, both over the widgets the block above resolved.
|
||||||
|
// The tour's overlay wraps the whole window, so it is built before the window is presented.
|
||||||
|
chooser_help_sync(c);
|
||||||
|
build_chooser_tour(c);
|
||||||
|
|
||||||
g_signal_connect(c.list, "row-activated", G_CALLBACK(on_row_activated), &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.wave_list, "row-activated", G_CALLBACK(on_wave_activated), &c);
|
||||||
g_signal_connect(c.waves_open, "clicked", G_CALLBACK(on_wave_open_clicked), &c);
|
g_signal_connect(c.waves_open, "clicked", G_CALLBACK(on_wave_open_clicked), &c);
|
||||||
@@ -1078,6 +1290,19 @@ bool choose_source(Config& cfg) {
|
|||||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_PICK_MS")) {
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_PICK_MS")) {
|
||||||
c.pick_source = g_timeout_add(static_cast<guint>(std::atoi(env)), pick_probe, &c);
|
c.pick_source = g_timeout_add(static_cast<guint>(std::atoi(env)), pick_probe, &c);
|
||||||
}
|
}
|
||||||
|
// A first run gets the tour here, on this window, before a source is picked: the chooser is the
|
||||||
|
// surface a first run meets first, and the window's own auto-start is unreachable until this call
|
||||||
|
// has returned with a source. 1.5 s is the window's own delay and for its reason -- the window has
|
||||||
|
// to be up and laid out before anything points at it -- and the walk's own finish sets
|
||||||
|
// `tour_pending`, which is how it continues into the application's window.
|
||||||
|
if (!load_settings().tour_seen) {
|
||||||
|
c.tour_source = g_timeout_add(1500, chooser_tour_autostart, &c);
|
||||||
|
}
|
||||||
|
// `UDPSCOPE_PROBE_CHOOSER_TOUR_MS`: schedule the tour probe *after* the auto-start above, so the
|
||||||
|
// reading is of a tour the run itself began rather than one the probe started.
|
||||||
|
if (const char* env = g_getenv("UDPSCOPE_PROBE_CHOOSER_TOUR_MS")) {
|
||||||
|
c.tour_probe_source = g_timeout_add(static_cast<guint>(std::atoi(env)), chooser_tour_probe, &c);
|
||||||
|
}
|
||||||
|
|
||||||
c.loop = g_main_loop_new(nullptr, FALSE);
|
c.loop = g_main_loop_new(nullptr, FALSE);
|
||||||
g_main_loop_run(c.loop);
|
g_main_loop_run(c.loop);
|
||||||
|
|||||||
+1
-1
@@ -4926,7 +4926,7 @@ void on_activate(GtkApplication* application, gpointer user) {
|
|||||||
GtkWidget* overlay = gtk_overlay_new();
|
GtkWidget* overlay = gtk_overlay_new();
|
||||||
gtk_window_set_child(GTK_WINDOW(app->window), overlay);
|
gtk_window_set_child(GTK_WINDOW(app->window), overlay);
|
||||||
gtk_overlay_set_child(GTK_OVERLAY(overlay), side_view);
|
gtk_overlay_set_child(GTK_OVERLAY(overlay), side_view);
|
||||||
app->tour_overlay = overlay;
|
app->tour.overlay = overlay;
|
||||||
build_tour(*app);
|
build_tour(*app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,13 +44,23 @@
|
|||||||
</child>
|
</child>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
<child>
|
||||||
|
<!-- The hint line, wrapped in a box of its own so the contextual help's "(i)" can join it: a
|
||||||
|
`GtkLabel` cannot host a sibling, and a hint overlaid in a corner could cover the text it
|
||||||
|
is meant to sit beside. The label keeps the slack, so the mark sits at the row's right. -->
|
||||||
|
<object class="GtkBox" id="signals_note">
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkLabel" id="signals_hint">
|
<object class="GtkLabel" id="signals_hint">
|
||||||
<property name="label" translatable="yes">drag a signal onto a plot</property>
|
<property name="label" translatable="yes">drag a signal onto a plot</property>
|
||||||
<property name="halign">start</property>
|
<property name="halign">start</property>
|
||||||
|
<property name="hexpand">1</property>
|
||||||
<property name="css-classes">dim-label</property>
|
<property name="css-classes">dim-label</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkListBox" id="signal_list">
|
<object class="GtkListBox" id="signal_list">
|
||||||
<property name="selection-mode">none</property>
|
<property name="selection-mode">none</property>
|
||||||
|
|||||||
@@ -66,7 +66,33 @@
|
|||||||
<property name="title" translatable="yes">Sources</property>
|
<property name="title" translatable="yes">Sources</property>
|
||||||
<property name="icon-name">network-transmit-receive-symbolic</property>
|
<property name="icon-name">network-transmit-receive-symbolic</property>
|
||||||
<property name="child">
|
<property name="child">
|
||||||
|
<object class="GtkBox">
|
||||||
|
<property name="orientation">vertical</property>
|
||||||
|
<child>
|
||||||
|
<!-- The heading row above the list. It exists mainly so the contextual help's
|
||||||
|
"(i)" has a row of its own to join: a hint appended to the list would be a
|
||||||
|
row among the sources, and one overlaid on the scroller could cover a row
|
||||||
|
it is meant to explain. The name is dim, because the switcher's pill above
|
||||||
|
already says which page this is. -->
|
||||||
|
<object class="GtkBox" id="sources_head">
|
||||||
|
<property name="orientation">horizontal</property>
|
||||||
|
<property name="spacing">6</property>
|
||||||
|
<property name="margin-start">12</property>
|
||||||
|
<property name="margin-end">6</property>
|
||||||
|
<property name="margin-top">6</property>
|
||||||
|
<child>
|
||||||
|
<object class="GtkLabel" id="sources_heading">
|
||||||
|
<property name="label" translatable="yes">Saved sources</property>
|
||||||
|
<property name="halign">start</property>
|
||||||
|
<property name="hexpand">1</property>
|
||||||
|
<property name="css-classes">dim-label</property>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
|
</child>
|
||||||
|
<child>
|
||||||
<object class="GtkScrolledWindow">
|
<object class="GtkScrolledWindow">
|
||||||
|
<property name="vexpand">1</property>
|
||||||
<property name="hscrollbar-policy">never</property>
|
<property name="hscrollbar-policy">never</property>
|
||||||
<property name="child">
|
<property name="child">
|
||||||
<object class="GtkListBox" id="sources_list">
|
<object class="GtkListBox" id="sources_list">
|
||||||
@@ -74,6 +100,8 @@
|
|||||||
</object>
|
</object>
|
||||||
</property>
|
</property>
|
||||||
</object>
|
</object>
|
||||||
|
</child>
|
||||||
|
</object>
|
||||||
</property>
|
</property>
|
||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
@@ -97,7 +125,7 @@
|
|||||||
</object>
|
</object>
|
||||||
</child>
|
</child>
|
||||||
<child>
|
<child>
|
||||||
<object class="GtkBox">
|
<object class="GtkBox" id="waves_actions">
|
||||||
<property name="orientation">horizontal</property>
|
<property name="orientation">horizontal</property>
|
||||||
<property name="spacing">6</property>
|
<property name="spacing">6</property>
|
||||||
<property name="halign">end</property>
|
<property name="halign">end</property>
|
||||||
|
|||||||
+89
-25
@@ -57,6 +57,48 @@ enum class EventGroupKind {
|
|||||||
Compare, ///< two events in two plots side by side, at one scale, with synchronised axes
|
Compare, ///< two events in two plots side by side, at one scale, with synchronised axes
|
||||||
Accumulate, ///< up to four events overlaid in the same traces, translucent, so agreement is brightness
|
Accumulate, ///< up to four events overlaid in the same traces, translucent, so agreement is brightness
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- the guided tour, shared by both windows (ui_tour.cpp) -------------------
|
||||||
|
//
|
||||||
|
// The tour is *two* tours now -- the main window's and the source chooser's -- because the chooser is
|
||||||
|
// the window a first run meets first and the surfaces it must explain are its own. The machinery is
|
||||||
|
// one implementation: a `TourView` is the overlay children (the four dim rectangles, the spotlight
|
||||||
|
// outline and the card) plus where the walk stands in it, and either window holds one. What differs
|
||||||
|
// is only the *stops*: a stop's target is a widget the owner resolves, so each window keeps its own
|
||||||
|
// table (the chooser runs before `App` exists and could not name one of `App`'s fields).
|
||||||
|
|
||||||
|
/// One stop of a guided tour: the widget to spotlight, and the two paragraphs of text beside it.
|
||||||
|
/// The target is resolved when the tour starts, so the table may name widgets that do not exist yet.
|
||||||
|
struct TourStop {
|
||||||
|
GtkWidget* target;
|
||||||
|
const char* title;
|
||||||
|
const char* body;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// A tour's widgets and its place in the walk. `hole` is the spotlight's rectangle in the overlay's
|
||||||
|
/// own coordinates, kept from the last frame that could measure the target: a frame can come between
|
||||||
|
/// two layout passes, where `gtk_widget_compute_bounds()` answers nothing, and a hole that emptied
|
||||||
|
/// there would make the spotlight and the card jump.
|
||||||
|
struct TourView {
|
||||||
|
GtkWidget* overlay = nullptr;
|
||||||
|
GtkWidget* dim[4] = {};
|
||||||
|
GtkWidget* spot = nullptr;
|
||||||
|
GtkWidget* card = nullptr;
|
||||||
|
GtkWidget* title = nullptr;
|
||||||
|
GtkWidget* body = nullptr;
|
||||||
|
GtkWidget* counter = nullptr;
|
||||||
|
GtkWidget* back = nullptr;
|
||||||
|
GtkWidget* next = nullptr;
|
||||||
|
int hole[4] = {0, 0, 0, 0};
|
||||||
|
bool running = false;
|
||||||
|
int index = 0;
|
||||||
|
std::vector<TourStop> plan;
|
||||||
|
/// 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`.
|
||||||
|
void (*on_done)(void* user) = nullptr;
|
||||||
|
void* done_user = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
struct App {
|
struct App {
|
||||||
ScopeModel* model = nullptr;
|
ScopeModel* model = nullptr;
|
||||||
Acquisition* acq = nullptr;
|
Acquisition* acq = nullptr;
|
||||||
@@ -232,26 +274,17 @@ struct App {
|
|||||||
/// are not a panel with a heading.
|
/// are not a panel with a heading.
|
||||||
GtkWidget* status_row = nullptr;
|
GtkWidget* status_row = nullptr;
|
||||||
GtkWidget* window_control_slot = nullptr;
|
GtkWidget* window_control_slot = nullptr;
|
||||||
/// The guided tour. `tour_overlay` is a `GtkOverlay` inserted above the window's content, and the
|
/// The hint hosts that are *not* a pane's heading row: a specific control's own row, so the "?" sits
|
||||||
/// four dim rectangles, the spotlight and the card are its overlay children. `tour_plan` is the
|
/// beside the thing it explains -- the "record events" switch, the search block's action row, and the
|
||||||
|
/// signals pane's hint line (whose box exists so a hint can join it beside the text).
|
||||||
|
GtkWidget* trigger_record_row = nullptr;
|
||||||
|
GtkWidget* trigger_pre_row = nullptr;
|
||||||
|
GtkWidget* signals_note = nullptr;
|
||||||
|
/// The guided tour. `tour.overlay` is a `GtkOverlay` inserted above the window's content, and the
|
||||||
|
/// four dim rectangles, the spotlight and the card are its overlay children; `tour.plan` is the
|
||||||
/// steps whose target was on screen when the tour started, so a surface that is not there is not
|
/// steps whose target was on screen when the tour started, so a surface that is not there is not
|
||||||
/// walked over. `tour_hole` is the spotlight's rectangle in the overlay's own coordinates, kept
|
/// walked over. The machinery is shared with the source chooser's tour -- see `TourView`.
|
||||||
/// from the last frame that could measure the target: a frame can come between two layout passes,
|
TourView tour;
|
||||||
/// where `gtk_widget_compute_bounds()` answers nothing, and a hole that emptied there would make
|
|
||||||
/// the spotlight and the card jump for that frame.
|
|
||||||
GtkWidget* tour_overlay = nullptr;
|
|
||||||
GtkWidget* tour_dim[4] = {};
|
|
||||||
GtkWidget* tour_spot = nullptr;
|
|
||||||
GtkWidget* tour_card = nullptr;
|
|
||||||
GtkWidget* tour_title = nullptr;
|
|
||||||
GtkWidget* tour_body = nullptr;
|
|
||||||
GtkWidget* tour_counter = nullptr;
|
|
||||||
GtkWidget* tour_back = nullptr;
|
|
||||||
GtkWidget* tour_next = nullptr;
|
|
||||||
int tour_hole[4] = {0, 0, 0, 0};
|
|
||||||
bool tour_running = false;
|
|
||||||
int tour_index = 0;
|
|
||||||
std::vector<int> tour_plan;
|
|
||||||
/// The first-run auto-start: set by the timer that fires it, and read by the probe and the dump.
|
/// 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".
|
/// The decision itself is `settings.tour_seen`, which is the whole of "this has happened before".
|
||||||
bool tour_auto_started = false;
|
bool tour_auto_started = false;
|
||||||
@@ -1732,19 +1765,50 @@ void on_contextual_help_toggled(GtkToggleButton* button, gpointer user);
|
|||||||
/// The "?" for one surface by id, for a host that only exists while a dialog is open (the math
|
/// The "?" for one surface by id, for a host that only exists while a dialog is open (the math
|
||||||
/// editor is rebuilt on every open, so it cannot be a row the window's own build visits).
|
/// editor is rebuilt on every open, so it cannot be a row the window's own build visits).
|
||||||
void context_help_add(App& app, const char* id, GtkWidget* host);
|
void context_help_add(App& app, const char* id, GtkWidget* host);
|
||||||
|
/// The same affordance for a surface that is *not* in `App`'s table -- the source chooser's, which
|
||||||
|
/// runs before `App` exists and so cannot name one of its fields. `host` is a `GtkBox` and the button
|
||||||
|
/// joins it as its last child, which is the rule the table's own rows follow; `on` false takes any
|
||||||
|
/// hint off `host` instead. `shortcut` may be "".
|
||||||
|
void context_help_button(GtkWidget* host, const char* id, const char* title, const char* body,
|
||||||
|
const char* shortcut, bool on);
|
||||||
|
/// How many hints a widget subtree carries, and their ids, for a window that has no `App` (the
|
||||||
|
/// chooser's probe). `context_help_count()` is this with `App::window` as the root.
|
||||||
|
int context_help_count_in(GtkWidget* root, std::string* ids);
|
||||||
|
/// The size of the first *visible* hint button in a subtree, read the way the probe's geometry is
|
||||||
|
/// (`gtk_widget_compute_bounds()`), and the number a shrinking affordance is checked against. Both
|
||||||
|
/// are 0 when the layer is off or nothing on screen carries one.
|
||||||
|
void context_help_size_in(GtkWidget* root, int* width, int* height);
|
||||||
|
/// `context_help_size_in()` with `App::window` as the root.
|
||||||
|
void context_help_size(App& app, int* width, int* height);
|
||||||
|
|
||||||
/// Builds the tour's overlay children (the four dim rectangles, the spotlight outline and the card)
|
/// Builds a tour's overlay children (the four dim rectangles, the spotlight outline and the card)
|
||||||
/// inside `app.tour_overlay`. Called once, as the overlay is inserted.
|
/// inside `overlay`. Called once, as the overlay is inserted, by either window that draws a tour.
|
||||||
|
void tour_build(TourView& view, GtkWidget* overlay);
|
||||||
|
/// Starts at the first stop. `stops` is the walk, already filtered to what is on screen; `on_done`
|
||||||
|
/// (may be null) runs when the walk passes its last stop, and is how the chooser hands off.
|
||||||
|
void tour_begin(TourView& view, std::vector<TourStop> stops, void (*on_done)(void* user),
|
||||||
|
void* done_user);
|
||||||
|
/// Moves `delta` stops, finishing the walk (and calling `on_done`) on the way past the last one.
|
||||||
|
void tour_advance(TourView& view, int delta);
|
||||||
|
/// Ends the walk without finishing it: every child of the overlay is hidden and nothing is left on
|
||||||
|
/// screen. Skip and Escape.
|
||||||
|
void tour_end(TourView& view);
|
||||||
|
/// Called from the tick while the walk runs: re-measures the target each frame, so a window resize
|
||||||
|
/// keeps the spotlight on the widget, and places the card where it does not cover it.
|
||||||
|
void tour_update(TourView& view);
|
||||||
|
/// The widget the current stop points at, or nullptr when the walk is not on a stop.
|
||||||
|
GtkWidget* tour_target(TourView& view);
|
||||||
|
|
||||||
|
/// 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);
|
||||||
/// Starts the tour at its first step and writes `tour_seen`. Safe to call while it already runs --
|
/// Starts the window's tour at its first step and writes `tour_seen`. Safe to call while it already
|
||||||
/// it restarts at step 0.
|
/// runs -- it restarts at step 0.
|
||||||
void start_tour(App& app);
|
void start_tour(App& app);
|
||||||
/// Moves `delta` steps, ending the tour on the way past the last one.
|
/// Moves `delta` steps, ending the tour on the way past the last one.
|
||||||
void tour_step(App& app, int delta);
|
void tour_step(App& app, int delta);
|
||||||
/// Ends the tour: every child of the overlay is hidden and nothing of the tour is left on screen.
|
/// Ends the tour: every child of the overlay is hidden and nothing of the tour is left on screen.
|
||||||
void tour_dismiss(App& app);
|
void tour_dismiss(App& app);
|
||||||
/// Called from the tick while the tour runs: re-measures the target each frame, so a window resize
|
/// The window's tour, re-measured each frame.
|
||||||
/// keeps the spotlight on the widget, and places the card where it does not cover it.
|
|
||||||
void update_tour(App& app);
|
void update_tour(App& app);
|
||||||
bool tour_active(const App& app);
|
bool tour_active(const App& app);
|
||||||
/// The widget the current tour step points at, or nullptr when the tour is not on a step. The probe
|
/// The widget the current tour step points at, or nullptr when the tour is not on a step. The probe
|
||||||
|
|||||||
+102
-20
@@ -1,24 +1,30 @@
|
|||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||||
//
|
//
|
||||||
// The contextual-help layer: one small "?" beside each surface the application can explain, whose
|
// The contextual-help layer: one small "(i)" beside each surface the application can explain, whose
|
||||||
// bubble says what that surface is for, in a title, a sentence or two, and the key that reaches it
|
// bubble says what that surface is for, in a title, a sentence or two, and the key that reaches it
|
||||||
// when there is one.
|
// when there is one.
|
||||||
//
|
//
|
||||||
// The surfaces are the table below -- a surface is a row -- and a hint *joins the surface's own
|
// The surfaces are the table below -- a surface is a row -- and a hint *joins the surface's own
|
||||||
// heading row* rather than being overlaid on it: a sibling of the title can never cover a control,
|
// row* rather than being overlaid on it: a sibling of the title, the switch or the pencil can never
|
||||||
// which an overlay child in a corner can and does. The one exception is the plot, which has no
|
// cover a control, which an overlay child in a corner can and does. Most rows name a panel's heading,
|
||||||
// heading: its "?" is an overlay child of the overlay the layout editor already uses, so it costs
|
// which is where its switch and its pencil live; the rest name the row of a *specific* control (the
|
||||||
// nothing and sits over the plot's top-right corner.
|
// "record events" switch, the search block) so a hint can sit beside the thing it explains. The one
|
||||||
|
// exception is the plot, which has no heading: its "(i)" is an overlay child of the overlay the
|
||||||
|
// layout editor already uses, so it costs nothing and sits over the plot's top-right corner.
|
||||||
//
|
//
|
||||||
// Off means *gone*, not greyed: the toggle removes the buttons instead of disabling them. Which is
|
// Off means *gone*, not greyed: the toggle removes the buttons instead of disabling them. Which is
|
||||||
// also why the buttons are found by a data key rather than kept in a list here -- the math editor's
|
// also why the buttons are found by a data key rather than kept in a list here -- the math editor's
|
||||||
// heading dies with its builder on every reopen, and a list of pointers into it would go stale.
|
// heading dies with its builder on every reopen, and a list of pointers into it would go stale.
|
||||||
|
//
|
||||||
|
// The same affordance serves a window that has no `App` -- the source chooser, which runs before one
|
||||||
|
// exists -- through `context_help_button()`, so both windows draw *one* button.
|
||||||
|
|
||||||
#include "ui_app.hpp"
|
#include "ui_app.hpp"
|
||||||
|
|
||||||
#include <gtk/gtk.h>
|
#include <gtk/gtk.h>
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -27,11 +33,14 @@ namespace udpscope {
|
|||||||
namespace ui_impl {
|
namespace ui_impl {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
/// The "?" glyph. `help-about-symbolic` is already the status bar's information button, so a second
|
/// The "(i)" glyph. Adwaita's `dialog-information-symbolic` is the plain information mark -- a circle
|
||||||
/// thing meaning "help" needs a name of its own: this is Adwaita's question mark, and the dump reads
|
/// with a lower-case i -- and it is the one this layer means; `help-about-symbolic` is already the
|
||||||
/// it back through `gtk_icon_theme_has_icon()` -- a hint button whose icon does not resolve is a
|
/// status bar's information button, so a second thing meaning "help" needed a name of its own.
|
||||||
/// blank control, the failure the pane [edit] buttons already paid for once.
|
/// `dialog-question-symbolic` was the first choice and read as "what is this?" rather than as the
|
||||||
constexpr const char* kHintIcon = "dialog-question-symbolic";
|
/// neutral information mark the ask names. The dump reads the name back through
|
||||||
|
/// `gtk_icon_theme_has_icon()` -- a hint button whose icon does not resolve is a *blank* control, the
|
||||||
|
/// failure the pane [edit] buttons already paid for once.
|
||||||
|
constexpr const char* kHintIcon = "dialog-information-symbolic";
|
||||||
/// The data key on a hint button, carrying the surface's id as the value. One key is both the mark
|
/// The data key on a hint button, carrying the surface's id as the value. One key is both the mark
|
||||||
/// that says "this is a hint" and the name the probe and the dump report it by.
|
/// that says "this is a hint" and the name the probe and the dump report it by.
|
||||||
constexpr const char* kHintKey = "udpscope-help-hint";
|
constexpr const char* kHintKey = "udpscope-help-hint";
|
||||||
@@ -89,6 +98,22 @@ constexpr HelpHint kHelpHints[] = {
|
|||||||
"Every triggered acquisition the cache keeps, with a picture of each. Open one to read it as a "
|
"Every triggered acquisition the cache keeps, with a picture of each. Open one to read it as a "
|
||||||
"file, or select two or four and have them compared in two plots or overlaid in one.",
|
"file, or select two or four and have them compared in two plots or overlaid in one.",
|
||||||
"", &App::events_head},
|
"", &App::events_head},
|
||||||
|
{"events_record", "Recording events",
|
||||||
|
"With this on, every acquisition the trigger latches is written to the event cache -- the "
|
||||||
|
"samples around it and a picture of it -- so an event can be read again long after it has left "
|
||||||
|
"the live buffer. It is the switch beside it, and the same one the events pane shows the result "
|
||||||
|
"of.",
|
||||||
|
"", &App::trigger_record_row},
|
||||||
|
{"trigger_advanced", "The trigger's finer settings",
|
||||||
|
"Pre-trigger share is how much of the event window is kept *before* the crossing, and hold-off "
|
||||||
|
"is the quiet time after a latch in which nothing new can be armed -- the two that decide what a "
|
||||||
|
"capture looks like. The condition rows above appear and disappear with the condition chosen.",
|
||||||
|
"", &App::trigger_pre_row},
|
||||||
|
{"signals_config", "Configuring signals",
|
||||||
|
"The list is locked until the pencil above opens the per-signal controls: the calibration that "
|
||||||
|
"maps raw counts to a unit, the colour a trace is drawn in, and which plot a signal is dragged "
|
||||||
|
"onto. A signal nothing measures can be switched off to stop acquiring it.",
|
||||||
|
"", &App::signals_note},
|
||||||
{"window", "The time window",
|
{"window", "The time window",
|
||||||
"How much time is on screen, counted back from the newest sample. Type a span, change its unit, "
|
"How much time is on screen, counted back from the newest sample. Type a span, change its unit, "
|
||||||
"or step by decades; the trigger's event window is framed out of the same span. No gesture on "
|
"or step by decades; the trigger's event window is framed out of the same span. No gesture on "
|
||||||
@@ -106,7 +131,7 @@ namespace {
|
|||||||
|
|
||||||
/// The bubble: a bold title, the paragraph, and the key when there is one. Wrapped at a stated
|
/// The bubble: a bold title, the paragraph, and the key when there is one. Wrapped at a stated
|
||||||
/// width rather than left to the popover's own measure, or a long sentence is one very wide line.
|
/// width rather than left to the popover's own measure, or a long sentence is one very wide line.
|
||||||
GtkWidget* hint_popover(const HelpHint& hint) {
|
GtkWidget* hint_popover(const char* title_text, const char* body_text, const char* shortcut) {
|
||||||
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||||
gtk_widget_set_margin_start(box, 12);
|
gtk_widget_set_margin_start(box, 12);
|
||||||
gtk_widget_set_margin_end(box, 12);
|
gtk_widget_set_margin_end(box, 12);
|
||||||
@@ -115,19 +140,19 @@ GtkWidget* hint_popover(const HelpHint& hint) {
|
|||||||
|
|
||||||
GtkWidget* title = gtk_label_new(nullptr);
|
GtkWidget* title = gtk_label_new(nullptr);
|
||||||
char markup[128];
|
char markup[128];
|
||||||
std::snprintf(markup, sizeof(markup), "<b>%s</b>", hint.title);
|
std::snprintf(markup, sizeof(markup), "<b>%s</b>", title_text);
|
||||||
gtk_label_set_markup(GTK_LABEL(title), markup);
|
gtk_label_set_markup(GTK_LABEL(title), markup);
|
||||||
gtk_label_set_xalign(GTK_LABEL(title), 0.0f);
|
gtk_label_set_xalign(GTK_LABEL(title), 0.0f);
|
||||||
gtk_box_append(GTK_BOX(box), title);
|
gtk_box_append(GTK_BOX(box), title);
|
||||||
|
|
||||||
GtkWidget* body = gtk_label_new(hint.body);
|
GtkWidget* body = gtk_label_new(body_text);
|
||||||
gtk_label_set_wrap(GTK_LABEL(body), TRUE);
|
gtk_label_set_wrap(GTK_LABEL(body), TRUE);
|
||||||
gtk_label_set_max_width_chars(GTK_LABEL(body), 42);
|
gtk_label_set_max_width_chars(GTK_LABEL(body), 42);
|
||||||
gtk_label_set_xalign(GTK_LABEL(body), 0.0f);
|
gtk_label_set_xalign(GTK_LABEL(body), 0.0f);
|
||||||
gtk_box_append(GTK_BOX(box), body);
|
gtk_box_append(GTK_BOX(box), body);
|
||||||
|
|
||||||
if (hint.shortcut != nullptr && hint.shortcut[0] != '\0') {
|
if (shortcut != nullptr && shortcut[0] != '\0') {
|
||||||
const std::string key = std::string("Shortcut: ") + hint.shortcut;
|
const std::string key = std::string("Shortcut: ") + shortcut;
|
||||||
GtkWidget* line = gtk_label_new(key.c_str());
|
GtkWidget* line = gtk_label_new(key.c_str());
|
||||||
gtk_widget_add_css_class(line, "dim-label");
|
gtk_widget_add_css_class(line, "dim-label");
|
||||||
gtk_label_set_xalign(GTK_LABEL(line), 0.0f);
|
gtk_label_set_xalign(GTK_LABEL(line), 0.0f);
|
||||||
@@ -139,15 +164,19 @@ GtkWidget* hint_popover(const HelpHint& hint) {
|
|||||||
return popover;
|
return popover;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One "?" button, marked with its surface's id.
|
/// One "(i)" button, marked with its surface's id. `udpscope-hint` is what makes it *tiny*: the
|
||||||
GtkWidget* hint_button(const HelpHint& hint) {
|
/// default menu button is a 34 px control with 8 px of padding, which is the size that read as a
|
||||||
|
/// second toolbar button beside each heading. The class trims the box and the glyph together (see
|
||||||
|
/// `install_css()`), and `flat` keeps Adwaita from drawing it as a button until it is hovered.
|
||||||
|
GtkWidget* hint_button(const char* id, const char* title, const char* body, const char* shortcut) {
|
||||||
GtkWidget* button = gtk_menu_button_new();
|
GtkWidget* button = gtk_menu_button_new();
|
||||||
gtk_menu_button_set_icon_name(GTK_MENU_BUTTON(button), kHintIcon);
|
gtk_menu_button_set_icon_name(GTK_MENU_BUTTON(button), kHintIcon);
|
||||||
gtk_menu_button_set_popover(GTK_MENU_BUTTON(button), hint_popover(hint));
|
gtk_menu_button_set_popover(GTK_MENU_BUTTON(button), hint_popover(title, body, shortcut));
|
||||||
gtk_widget_add_css_class(button, "flat");
|
gtk_widget_add_css_class(button, "flat");
|
||||||
|
gtk_widget_add_css_class(button, "udpscope-hint");
|
||||||
gtk_widget_set_valign(button, GTK_ALIGN_CENTER);
|
gtk_widget_set_valign(button, GTK_ALIGN_CENTER);
|
||||||
gtk_widget_set_tooltip_text(button, "What is this?");
|
gtk_widget_set_tooltip_text(button, "What is this?");
|
||||||
g_object_set_data_full(G_OBJECT(button), kHintKey, g_strdup(hint.id), g_free);
|
g_object_set_data_full(G_OBJECT(button), kHintKey, g_strdup(id), g_free);
|
||||||
return button;
|
return button;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,7 +215,7 @@ void sync_one(App& app, int row, GtkWidget* host) {
|
|||||||
const HelpHint& hint = kHelpHints[row];
|
const HelpHint& hint = kHelpHints[row];
|
||||||
remove_hint(host);
|
remove_hint(host);
|
||||||
if (!app.settings.contextual_help) return;
|
if (!app.settings.contextual_help) return;
|
||||||
GtkWidget* button = hint_button(hint);
|
GtkWidget* button = hint_button(hint.id, hint.title, hint.body, hint.shortcut);
|
||||||
if (GTK_IS_BOX(host)) {
|
if (GTK_IS_BOX(host)) {
|
||||||
gtk_box_append(GTK_BOX(host), button);
|
gtk_box_append(GTK_BOX(host), button);
|
||||||
} else if (GTK_IS_OVERLAY(host)) {
|
} else if (GTK_IS_OVERLAY(host)) {
|
||||||
@@ -220,6 +249,23 @@ int count_hints(GtkWidget* widget, std::string* ids) {
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The first *visible* hint button in the subtree, which is the one whose allocation
|
||||||
|
/// `context_help_size_in()` reports -- every hint shares the one `udpscope-hint` size, so one of them
|
||||||
|
/// is the reading. Invisible ones are skipped: a hint inside a collapsed expander (the trigger's
|
||||||
|
/// condition rows live in one) is off screen and reads 0x0, which would make the number say the
|
||||||
|
/// affordance had shrunk to nothing.
|
||||||
|
GtkWidget* first_hint(GtkWidget* widget) {
|
||||||
|
if (widget == nullptr) return nullptr;
|
||||||
|
if (g_object_get_data(G_OBJECT(widget), kHintKey) != nullptr) {
|
||||||
|
return gtk_widget_is_visible(widget) ? widget : nullptr;
|
||||||
|
}
|
||||||
|
for (GtkWidget* child = gtk_widget_get_first_child(widget); child != nullptr;
|
||||||
|
child = gtk_widget_get_next_sibling(child)) {
|
||||||
|
if (GtkWidget* found = first_hint(child)) return found;
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void context_help_sync(App& app) {
|
void context_help_sync(App& app) {
|
||||||
@@ -232,12 +278,48 @@ void context_help_add(App& app, const char* id, GtkWidget* host) {
|
|||||||
sync_one(app, static_cast<int>(hint - kHelpHints), host);
|
sync_one(app, static_cast<int>(hint - kHelpHints), host);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void context_help_button(GtkWidget* host, const char* id, const char* title, const char* body,
|
||||||
|
const char* shortcut, bool on) {
|
||||||
|
if (host == nullptr || !GTK_IS_BOX(host)) return;
|
||||||
|
remove_hint(host);
|
||||||
|
if (!on) return;
|
||||||
|
gtk_box_append(GTK_BOX(host), hint_button(id, title, body, shortcut));
|
||||||
|
}
|
||||||
|
|
||||||
|
int context_help_count_in(GtkWidget* root, std::string* ids) {
|
||||||
|
if (ids != nullptr) ids->clear();
|
||||||
|
return count_hints(root, ids);
|
||||||
|
}
|
||||||
|
|
||||||
int context_help_count(App& app, std::string* ids) {
|
int context_help_count(App& app, std::string* ids) {
|
||||||
if (ids != nullptr) ids->clear();
|
if (ids != nullptr) ids->clear();
|
||||||
if (app.window == nullptr) return 0;
|
if (app.window == nullptr) return 0;
|
||||||
return count_hints(app.window, ids);
|
return count_hints(app.window, ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void context_help_size_in(GtkWidget* root, int* width, int* height) {
|
||||||
|
if (width != nullptr) *width = 0;
|
||||||
|
if (height != nullptr) *height = 0;
|
||||||
|
GtkWidget* hint = first_hint(root);
|
||||||
|
if (hint == nullptr) return;
|
||||||
|
graphene_rect_t r = GRAPHENE_RECT_INIT(0.0f, 0.0f, 0.0f, 0.0f);
|
||||||
|
// `gtk_widget_compute_bounds()` -- the rectangle on screen -- and not `get_width()`: the latter is
|
||||||
|
// the widget's *content* box, a border and a padding short of what a reader sees, which is the
|
||||||
|
// measurement trap the tour card already paid for (see invariants-gtk-and-chrome.md).
|
||||||
|
if (!gtk_widget_compute_bounds(hint, root, &r)) return;
|
||||||
|
if (width != nullptr) *width = static_cast<int>(std::lround(r.size.width));
|
||||||
|
if (height != nullptr) *height = static_cast<int>(std::lround(r.size.height));
|
||||||
|
}
|
||||||
|
|
||||||
|
void context_help_size(App& app, int* width, int* height) {
|
||||||
|
if (app.window == nullptr) {
|
||||||
|
if (width != nullptr) *width = 0;
|
||||||
|
if (height != nullptr) *height = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
context_help_size_in(app.window, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
void on_contextual_help_toggled(GtkToggleButton* button, gpointer user) {
|
void on_contextual_help_toggled(GtkToggleButton* button, gpointer user) {
|
||||||
App* app = static_cast<App*>(user);
|
App* app = static_cast<App*>(user);
|
||||||
const bool on = gtk_toggle_button_get_active(button) != FALSE;
|
const bool on = gtk_toggle_button_get_active(button) != FALSE;
|
||||||
|
|||||||
+10
-6
@@ -4565,11 +4565,11 @@ void helpui_report_geometry(App& a, const char* what) {
|
|||||||
graphene_rect_t card = GRAPHENE_RECT_INIT(0, 0, 0, 0);
|
graphene_rect_t card = GRAPHENE_RECT_INIT(0, 0, 0, 0);
|
||||||
GtkWidget* t = tour_target(a);
|
GtkWidget* t = tour_target(a);
|
||||||
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_is_visible(t) &&
|
const bool have_target = t != nullptr && gtk_widget_is_visible(t) &&
|
||||||
gtk_widget_compute_bounds(t, a.window, &target);
|
gtk_widget_compute_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) {
|
||||||
return outer.origin.x <= inner.origin.x + 0.5 && outer.origin.y <= inner.origin.y + 0.5 &&
|
return outer.origin.x <= inner.origin.x + 0.5 && outer.origin.y <= inner.origin.y + 0.5 &&
|
||||||
outer.origin.x + outer.size.width >= inner.origin.x + inner.size.width - 0.5 &&
|
outer.origin.x + outer.size.width >= inner.origin.x + inner.size.width - 0.5 &&
|
||||||
@@ -4607,7 +4607,11 @@ gboolean helpui_probe_step(gpointer user) {
|
|||||||
if (tour_active(*a)) tour_dismiss(*a);
|
if (tour_active(*a)) tour_dismiss(*a);
|
||||||
std::string ids;
|
std::string ids;
|
||||||
const int hints = context_help_count(*a, &ids);
|
const int hints = context_help_count(*a, &ids);
|
||||||
std::fprintf(stderr, "[helpui] contextual on: hints=%d ids={%s}\n", hints, ids.c_str());
|
int hint_w = 0;
|
||||||
|
int hint_h = 0;
|
||||||
|
context_help_size(*a, &hint_w, &hint_h);
|
||||||
|
std::fprintf(stderr, "[helpui] contextual on: hints=%d ids={%s} hint=%dx%d\n", hints,
|
||||||
|
ids.c_str(), hint_w, hint_h);
|
||||||
int total = 0;
|
int total = 0;
|
||||||
int clicks = 0;
|
int clicks = 0;
|
||||||
helpui_count_controllers(a, &total, &clicks);
|
helpui_count_controllers(a, &total, &clicks);
|
||||||
@@ -4736,11 +4740,11 @@ gboolean helpui_probe_step(gpointer user) {
|
|||||||
"[helpui] after Skip: visible=%d dims_hidden=%d spot_hidden=%d card_hidden=%d "
|
"[helpui] after Skip: visible=%d dims_hidden=%d spot_hidden=%d card_hidden=%d "
|
||||||
"controllers=%d (was %d) click_gestures=%d\n",
|
"controllers=%d (was %d) click_gestures=%d\n",
|
||||||
tour_active(*a) ? 1 : 0,
|
tour_active(*a) ? 1 : 0,
|
||||||
hidden(a->tour_dim[0]) && hidden(a->tour_dim[1]) && hidden(a->tour_dim[2]) &&
|
hidden(a->tour.dim[0]) && hidden(a->tour.dim[1]) && hidden(a->tour.dim[2]) &&
|
||||||
hidden(a->tour_dim[3])
|
hidden(a->tour.dim[3])
|
||||||
? 1
|
? 1
|
||||||
: 0,
|
: 0,
|
||||||
hidden(a->tour_spot) ? 1 : 0, hidden(a->tour_card) ? 1 : 0, total,
|
hidden(a->tour.spot) ? 1 : 0, hidden(a->tour.card) ? 1 : 0, total,
|
||||||
a->dbg_helpui_controllers, clicks);
|
a->dbg_helpui_controllers, clicks);
|
||||||
capture_help_state(*a);
|
capture_help_state(*a);
|
||||||
std::fprintf(stderr, "[helpui] layer: %s\n", a->dbg_context_help.c_str());
|
std::fprintf(stderr, "[helpui] layer: %s\n", a->dbg_context_help.c_str());
|
||||||
|
|||||||
@@ -594,6 +594,9 @@ GtkWidget* build_signal_panel(App& app) {
|
|||||||
app.signal_list = GTK_WIDGET(gtk_builder_get_object(ui, "signal_list"));
|
app.signal_list = GTK_WIDGET(gtk_builder_get_object(ui, "signal_list"));
|
||||||
// The heading row, where the contextual help's "?" joins (see `ui_context_help.cpp`).
|
// The heading row, where the contextual help's "?" joins (see `ui_context_help.cpp`).
|
||||||
app.signals_head = GTK_WIDGET(gtk_builder_get_object(ui, "signals_head"));
|
app.signals_head = GTK_WIDGET(gtk_builder_get_object(ui, "signals_head"));
|
||||||
|
// The hint line under it, which is where the "signals_config" hint joins: the heading already
|
||||||
|
// carries one, and the rule the layer follows is a hint beside the thing it explains.
|
||||||
|
app.signals_note = GTK_WIDGET(gtk_builder_get_object(ui, "signals_note"));
|
||||||
app.signals_edit = GTK_WIDGET(gtk_builder_get_object(ui, "signals_edit"));
|
app.signals_edit = GTK_WIDGET(gtk_builder_get_object(ui, "signals_edit"));
|
||||||
if (app.signals_edit != nullptr) {
|
if (app.signals_edit != nullptr) {
|
||||||
g_signal_connect(app.signals_edit, "clicked", G_CALLBACK(on_pane_edit_clicked), &app);
|
g_signal_connect(app.signals_edit, "clicked", G_CALLBACK(on_pane_edit_clicked), &app);
|
||||||
|
|||||||
+237
-184
@@ -2,7 +2,7 @@
|
|||||||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||||||
//
|
//
|
||||||
// The guided tour: a walk over the surfaces a first run has no way of knowing about, drawn on a
|
// The guided tour: a walk over the surfaces a first run has no way of knowing about, drawn on a
|
||||||
// `GtkOverlay` inserted above the window's content.
|
// `GtkOverlay` inserted above a window's content.
|
||||||
//
|
//
|
||||||
// Why an overlay and not a popover per step: a popover takes a pointer grab, and a run of them takes
|
// Why an overlay and not a popover per step: a popover takes a pointer grab, and a run of them takes
|
||||||
// one per step. The overlay takes none at all -- what makes the tour modal is the four dim
|
// one per step. The overlay takes none at all -- what makes the tour modal is the four dim
|
||||||
@@ -15,6 +15,13 @@
|
|||||||
// is re-measured on every frame -- the target's own allocation is 0 on the frame it is first shown
|
// is re-measured on every frame -- the target's own allocation is 0 on the frame it is first shown
|
||||||
// (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,
|
||||||
|
// the trigger and the like; the source chooser's -- which appears *before* `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
|
||||||
|
// between the two is only which widgets they name: the chooser keeps its table in
|
||||||
|
// `source_dialog.cpp`, where its widgets live.
|
||||||
|
|
||||||
#include "ui_app.hpp"
|
#include "ui_app.hpp"
|
||||||
|
|
||||||
@@ -24,44 +31,13 @@
|
|||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace udpscope {
|
namespace udpscope {
|
||||||
namespace ui_impl {
|
namespace ui_impl {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
/// One step. The target is measured, never named by a stored rectangle: the point of re-measuring
|
|
||||||
/// every frame is that nothing here knows where the widget is.
|
|
||||||
struct TourStep {
|
|
||||||
const char* id;
|
|
||||||
const char* title;
|
|
||||||
const char* body;
|
|
||||||
GtkWidget* App::* target;
|
|
||||||
};
|
|
||||||
|
|
||||||
constexpr TourStep kTourSteps[] = {
|
|
||||||
{"plot", "The plot",
|
|
||||||
"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.",
|
|
||||||
&App::area_overlay},
|
|
||||||
{"side", "The side panel",
|
|
||||||
"The working surface beside the plot. Measure holds the cursors and the measurements; Signals "
|
|
||||||
"holds the trigger and the list of signals. The buttons in the bar above show and hide it.",
|
|
||||||
&App::side_box},
|
|
||||||
{"trigger", "The trigger",
|
|
||||||
"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.",
|
|
||||||
&App::trigger_body},
|
|
||||||
{"signals", "The signals",
|
|
||||||
"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.",
|
|
||||||
&App::signal_list},
|
|
||||||
{"status", "The status line",
|
|
||||||
"The stream's own report, once a second: the display mode, the rate arriving, the window on "
|
|
||||||
"screen and the buffer holding it. The information button opens the counters behind it.",
|
|
||||||
&App::status_row},
|
|
||||||
};
|
|
||||||
constexpr int kTourStepCount = static_cast<int>(sizeof(kTourSteps) / sizeof(kTourSteps[0]));
|
|
||||||
|
|
||||||
/// The card's width. Wide enough for a comfortable line of the paragraph, narrow enough to fit
|
/// The card's width. Wide enough for a comfortable line of the paragraph, narrow enough to fit
|
||||||
/// beside the plot on the documented window -- at 756 px the side panel is 320 and a card wider than
|
/// beside the plot on the documented window -- at 756 px the side panel is 320 and a card wider than
|
||||||
/// that is a card that has to cover its own target. It is a *floor* on the box, not a cap on the
|
/// that is a card that has to cover its own target. It is a *floor* on the box, not a cap on the
|
||||||
@@ -94,173 +70,166 @@ void place_natural(GtkWidget* widget, int x, int y) {
|
|||||||
gtk_widget_set_margin_top(widget, std::max(0, y));
|
gtk_widget_set_margin_top(widget, std::max(0, y));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The step currently walked, or nullptr when the tour is not on a step.
|
/// The stop currently walked, or nullptr when the walk is not on a stop.
|
||||||
const TourStep* current_step(const App& app) {
|
const TourStop* current_stop(const TourView& view) {
|
||||||
if (app.tour_index < 0 || app.tour_index >= static_cast<int>(app.tour_plan.size())) return nullptr;
|
if (view.index < 0 || view.index >= static_cast<int>(view.plan.size())) return nullptr;
|
||||||
return &kTourSteps[app.tour_plan[static_cast<std::size_t>(app.tour_index)]];
|
return &view.plan[static_cast<std::size_t>(view.index)];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Restates the card's text for the current step. Called on a change of step, not per frame.
|
/// Restates the card's text for the current stop. Called on a change of step, not per frame.
|
||||||
void show_step(App& app) {
|
void show_step(TourView& view) {
|
||||||
const TourStep* step = current_step(app);
|
const TourStop* stop = current_stop(view);
|
||||||
if (step == nullptr || app.tour_card == nullptr) return;
|
if (stop == nullptr || view.card == nullptr) return;
|
||||||
// The hole belongs to the step: reusing the last one for a frame would put the spotlight on the
|
// The hole belongs to the step: reusing the last one for a frame would put the spotlight on the
|
||||||
// surface the tour has just left, and the next frame fills this one in.
|
// surface the tour has just left, and the next frame fills this one in.
|
||||||
app.tour_hole[0] = app.tour_hole[1] = app.tour_hole[2] = app.tour_hole[3] = 0;
|
view.hole[0] = view.hole[1] = view.hole[2] = view.hole[3] = 0;
|
||||||
char markup[128];
|
char markup[128];
|
||||||
std::snprintf(markup, sizeof(markup), "<b>%s</b>", step->title);
|
std::snprintf(markup, sizeof(markup), "<b>%s</b>", stop->title);
|
||||||
gtk_label_set_markup(GTK_LABEL(app.tour_title), markup);
|
gtk_label_set_markup(GTK_LABEL(view.title), markup);
|
||||||
gtk_label_set_text(GTK_LABEL(app.tour_body), step->body);
|
gtk_label_set_text(GTK_LABEL(view.body), stop->body);
|
||||||
char counter[64];
|
char counter[64];
|
||||||
std::snprintf(counter, sizeof(counter), "%d of %d",
|
std::snprintf(counter, sizeof(counter), "%d of %d", view.index + 1,
|
||||||
app.tour_index + 1, static_cast<int>(app.tour_plan.size()));
|
static_cast<int>(view.plan.size()));
|
||||||
gtk_label_set_text(GTK_LABEL(app.tour_counter), counter);
|
gtk_label_set_text(GTK_LABEL(view.counter), counter);
|
||||||
gtk_widget_set_visible(app.tour_back, app.tour_index > 0);
|
gtk_widget_set_visible(view.back, view.index > 0);
|
||||||
gtk_button_set_label(GTK_BUTTON(app.tour_next),
|
gtk_button_set_label(GTK_BUTTON(view.next),
|
||||||
app.tour_index + 1 >= static_cast<int>(app.tour_plan.size()) ? "Finish"
|
view.index + 1 >= static_cast<int>(view.plan.size()) ? "Finish" : "Next");
|
||||||
: "Next");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
GtkWidget* tour_target(App& app) {
|
// --- the machinery, shared by both windows ----------------------------------
|
||||||
const TourStep* step = current_step(app);
|
|
||||||
return step != nullptr ? app.*(step->target) : nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void build_tour(App& app) {
|
void tour_build(TourView& view, GtkWidget* overlay) {
|
||||||
if (app.tour_overlay == nullptr) return;
|
if (overlay == nullptr || view.card != nullptr) return;
|
||||||
for (GtkWidget*& dim : app.tour_dim) {
|
view.overlay = overlay;
|
||||||
|
for (GtkWidget*& dim : view.dim) {
|
||||||
dim = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
dim = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||||
gtk_widget_add_css_class(dim, "udpscope-tour-dim");
|
gtk_widget_add_css_class(dim, "udpscope-tour-dim");
|
||||||
gtk_overlay_add_overlay(GTK_OVERLAY(app.tour_overlay), dim);
|
gtk_overlay_add_overlay(GTK_OVERLAY(overlay), dim);
|
||||||
}
|
}
|
||||||
app.tour_spot = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
view.spot = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||||
gtk_widget_add_css_class(app.tour_spot, "udpscope-tour-spot");
|
gtk_widget_add_css_class(view.spot, "udpscope-tour-spot");
|
||||||
// The outline is a frame, not a shield: a click on the *target* is a click on the target.
|
// The outline is a frame, not a shield: a click on the *target* is a click on the target.
|
||||||
gtk_widget_set_can_target(app.tour_spot, FALSE);
|
gtk_widget_set_can_target(view.spot, FALSE);
|
||||||
gtk_overlay_add_overlay(GTK_OVERLAY(app.tour_overlay), app.tour_spot);
|
gtk_overlay_add_overlay(GTK_OVERLAY(overlay), view.spot);
|
||||||
|
|
||||||
// The card, last: overlay children are painted in the order they were added, and the card has to
|
// The card, last: overlay children are painted in the order they were added, and the card has to
|
||||||
// be above the dim strips that cover the rest of the window.
|
// be above the dim strips that cover the rest of the window.
|
||||||
app.tour_card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
view.card = gtk_box_new(GTK_ORIENTATION_VERTICAL, 8);
|
||||||
gtk_widget_add_css_class(app.tour_card, "udpscope-tour-card");
|
gtk_widget_add_css_class(view.card, "udpscope-tour-card");
|
||||||
gtk_widget_set_size_request(app.tour_card, kCardWidth, -1);
|
gtk_widget_set_size_request(view.card, kCardWidth, -1);
|
||||||
|
|
||||||
app.tour_title = gtk_label_new(nullptr);
|
view.title = gtk_label_new(nullptr);
|
||||||
gtk_label_set_xalign(GTK_LABEL(app.tour_title), 0.0f);
|
gtk_label_set_xalign(GTK_LABEL(view.title), 0.0f);
|
||||||
gtk_box_append(GTK_BOX(app.tour_card), app.tour_title);
|
gtk_box_append(GTK_BOX(view.card), view.title);
|
||||||
|
|
||||||
app.tour_body = gtk_label_new(nullptr);
|
view.body = gtk_label_new(nullptr);
|
||||||
gtk_label_set_wrap(GTK_LABEL(app.tour_body), TRUE);
|
gtk_label_set_wrap(GTK_LABEL(view.body), TRUE);
|
||||||
// The cap that keeps the card narrow. `max_width_chars` is what bounds a wrapped label's *natural*
|
// The cap that keeps the card narrow. `max_width_chars` is what bounds a wrapped label's *natural*
|
||||||
// width; without it the paragraph's natural width is the whole sentence on one line and the card
|
// width; without it the paragraph's natural width is the whole sentence on one line and the card
|
||||||
// is that wide however it is placed.
|
// is that wide however it is placed.
|
||||||
gtk_label_set_max_width_chars(GTK_LABEL(app.tour_body), 30);
|
gtk_label_set_max_width_chars(GTK_LABEL(view.body), 30);
|
||||||
gtk_label_set_xalign(GTK_LABEL(app.tour_body), 0.0f);
|
gtk_label_set_xalign(GTK_LABEL(view.body), 0.0f);
|
||||||
gtk_box_append(GTK_BOX(app.tour_card), app.tour_body);
|
gtk_box_append(GTK_BOX(view.card), view.body);
|
||||||
|
|
||||||
GtkWidget* footer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
GtkWidget* footer = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||||
app.tour_counter = gtk_label_new(nullptr);
|
view.counter = gtk_label_new(nullptr);
|
||||||
gtk_widget_add_css_class(app.tour_counter, "dim-label");
|
gtk_widget_add_css_class(view.counter, "dim-label");
|
||||||
gtk_label_set_xalign(GTK_LABEL(app.tour_counter), 0.0f);
|
gtk_label_set_xalign(GTK_LABEL(view.counter), 0.0f);
|
||||||
gtk_widget_set_hexpand(app.tour_counter, TRUE);
|
gtk_widget_set_hexpand(view.counter, TRUE);
|
||||||
gtk_box_append(GTK_BOX(footer), app.tour_counter);
|
gtk_box_append(GTK_BOX(footer), view.counter);
|
||||||
app.tour_back = gtk_button_new_with_label("Back");
|
view.back = gtk_button_new_with_label("Back");
|
||||||
g_signal_connect(app.tour_back, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
g_signal_connect(view.back, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||||
tour_step(*static_cast<App*>(user), -1);
|
tour_advance(*static_cast<TourView*>(user), -1);
|
||||||
}),
|
}),
|
||||||
&app);
|
&view);
|
||||||
gtk_box_append(GTK_BOX(footer), app.tour_back);
|
gtk_box_append(GTK_BOX(footer), view.back);
|
||||||
GtkWidget* skip = gtk_button_new_with_label("Skip");
|
GtkWidget* skip = gtk_button_new_with_label("Skip");
|
||||||
g_signal_connect(skip, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
g_signal_connect(skip, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||||
tour_dismiss(*static_cast<App*>(user));
|
tour_end(*static_cast<TourView*>(user));
|
||||||
}),
|
}),
|
||||||
&app);
|
&view);
|
||||||
gtk_box_append(GTK_BOX(footer), skip);
|
gtk_box_append(GTK_BOX(footer), skip);
|
||||||
app.tour_next = gtk_button_new_with_label("Next");
|
view.next = gtk_button_new_with_label("Next");
|
||||||
gtk_widget_add_css_class(app.tour_next, "suggested-action");
|
gtk_widget_add_css_class(view.next, "suggested-action");
|
||||||
g_signal_connect(app.tour_next, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
g_signal_connect(view.next, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||||
tour_step(*static_cast<App*>(user), 1);
|
tour_advance(*static_cast<TourView*>(user), 1);
|
||||||
}),
|
}),
|
||||||
&app);
|
&view);
|
||||||
gtk_box_append(GTK_BOX(footer), app.tour_next);
|
gtk_box_append(GTK_BOX(footer), view.next);
|
||||||
gtk_box_append(GTK_BOX(app.tour_card), footer);
|
gtk_box_append(GTK_BOX(view.card), footer);
|
||||||
gtk_overlay_add_overlay(GTK_OVERLAY(app.tour_overlay), app.tour_card);
|
gtk_overlay_add_overlay(GTK_OVERLAY(overlay), view.card);
|
||||||
|
|
||||||
// Everything starts hidden: the tour is not on screen until it is started.
|
// Everything starts hidden: the tour is not on screen until it is started.
|
||||||
for (GtkWidget* dim : app.tour_dim) gtk_widget_set_visible(dim, FALSE);
|
for (GtkWidget* dim : view.dim) gtk_widget_set_visible(dim, FALSE);
|
||||||
gtk_widget_set_visible(app.tour_spot, FALSE);
|
gtk_widget_set_visible(view.spot, FALSE);
|
||||||
gtk_widget_set_visible(app.tour_card, FALSE);
|
gtk_widget_set_visible(view.card, FALSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
void start_tour(App& app) {
|
void tour_begin(TourView& view, std::vector<TourStop> stops, void (*on_done)(void* user),
|
||||||
if (app.tour_overlay == nullptr || app.tour_card == nullptr) return;
|
void* done_user) {
|
||||||
// "The tour has run" is written when it *starts*: a user who skips it has still seen it, and the
|
if (view.overlay == nullptr || view.card == nullptr) return;
|
||||||
// alternative -- marking it on the last step -- would offer the whole tour again to everyone who
|
view.plan = std::move(stops);
|
||||||
// decided in the first second that they did not want it.
|
if (view.plan.empty()) return;
|
||||||
if (!app.settings.tour_seen) {
|
view.on_done = on_done;
|
||||||
app.settings.tour_seen = true;
|
view.done_user = done_user;
|
||||||
(void)save_settings(app.settings);
|
view.index = 0;
|
||||||
}
|
view.running = true;
|
||||||
// The steps whose target is on screen, in table order. A surface that is not there is not walked
|
for (GtkWidget* dim : view.dim) gtk_widget_set_visible(dim, TRUE);
|
||||||
// over -- the point of asking `is_visible()` rather than `get_visible()` is that a widget under a
|
gtk_widget_set_visible(view.spot, TRUE);
|
||||||
// hidden parent still reports its own flag as true.
|
gtk_widget_set_visible(view.card, TRUE);
|
||||||
app.tour_plan.clear();
|
show_step(view);
|
||||||
for (int i = 0; i < kTourStepCount; ++i) {
|
tour_update(view);
|
||||||
GtkWidget* target = app.*(kTourSteps[i].target);
|
|
||||||
if (target != nullptr && gtk_widget_is_visible(target)) app.tour_plan.push_back(i);
|
|
||||||
}
|
|
||||||
if (app.tour_plan.empty()) return;
|
|
||||||
app.tour_index = 0;
|
|
||||||
app.tour_running = true;
|
|
||||||
for (GtkWidget* dim : app.tour_dim) gtk_widget_set_visible(dim, TRUE);
|
|
||||||
gtk_widget_set_visible(app.tour_spot, TRUE);
|
|
||||||
gtk_widget_set_visible(app.tour_card, TRUE);
|
|
||||||
show_step(app);
|
|
||||||
update_tour(app);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void tour_step(App& app, int delta) {
|
void tour_advance(TourView& view, int delta) {
|
||||||
if (!app.tour_running) return;
|
if (!view.running) return;
|
||||||
const int next = app.tour_index + delta;
|
const int next = view.index + delta;
|
||||||
if (next < 0 || next >= static_cast<int>(app.tour_plan.size())) {
|
if (next < 0 || next >= static_cast<int>(view.plan.size())) {
|
||||||
tour_dismiss(app);
|
// Past the last stop: this is the walk *finishing*, which is the one ending that hands the tour
|
||||||
|
// on. Take the callback out before `tour_end()` clears it.
|
||||||
|
void (*done)(void*) = view.on_done;
|
||||||
|
void* user = view.done_user;
|
||||||
|
tour_end(view);
|
||||||
|
if (done != nullptr) done(user);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
app.tour_index = next;
|
view.index = next;
|
||||||
show_step(app);
|
show_step(view);
|
||||||
update_tour(app);
|
tour_update(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
void tour_dismiss(App& app) {
|
void tour_end(TourView& view) {
|
||||||
app.tour_running = false;
|
view.running = false;
|
||||||
app.tour_index = 0;
|
view.index = 0;
|
||||||
for (GtkWidget* dim : app.tour_dim) {
|
view.plan.clear();
|
||||||
|
view.on_done = nullptr;
|
||||||
|
view.done_user = nullptr;
|
||||||
|
for (GtkWidget* dim : view.dim) {
|
||||||
if (dim != nullptr) gtk_widget_set_visible(dim, FALSE);
|
if (dim != nullptr) gtk_widget_set_visible(dim, FALSE);
|
||||||
}
|
}
|
||||||
if (app.tour_spot != nullptr) gtk_widget_set_visible(app.tour_spot, FALSE);
|
if (view.spot != nullptr) gtk_widget_set_visible(view.spot, FALSE);
|
||||||
if (app.tour_card != nullptr) gtk_widget_set_visible(app.tour_card, FALSE);
|
if (view.card != nullptr) gtk_widget_set_visible(view.card, FALSE);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool tour_active(const App& app) { return app.tour_running; }
|
GtkWidget* tour_target(TourView& view) {
|
||||||
|
const TourStop* stop = current_stop(view);
|
||||||
|
return stop != nullptr ? stop->target : nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
int tour_step_count(const App& app) { return static_cast<int>(app.tour_plan.size()); }
|
void tour_update(TourView& view) {
|
||||||
|
if (!view.running || view.overlay == nullptr) return;
|
||||||
int tour_step_index(const App& app) { return app.tour_index; }
|
const int w = gtk_widget_get_width(view.overlay);
|
||||||
|
const int h = gtk_widget_get_height(view.overlay);
|
||||||
void update_tour(App& app) {
|
|
||||||
if (!app.tour_running || app.tour_overlay == nullptr) return;
|
|
||||||
const int w = gtk_widget_get_width(app.tour_overlay);
|
|
||||||
const int h = gtk_widget_get_height(app.tour_overlay);
|
|
||||||
if (w <= 0 || h <= 0) return; // no allocation yet: nothing to place anything against
|
if (w <= 0 || h <= 0) return; // no allocation yet: nothing to place anything against
|
||||||
|
|
||||||
// The target, re-measured every frame. Its allocation is 0 on the frame it is first shown, so a
|
// The target, re-measured every frame. Its allocation is 0 on the frame it is first shown, so a
|
||||||
// one-shot measurement would leave the hole in the corner.
|
// one-shot measurement would leave the hole in the corner.
|
||||||
GtkWidget* target = tour_target(app);
|
GtkWidget* target = tour_target(view);
|
||||||
graphene_rect_t bounds = GRAPHENE_RECT_INIT(0.0f, 0.0f, 0.0f, 0.0f);
|
graphene_rect_t bounds = GRAPHENE_RECT_INIT(0.0f, 0.0f, 0.0f, 0.0f);
|
||||||
const bool target_present = target != nullptr && gtk_widget_is_visible(target);
|
const bool target_present = target != nullptr && gtk_widget_is_visible(target);
|
||||||
if (target_present && gtk_widget_compute_bounds(target, app.tour_overlay, &bounds)) {
|
if (target_present && gtk_widget_compute_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.
|
||||||
@@ -268,28 +237,28 @@ void update_tour(App& app) {
|
|||||||
const int y0 = static_cast<int>(std::floor(bounds.origin.y)) - 3;
|
const int y0 = static_cast<int>(std::floor(bounds.origin.y)) - 3;
|
||||||
const int x1 = static_cast<int>(std::ceil(bounds.origin.x + bounds.size.width)) + 3;
|
const int x1 = static_cast<int>(std::ceil(bounds.origin.x + bounds.size.width)) + 3;
|
||||||
const int y1 = static_cast<int>(std::ceil(bounds.origin.y + bounds.size.height)) + 3;
|
const int y1 = static_cast<int>(std::ceil(bounds.origin.y + bounds.size.height)) + 3;
|
||||||
app.tour_hole[0] = std::clamp(x0, 0, w);
|
view.hole[0] = std::clamp(x0, 0, w);
|
||||||
app.tour_hole[1] = std::clamp(y0, 0, h);
|
view.hole[1] = std::clamp(y0, 0, h);
|
||||||
app.tour_hole[2] = std::clamp(x1, 0, w) - app.tour_hole[0];
|
view.hole[2] = std::clamp(x1, 0, w) - view.hole[0];
|
||||||
app.tour_hole[3] = std::clamp(y1, 0, h) - app.tour_hole[1];
|
view.hole[3] = std::clamp(y1, 0, h) - view.hole[1];
|
||||||
} else if (!target_present) {
|
} else if (!target_present) {
|
||||||
// Nothing on screen to point at: the whole surface dims and the card takes a corner. A step whose
|
// Nothing on screen to point at: the whole surface dims and the card takes a corner. A step whose
|
||||||
// target went away mid-tour is not a step to be skipped silently.
|
// target went away mid-tour is not a step to be skipped silently.
|
||||||
app.tour_hole[0] = app.tour_hole[1] = app.tour_hole[2] = app.tour_hole[3] = 0;
|
view.hole[0] = view.hole[1] = view.hole[2] = view.hole[3] = 0;
|
||||||
}
|
}
|
||||||
// ... and when the target *is* there but could not be measured, the previous hole stands: a frame
|
// ... and when the target *is* there but could not be measured, the previous hole stands: a frame
|
||||||
// can arrive between two layout passes, and emptying the hole there is the spotlight and the card
|
// can arrive between two layout passes, and emptying the hole there is the spotlight and the card
|
||||||
// jumping to the corner and back.
|
// jumping to the corner and back.
|
||||||
const int hx = app.tour_hole[0];
|
const int hx = view.hole[0];
|
||||||
const int hy = app.tour_hole[1];
|
const int hy = view.hole[1];
|
||||||
const int hw = app.tour_hole[2];
|
const int hw = view.hole[2];
|
||||||
const int hh = app.tour_hole[3];
|
const int hh = view.hole[3];
|
||||||
|
|
||||||
place(app.tour_dim[0], 0, 0, w, hy); // above the hole
|
place(view.dim[0], 0, 0, w, hy); // above the hole
|
||||||
place(app.tour_dim[1], 0, hy + hh, w, h - hy - hh); // below it
|
place(view.dim[1], 0, hy + hh, w, h - hy - hh); // below it
|
||||||
place(app.tour_dim[2], 0, hy, hx, hh); // to its left
|
place(view.dim[2], 0, hy, hx, hh); // to its left
|
||||||
place(app.tour_dim[3], hx + hw, hy, w - hx - hw, hh); // to its right
|
place(view.dim[3], hx + hw, hy, w - hx - hw, hh); // to its right
|
||||||
place(app.tour_spot, hx, hy, hw, hh);
|
place(view.spot, hx, hy, hw, hh);
|
||||||
|
|
||||||
// The card, placed clear of the hole whenever the window has room for it. The strip order is the
|
// The card, placed clear of the hole whenever the window has room for it. The strip order is the
|
||||||
// order a card is easiest to read in: under the thing it describes, then over it, then in
|
// order a card is easiest to read in: under the thing it describes, then over it, then in
|
||||||
@@ -316,19 +285,18 @@ void update_tour(App& app) {
|
|||||||
int cw = 0;
|
int cw = 0;
|
||||||
int ch = 0;
|
int ch = 0;
|
||||||
graphene_rect_t card_bounds = GRAPHENE_RECT_INIT(0, 0, 0, 0);
|
graphene_rect_t card_bounds = GRAPHENE_RECT_INIT(0, 0, 0, 0);
|
||||||
if (gtk_widget_is_visible(app.tour_card) && gtk_widget_get_width(app.tour_card) > 0 &&
|
if (gtk_widget_is_visible(view.card) && gtk_widget_get_width(view.card) > 0 &&
|
||||||
gtk_widget_compute_bounds(app.tour_card, app.tour_overlay, &card_bounds)) {
|
gtk_widget_compute_bounds(view.card, view.overlay, &card_bounds)) {
|
||||||
cw = static_cast<int>(std::ceil(card_bounds.size.width));
|
cw = static_cast<int>(std::ceil(card_bounds.size.width));
|
||||||
ch = static_cast<int>(std::ceil(card_bounds.size.height));
|
ch = static_cast<int>(std::ceil(card_bounds.size.height));
|
||||||
}
|
}
|
||||||
if (cw <= 0 || ch <= 0) {
|
if (cw <= 0 || ch <= 0) {
|
||||||
gtk_widget_measure(app.tour_card, GTK_ORIENTATION_HORIZONTAL, -1, nullptr, &cw, nullptr,
|
gtk_widget_measure(view.card, GTK_ORIENTATION_HORIZONTAL, -1, nullptr, &cw, nullptr, nullptr);
|
||||||
nullptr);
|
cw = std::max(kCardWidth, cw - gtk_widget_get_margin_start(view.card) -
|
||||||
cw = std::max(kCardWidth, cw - gtk_widget_get_margin_start(app.tour_card) -
|
gtk_widget_get_margin_end(view.card));
|
||||||
gtk_widget_get_margin_end(app.tour_card));
|
gtk_widget_measure(view.card, GTK_ORIENTATION_VERTICAL, -1, nullptr, &ch, nullptr, nullptr);
|
||||||
gtk_widget_measure(app.tour_card, GTK_ORIENTATION_VERTICAL, -1, nullptr, &ch, nullptr, nullptr);
|
ch = std::max(0, ch - gtk_widget_get_margin_top(view.card) -
|
||||||
ch = std::max(0, ch - gtk_widget_get_margin_top(app.tour_card) -
|
gtk_widget_get_margin_bottom(view.card));
|
||||||
gtk_widget_get_margin_bottom(app.tour_card));
|
|
||||||
}
|
}
|
||||||
int cx = 0, cy = 0;
|
int cx = 0, cy = 0;
|
||||||
if (h - (hy + hh) >= ch + 2 * kGap) {
|
if (h - (hy + hh) >= ch + 2 * kGap) {
|
||||||
@@ -352,12 +320,92 @@ void update_tour(App& app) {
|
|||||||
cx = std::max(kGap, w - cw - kGap);
|
cx = std::max(kGap, w - cw - kGap);
|
||||||
cy = std::max(kGap, h - ch - kGap);
|
cy = std::max(kGap, h - ch - kGap);
|
||||||
}
|
}
|
||||||
place_natural(app.tour_card, cx, cy);
|
place_natural(view.card, cx, cy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- the main window's tour, and its table of stops --------------------------
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
/// One stop of the *window's* tour. The target is measured, never named by a stored rectangle: the
|
||||||
|
/// point of re-measuring every frame is that nothing here knows where the widget is.
|
||||||
|
struct TourStep {
|
||||||
|
const char* id;
|
||||||
|
const char* title;
|
||||||
|
const char* body;
|
||||||
|
GtkWidget* App::* target;
|
||||||
|
};
|
||||||
|
|
||||||
|
constexpr TourStep kTourSteps[] = {
|
||||||
|
{"plot", "The plot",
|
||||||
|
"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.",
|
||||||
|
&App::area_overlay},
|
||||||
|
{"side", "The side panel",
|
||||||
|
"The working surface beside the plot. Measure holds the cursors and the measurements; Signals "
|
||||||
|
"holds the trigger and the list of signals. The buttons in the bar above show and hide it.",
|
||||||
|
&App::side_box},
|
||||||
|
{"trigger", "The trigger",
|
||||||
|
"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.",
|
||||||
|
&App::trigger_body},
|
||||||
|
{"signals", "The signals",
|
||||||
|
"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.",
|
||||||
|
&App::signal_list},
|
||||||
|
{"status", "The status line",
|
||||||
|
"The stream's own report, once a second: the display mode, the rate arriving, the window on "
|
||||||
|
"screen and the buffer holding it. The information button opens the counters behind it.",
|
||||||
|
&App::status_row},
|
||||||
|
};
|
||||||
|
constexpr int kTourStepCount = static_cast<int>(sizeof(kTourSteps) / sizeof(kTourSteps[0]));
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void build_tour(App& app) {
|
||||||
|
tour_build(app.tour, app.tour.overlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
void start_tour(App& app) {
|
||||||
|
if (app.tour.overlay == nullptr || app.tour.card == nullptr) return;
|
||||||
|
// "The tour has run" is written when it *starts*: a user who skips it has still seen it, and the
|
||||||
|
// alternative -- marking it on the last step -- would offer the whole tour again to everyone who
|
||||||
|
// decided in the first second that they did not want it.
|
||||||
|
if (!app.settings.tour_seen) {
|
||||||
|
app.settings.tour_seen = true;
|
||||||
|
(void)save_settings(app.settings);
|
||||||
|
}
|
||||||
|
// The stops whose target is on screen, in table order. A surface that is not there is not walked
|
||||||
|
// over -- the point of asking `is_visible()` rather than `get_visible()` is that a widget under a
|
||||||
|
// hidden parent still reports its own flag as true.
|
||||||
|
std::vector<TourStop> stops;
|
||||||
|
for (int i = 0; i < kTourStepCount; ++i) {
|
||||||
|
GtkWidget* target = app.*(kTourSteps[i].target);
|
||||||
|
if (target != nullptr && gtk_widget_is_visible(target)) {
|
||||||
|
stops.push_back({target, kTourSteps[i].title, kTourSteps[i].body});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stops.empty()) return;
|
||||||
|
tour_begin(app.tour, std::move(stops), nullptr, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
void tour_step(App& app, int delta) { tour_advance(app.tour, delta); }
|
||||||
|
|
||||||
|
void tour_dismiss(App& app) { tour_end(app.tour); }
|
||||||
|
|
||||||
|
bool tour_active(const App& app) { return app.tour.running; }
|
||||||
|
|
||||||
|
int tour_step_count(const App& app) { return static_cast<int>(app.tour.plan.size()); }
|
||||||
|
|
||||||
|
int tour_step_index(const App& app) { return app.tour.index; }
|
||||||
|
|
||||||
|
void update_tour(App& app) { tour_update(app.tour); }
|
||||||
|
|
||||||
|
GtkWidget* tour_target(App& app) { return tour_target(app.tour); }
|
||||||
|
|
||||||
gboolean tour_autostart(gpointer user) {
|
gboolean tour_autostart(gpointer user) {
|
||||||
App* app = static_cast<App*>(user);
|
App* app = static_cast<App*>(user);
|
||||||
if (app->window == nullptr || app->tour_overlay == nullptr) return G_SOURCE_REMOVE;
|
if (app->window == nullptr || app->tour.overlay == nullptr) return G_SOURCE_REMOVE;
|
||||||
app->tour_auto_started = true;
|
app->tour_auto_started = true;
|
||||||
start_tour(*app);
|
start_tour(*app);
|
||||||
return G_SOURCE_REMOVE;
|
return G_SOURCE_REMOVE;
|
||||||
@@ -365,7 +413,7 @@ gboolean tour_autostart(gpointer user) {
|
|||||||
|
|
||||||
gboolean tour_start_requested(gpointer user) {
|
gboolean tour_start_requested(gpointer user) {
|
||||||
App* app = static_cast<App*>(user);
|
App* app = static_cast<App*>(user);
|
||||||
if (app->window == nullptr || app->tour_overlay == nullptr) return G_SOURCE_REMOVE;
|
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
|
// 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.
|
// existed, and the reading has to say which of the two started it.
|
||||||
app->tour_requested = true;
|
app->tour_requested = true;
|
||||||
@@ -406,21 +454,26 @@ void capture_help_state(App& app) {
|
|||||||
std::string ids;
|
std::string ids;
|
||||||
const int hints = context_help_count(app, &ids);
|
const int hints = context_help_count(app, &ids);
|
||||||
// The icon is read back through the theme for the reason the pane [edit] buttons are: a name
|
// The icon is read back through the theme for the reason the pane [edit] buttons are: a name
|
||||||
// Adwaita does not have is a *blank* control that works and reads as nothing at all.
|
// Adwaita does not have is a *blank* control that works and reads as nothing at all. The size is
|
||||||
|
// the reading a "much tinier" affordance is checked against, and it is measured the way every
|
||||||
|
// other rectangle here is (`gtk_widget_compute_bounds()`), on the tick while the widget is alive.
|
||||||
GtkIconTheme* theme = gtk_icon_theme_get_for_display(gdk_display_get_default());
|
GtkIconTheme* theme = gtk_icon_theme_get_for_display(gdk_display_get_default());
|
||||||
const bool icon_ok = theme != nullptr && gtk_icon_theme_has_icon(theme, "dialog-question-symbolic");
|
const bool icon_ok = theme != nullptr && gtk_icon_theme_has_icon(theme, "dialog-information-symbolic");
|
||||||
char layer[512];
|
int hint_w = 0;
|
||||||
|
int hint_h = 0;
|
||||||
|
context_help_size(app, &hint_w, &hint_h);
|
||||||
|
char layer[640];
|
||||||
std::snprintf(layer, sizeof(layer),
|
std::snprintf(layer, sizeof(layer),
|
||||||
"contextual=%s hints=%d ids={%s} icon=dialog-question-symbolic(%s); tour=%s steps=%d "
|
"contextual=%s hints=%d ids={%s} icon=dialog-information-symbolic(%s) hint=%dx%d; "
|
||||||
"step=%d auto=%d requested=%d",
|
"tour=%s steps=%d step=%d auto=%d requested=%d",
|
||||||
app.settings.contextual_help ? "on" : "off", hints, ids.empty() ? "" : ids.c_str(),
|
app.settings.contextual_help ? "on" : "off", hints, ids.empty() ? "" : ids.c_str(),
|
||||||
icon_ok ? "ok" : "MISSING", app.tour_running ? "running" : "idle",
|
icon_ok ? "ok" : "MISSING", hint_w, hint_h,
|
||||||
tour_step_count(app), tour_step_index(app), app.tour_auto_started ? 1 : 0,
|
app.tour.running ? "running" : "idle", tour_step_count(app), tour_step_index(app),
|
||||||
app.tour_requested ? 1 : 0);
|
app.tour_auto_started ? 1 : 0, app.tour_requested ? 1 : 0);
|
||||||
app.dbg_context_help = layer;
|
app.dbg_context_help = layer;
|
||||||
app.dbg_tour_steps = tour_step_count(app);
|
app.dbg_tour_steps = tour_step_count(app);
|
||||||
app.dbg_tour_step = tour_step_index(app);
|
app.dbg_tour_step = tour_step_index(app);
|
||||||
app.dbg_tour_visible = app.tour_running ? 1 : 0;
|
app.dbg_tour_visible = app.tour.running ? 1 : 0;
|
||||||
|
|
||||||
std::string figures;
|
std::string figures;
|
||||||
for (int i = 0; i < kFigureSurfaceCount; ++i) {
|
for (int i = 0; i < kFigureSurfaceCount; ++i) {
|
||||||
|
|||||||
+7
-1
@@ -530,7 +530,10 @@ GtkWidget* build_trigger_panel(App& app) {
|
|||||||
row_for(ui, "level_row", app.trigger_level_spin);
|
row_for(ui, "level_row", app.trigger_level_spin);
|
||||||
|
|
||||||
app.trigger_pre_spin = spin(0.0, 99.0, 1.0, 0, 50.0);
|
app.trigger_pre_spin = spin(0.0, 99.0, 1.0, 0, 50.0);
|
||||||
row_for(ui, "pre_row", app.trigger_pre_spin);
|
// The row is kept, not just filled: the contextual help's "trigger_advanced" hint joins it, beside
|
||||||
|
// the pre-trigger share -- the panel's finer settings, which is what the ask calls the trigger's
|
||||||
|
// advanced ones.
|
||||||
|
app.trigger_pre_row = row_for(ui, "pre_row", app.trigger_pre_spin);
|
||||||
|
|
||||||
app.trigger_holdoff_spin = spin(0.0, 3600.0, 0.001, 4,
|
app.trigger_holdoff_spin = spin(0.0, 3600.0, 0.001, 4,
|
||||||
app.model->config().trigger_holdoff_s);
|
app.model->config().trigger_holdoff_s);
|
||||||
@@ -564,6 +567,9 @@ GtkWidget* build_trigger_panel(App& app) {
|
|||||||
gtk_widget_set_halign(app.events_record_switch, GTK_ALIGN_END);
|
gtk_widget_set_halign(app.events_record_switch, GTK_ALIGN_END);
|
||||||
gtk_widget_set_valign(app.events_record_switch, GTK_ALIGN_CENTER);
|
gtk_widget_set_valign(app.events_record_switch, GTK_ALIGN_CENTER);
|
||||||
gtk_box_append(GTK_BOX(record_row), app.events_record_switch);
|
gtk_box_append(GTK_BOX(record_row), app.events_record_switch);
|
||||||
|
// Where the contextual help's "events_record" hint joins -- beside the switch, never over it
|
||||||
|
// (see `ui_context_help.cpp`). Kept on the App because the layer's table names a field.
|
||||||
|
app.trigger_record_row = record_row;
|
||||||
}
|
}
|
||||||
|
|
||||||
app.arm_button = GTK_WIDGET(gtk_builder_get_object(ui, "trigger_arm"));
|
app.arm_button = GTK_WIDGET(gtk_builder_get_object(ui, "trigger_arm"));
|
||||||
|
|||||||
+14
-1
@@ -68,7 +68,20 @@ void install_css() {
|
|||||||
".udpscope-tour-dim { background-color: rgba(0, 0, 0, 0.45); }\n"
|
".udpscope-tour-dim { background-color: rgba(0, 0, 0, 0.45); }\n"
|
||||||
".udpscope-tour-spot { border: 2px solid @accent_bg_color; border-radius: 8px; }\n"
|
".udpscope-tour-spot { border: 2px solid @accent_bg_color; border-radius: 8px; }\n"
|
||||||
".udpscope-tour-card { background-color: @window_bg_color; border: 1px solid @borders;"
|
".udpscope-tour-card { background-color: @window_bg_color; border: 1px solid @borders;"
|
||||||
" border-radius: 12px; padding: 12px; box-shadow: 0 3px 12px rgba(0, 0, 0, 0.45); }\n";
|
" border-radius: 12px; padding: 12px; box-shadow: 0 3px 12px rgba(0, 0, 0, 0.45); }\n"
|
||||||
|
// The contextual-help "(i)": a *mark*, not a button. Adwaita's menu button is a 34x34 control
|
||||||
|
// (a 24 px floor plus 5 px of padding) with a 16 px glyph, which beside a heading reads as a
|
||||||
|
// second toolbar button -- the size the ask calls too big. Measured on libadwaita 1.10: the
|
||||||
|
// outer `menubutton` node *and* its `button` subnode both carry a floor and padding, and a class
|
||||||
|
// selector on either alone leaves the other at 34 (`min-width` on the widget itself is ignored
|
||||||
|
// by `GtkMenuButton`'s measure, and so is a size request -- tried both). Zeroing all three --
|
||||||
|
// the outer node, the subnode and its glyph -- is what brings it to **16x16**: the icon's own
|
||||||
|
// size and nothing else, because a `min-width` is a floor, not a cap, so 16 px is where a 16 px
|
||||||
|
// Adwaita glyph stops. That is the reading `hint=WxH` in the dump prints.
|
||||||
|
"menubutton.udpscope-hint { min-width: 0; min-height: 0; padding: 0; margin: 0; }\n"
|
||||||
|
"menubutton.udpscope-hint > button { min-width: 0; min-height: 0; padding: 0; }\n"
|
||||||
|
"menubutton.udpscope-hint > button > image { min-width: 12px; min-height: 12px;"
|
||||||
|
" -gtk-icon-size: 12px; }\n";
|
||||||
GtkCssProvider* p = gtk_css_provider_new();
|
GtkCssProvider* p = gtk_css_provider_new();
|
||||||
gtk_css_provider_load_from_string(p, kCss);
|
gtk_css_provider_load_from_string(p, kCss);
|
||||||
gtk_style_context_add_provider_for_display(gdk_display_get_default(),
|
gtk_style_context_add_provider_for_display(gdk_display_get_default(),
|
||||||
|
|||||||
Reference in new issue
Block a user