Feature/source plugins #1

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

No files matched your search

+4 -2
View File
@@ -2915,9 +2915,11 @@ now — they are listed so they are not mistaken for anything else:
unlocked, because configuring the host is what a first run is for. Each pane's heading also carries its **own switch**, the same control as the unlocked, because configuring the host is what a first run is for. Each pane's heading also carries its **own switch**, the same control as the
header bar's button — clicking either moves both — because a pane that is *disabled* while its header bar's button — clicking either moves both — because a pane that is *disabled* while its
feature is off is a pane whose controls you would otherwise have to leave to turn it back on. The feature is off is a pane whose controls you would otherwise have to leave to turn it back on. The
switcher's policy is `wide` and each page has a title and no icon (the switcher's own placeholder icon is hidden). With the math sidebar is one **dock** now, with no tab switcher: the signal list, the cursors and the measurements
in one scroller, shown from the start and insensitive until the stream has signals; `m` and `g`
scroll it to the cursors and back to the signal list. With the math
list folded into the signal list under a `MATH` divider whose `+` creates a channel — a derived channel already *is* a channel in list folded into the signal list under a `MATH` divider whose `+` creates a channel — a derived channel already *is* a channel in
the model, so the two lists were the same list drawn twice. A tab is one-of-*N*, so no the model, so the two lists were the same list drawn twice. No
toggle hides a pane any more: cursors-off **disables** its readout and leaves it in place, and toggle hides a pane any more: cursors-off **disables** its readout and leaves it in place, and
trigger-off leaves the trigger panel fully editable — a trigger is configured *before* it is trigger-off leaves the trigger panel fully editable — a trigger is configured *before* it is
switched on, and the locked view says `stream (trigger off)` instead of the settings — and the measurements' and math panes' toolbar toggles are gone with the panes. switched on, and the locked view says `stream (trigger off)` instead of the settings — and the measurements' and math panes' toolbar toggles are gone with the panes.
+1 -1
View File
@@ -36,7 +36,7 @@ What feeds `Acquisition` is a `DataSource`, one of the registered *kinds*.
`src/source/udps_*.cpp` (`grep -rn "udps_" src` outside those files is a leak of the old coupling). `src/source/udps_*.cpp` (`grep -rn "udps_" src` outside those files is a leak of the old coupling).
- The frame is **single-timebase**. A multi-timebase source (ABScope) will change the interface and - The frame is **single-timebase**. A multi-timebase source (ABScope) will change the interface and
the data path together; do not bend one frame to carry two clocks. the data path together; do not bend one frame to carry two clocks.
- A kind's capability panels are pages of `side_stack`, named `source-<id>`. A disconnected source - A kind's capability panels are pages of the slide-over (`slideover_add_page`), named `source-<id>`. A disconnected source
leaves them **insensitive, not hidden** (`apply_source_panels` in `src/ui_source.cpp`). leaves them **insensitive, not hidden** (`apply_source_panels` in `src/ui_source.cpp`).
- Session file naming for a non-UDPS kind: the pseudo-host is `<kind>-<identity>` (characters outside - Session file naming for a non-UDPS kind: the pseudo-host is `<kind>-<identity>` (characters outside
`[A-Za-z0-9._-]` become `_`) on port **1** (0 is rejected by the session-file parser), the kind is `[A-Za-z0-9._-]` become `_`) on port **1** (0 is rejected by the session-file parser), the kind is
+20 -26
View File
@@ -50,7 +50,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
runs after the window is destroyed and asking GTK about a widget then is four criticals, which is runs after the window is destroyed and asking GTK about a widget then is four criticals, which is
how the first version of that line earned them. What stays in the menu is what acts on the *data* how the first version of that line earned them. What stays in the menu is what acts on the *data*
(Fit, Export, Configurations) and the one editor that is not a pane (the layout editor): the signal list (Fit, Export, Configurations) and the one editor that is not a pane (the layout editor): the signal list
is a *tab* of the sidebar, so there is nothing left to reveal or hide about it. The Arm/Disarm button in the trigger panel still has text. is a section of the sidebar's dock, so there is nothing left to reveal or hide about it. The Arm/Disarm button in the trigger panel still has text.
- **Five of this app's icons are its own, and they are a `GResource` compiled into the - **Five of this app's icons are its own, and they are a `GResource` compiled into the
binary.** No icon theme has a glyph for an oscilloscope trigger, a pair of cursors or a binary.** No icon theme has a glyph for an oscilloscope trigger, a pair of cursors or a
measurement ruler, and the stock names those buttons used to carry said something else measurement ruler, and the stock names those buttons used to carry said something else
@@ -61,9 +61,9 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
an icon with a live user is not an orphan, whatever the toggle that introduced it did -- and it is an icon with a live user is not an orphan, whatever the toggle that introduced it did -- and it is
the Preferences window's icon. the Preferences window's icon.
The sidebar's **tabs have no icons any more**: two glyphs (`udpscope-tab-measure-symbolic`, The sidebar has **no tabs any more** (it is one dock): two tab glyphs (`udpscope-tab-measure-symbolic`,
`udpscope-tab-signals-symbolic`, each a 32x16 pair) were drawn for them and judged not understandable `udpscope-tab-signals-symbolic`, each a 32x16 pair) were drawn for them and judged not understandable
enough, so the tabs are text and both SVGs were deleted. The *trigger's* own icon was redrawn to the enough, so the tabs were text, and both SVGs were deleted. The *trigger's* own icon was redrawn to the
Signals glyph's picture at that time and kept it. Signals glyph's picture at that time and kept it.
- **A shortcut table row's `accel` is GTK accelerator syntax, and a pointer gesture has - **A shortcut table row's `accel` is GTK accelerator syntax, and a pointer gesture has
none.** `src/shortcuts.cpp`'s strings are handed to `AdwShortcutsItem`, which parses them none.** `src/shortcuts.cpp`'s strings are handed to `AdwShortcutsItem`, which parses them
@@ -229,11 +229,13 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
instead of across. `gtk_widget_set_visible()` is what a layout honours, so anything that must not instead of across. `gtk_widget_set_visible()` is what a layout honours, so anything that must not
take room has to be hidden and not merely unrevealed; the revealer keeps the slide while the pane take room has to be hidden and not merely unrevealed; the revealer keeps the slide while the pane
*is* open. The allocated width is how this was found -- the revealer's own measure said 0. *is* open. The allocated width is how this was found -- the revealer's own measure said 0.
**The sidebar is a tab stack now, and has no `GtkRevealer` left in it**: an `AdwViewSwitcher` **The sidebar is one dock now, and has no `GtkRevealer` left in it**: a single `GtkScrolledWindow`
above an `AdwViewStack`, each page a `GtkScrolledWindow` of its own (the two pages are different (`dock_scroller`) holding the signal list, the cursors and the measurements one under another. It
heights, so one shared scroller would leave a switch scrolled where the new page never asked to replaced an `AdwViewSwitcher` over an `AdwViewStack`, whose four text tabs did not fit 320 px (and a source
be). The reason is the one this trap teaches: five sections competing for one 320 px column left can add more); a source kind's own panels are slide-over pages, not dock sections. The reason is the one
every one of them short of room. A *feature's* on/off is no longer a visibility at all -- the this trap teaches: sections competing for one 320 px column each left every one of them short of room, and
a scroller gives each its natural height. `m` and `g` scroll the dock to the cursors and to the signal list
(`dock_show_section()`). A *feature's* on/off is no longer a visibility at all -- the
**cursors'** toggle sets its pane's **body** insensitive -- the readout has nothing to show with the **cursors'** toggle sets its pane's **body** insensitive -- the readout has nothing to show with the
instruments off -- and the *body* and not the pane, because the pane's heading carries its own switch instruments off -- and the *body* and not the pane, because the pane's heading carries its own switch
(see the next bullet). The **trigger's body is deliberately never disabled**: a trigger is configured (see the next bullet). The **trigger's body is deliberately never disabled**: a trigger is configured
@@ -241,23 +243,15 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
ordinary way to work. It used to go insensitive with the mode, which left the settings unreachable ordinary way to work. It used to go insensitive with the mode, which left the settings unreachable
exactly when a user wanted to change them; `trigger panel sensitive …=1` is the regression guard, and a exactly when a user wanted to change them; `trigger panel sensitive …=1` is the regression guard, and a
`0` there means the configuration has been locked out again. The `0` there means the configuration has been locked out again. The
trap itself does not go away with the widget: a stack's hidden page measures 0x0 too and is still trap itself does not go away with the widget: a section scrolled out of view is still laid out, but
given the leftover width, so **ask about state, not about size** carries over unchanged. Every **ask about state, not about size** still holds for anything a tick reads.
allocation read in the sidebar is a debug capture, and each reads 0 on the page that is *not* - **The dock is shown but insensitive until the stream has signals ("disable, do not hide").**
current -- a change in what the reading means, not a defect, and no functional code may depend on `update_side_visibility()` and the tick's per-frame block set the column's visibility from the user's wish
those numbers. alone (`sidebar_wanted`) and the dock's *sensitivity* from `have_signals`; the toolbar's Dock button stays
- **The sidebar's tabs are text: `AdwViewSwitcherPolicy.wide`, a title on every page, no icon.** The sensitive throughout. The wish is persisted (`dock = true|false` in the session file) and a change of it
built-in pages get their titles in the `.ui` and a source kind's pages from `SourcePanel::title` schedules a save. There is no tab icon to keep clear any more: the `keep_tabs_icon_free()` workaround for
(`adw_view_stack_add_titled`); the page's *name* still identifies it to `select_side_tab()` and the `AdwViewSwitcherButton`'s placeholder `[x]` image went with the switcher, and `dock:` in the dump
session file. An icon-only version existed (policy `narrow`, empty titles, the pills named in code by replaces the `switcher:` line.
`name_side_tabs()`, because `narrow` still draws a label beside an icon); it was reversed because the
glyphs were not understandable. A page added with an empty title is a blank pill, and
`switcher: … pills=N labels=N images=0 pages=N` reads it as `labels` below `pills`. **No icon at all**
is not what "no `icon-name`" gives you: `AdwViewSwitcherButton` always builds a `GtkImage` and falls
back to `image-missing` (the `[x]` placeholder, which also squeezed the title to "Meas..."), and no
property turns it off. `keep_tabs_icon_free()` (`src/ui_source.cpp`) hides every image in the switcher,
re-hides one that is shown again, and re-runs on the stack's `items-changed` (connected *after* the
switcher's own, so the new button exists) and on the switcher's `map`; `images=0` is the reading.
- **An *overlaid* sidebar covers the plot, so a drag out of it has nowhere to land.** `collapsed` in an - **An *overlaid* sidebar covers the plot, so a drag out of it has nowhere to land.** `collapsed` in an
`AdwOverlaySplitView` means the sidebar sits *on* the plot rather than beside it: the pointer is over the `AdwOverlaySplitView` means the sidebar sits *on* the plot rather than beside it: the pointer is over the
list, never over a drop target, and a signal can be dragged but not dropped. `row_drag_sidebar()` hides list, never over a drop target, and a signal can be dragged but not dropped. `row_drag_sidebar()` hides
@@ -278,7 +272,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
`sync_panel_switches()` re-reads each pair from the toolbar's state after any change — so clicking `sync_panel_switches()` re-reads each pair from the toolbar's state after any change — so clicking
either end moves both, and `panel switches: trigger=1/1 cursors=1/1` is the reading. Two traps: either end moves both, and `panel switches: trigger=1/1 cursors=1/1` is the reading. Two traps:
a `GtkSwitch` emits `notify::active` for a *programmatic* write as well, so the sync holds a guard a `GtkSwitch` emits `notify::active` for a *programmatic* write as well, so the sync holds a guard
(`App::panel_switch_sync`) or the program's own write comes back as a click; and the switcher must (`App::panel_switch_sync`) or the program's own write comes back as a click; and the switch must
stay *out* of what the pane disables, because a control inside an insensitive parent cannot be stay *out* of what the pane disables, because a control inside an insensitive parent cannot be
clicked — a switch that greyed out with its own pane could never turn it back on. That is why the clicked — a switch that greyed out with its own pane could never turn it back on. That is why the
sensitive target is the pane's **body** (`trigger_body`, `cursors_body`) and not the pane. sensitive target is the pane's **body** (`trigger_body`, `cursors_body`) and not the pane.
+3 -2
View File
@@ -23,10 +23,11 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
toggle is the same mistake in one line, and it cannot be told from a real change by a "is this our own toggle is the same mistake in one line, and it cannot be told from a real change by a "is this our own
write" flag either, since the toggle's signal may fire after the write returns. Instead `App::sidebar_wanted` write" flag either, since the toggle's signal may fire after the write returns. Instead `App::sidebar_wanted`
holds the wish (written only by `on_sidebar_toggled`, guarded by `App::sidebar_syncing` around our own holds the wish (written only by `on_sidebar_toggled`, guarded by `App::sidebar_syncing` around our own
`set_active`), `update_side_visibility()` is the single place that turns that wish plus "are there signals" `set_active`), `update_side_visibility()` is the single place that turns that wish into the panel's visibility
into the panel's *and* the button's state together, and `tick()` re-asserts them when the widget disagrees — and "are there signals" into the dock's *sensitivity* (the button stays sensitive), and `tick()` re-asserts both when the widget disagrees —
which is also why a hide *gesture* does not stick: the panel stays where the button says it is. The which is also why a hide *gesture* does not stick: the panel stays where the button says it is. The
no-signals case must leave the wish alone, or a source connecting later finds a sidebar the user never closed. no-signals case must leave the wish alone, or a source connecting later finds a sidebar the user never closed.
The wish is the session's `dock` key: a change of it (not a program's own write, and not a restore) schedules a save.
- **`AdwOverlaySplitView:sidebar-width-fraction` is a 0..1 fraction of the window, whatever - **`AdwOverlaySplitView:sidebar-width-fraction` is a 0..1 fraction of the window, whatever
`sidebar-width-unit` says.** Setting a pixel count there (320) is a `GObject` critical, and it silently `sidebar-width-unit` says.** Setting a pixel count there (320) is a `GObject` critical, and it silently
falls back to 0.25 — which reads as "the sidebar is mysteriously 560 px wide" on a wide window with no falls back to 0.25 — which reads as "the sidebar is mysteriously 560 px wide" on a wide window with no
+4 -1
View File
@@ -30,7 +30,10 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
not freeze a colour that is meant to follow the signal list), **the math channels with their not freeze a colour that is meant to follow the signal list), **the math channels with their
graphs**, **the measurements pane**, and **`measure_stats`** — the measurements statistics graphs**, **the measurements pane**, and **`measure_stats`** — the measurements statistics
toggle; written as a bare top-level boolean (`measure_stats = true/false`), defaults to `false` toggle; written as a bare top-level boolean (`measure_stats = true/false`), defaults to `false`
when absent. Signal identity when absent, and **`dock`** -- the Dock button's state (the user's last word on it, `sidebar_wanted`),
a bare top-level boolean written where the sidebar's `side_tab` used to be, default `true`. The parser
ignores unknown keys, so a file written before the tabs became a dock (`side_tab = "measure"`) still loads
and simply opens with the dock shown; nothing needs to read the old key. Signal identity
is the *name* throughout (a CONFIG renumbers channels), 'enabled' is read off the channels the is the *name* throughout (a CONFIG renumbers channels), 'enabled' is read off the channels the
model built -- an enabled signal is exactly one that produced a channel -- and `apply_layout()` model built -- an enabled signal is exactly one that produced a channel -- and `apply_layout()`
installs the graphs **before** assigning the rows, since a row may hold a math channel's name and installs the graphs **before** assigning the rows, since a row may hold a math channel's name and
+61 -55
View File
@@ -479,22 +479,18 @@ Two lines of the self-check output are assertions in disguise:
substitutes a missing-image placeholder, the button still works, and every other line of the substitutes a missing-image placeholder, the button still works, and every other line of the
dump still passes. Changing an icon name is a one-line change in `build_toolbar()` and this dump still passes. Changing an icon name is a one-line change in `build_toolbar()` and this
is the only check that it names something that exists. is the only check that it names something that exists.
- `switcher: policy=wide pills=4 labels=4 images=0 pages=4 size=308x34` (in the exit dump, beside `panels:`) — the - `dock: shown=1 sensitive=1 w=320 scroller_pages=1` (in the exit dump, beside `panels:`) -- the sidebar's dock
sidebar tabs as the switcher actually draws them, and they are **words**: `labels` counts *mapped labels as the last tick found it. It replaces the old `switcher:` line: there is no tab switcher, the sidebar is
with text*, so `pills=labels=pages` says every page has a visible title. **`images` must be `0`**: it one scroller (`dock_scroller`) holding the signal list, the cursors and the measurements. **`shown=1`
counts *mapped* `GtkImage`s inside the switcher. `AdwViewSwitcherButton` builds an icon in every tab, and always** (the dock is on screen from the start; `shown=0` only when the user hid it with the Dock button,
a page with a title and no `icon-name` gets `image-missing` -- the `[x]` placeholder; the tabs hide it which a probe does deliberately); **`sensitive=0` only before the stream has signals** ("disable, do not
(`keep_tabs_icon_free()`, which also covers pages `build_source_panels()` adds). A nonzero `images` hide") -- a run against a port with no sender reads `shown=1 sensitive=0`, a normal run `sensitive=1`;
names a tab still drawing an icon or its placeholder (`images=pages` is the unfixed state), and `w=` is the sidebar's own width (320); **`scroller_pages=1`** counts the scrollers directly under the dock
`pills`/`labels` never saw it. Read it on a source with extra pages (`--source probe` under column and says there is one scroller and no stack. These are captured on the tick or at the exit probe:
`UDPSCOPE_PROBE_SOURCE_KINDS`) as well as on UDPS. `pages` is read from the stack the dump runs after the window is destroyed. A source with extra panels adds *no* dock section: its panels
(2 for UDPS, 3 for the probe source, 4 for the E-1608 with its Analog out and Digital I/O tabs). A are slide-over pages (`source-<id>`), so the line reads the same on the E-1608 and the probe kind.
`labels` below `pills` is a page added with an empty title; `pills=0` means nothing is mapped, which
makes the rest of the line meaningless rather than a pass. `policy=wide` is read rather than assumed.
`size=` is the bar's own size: a text tab costs about 34 px of height, and a width of 308 shows the
four tabs fit the 320 px column.
- `help layer: contextual=on hints=12 ids={events plot status cursors measures trigger trigger_advanced events_record trigger_search signals signals_config window} icon=dialog-information-symbolic(ok) hint=12x12; tour=idle steps=5 step=0 auto=1 requested=0` (in the exit - `help layer: contextual=on hints=12 ids={events plot status cursors measures trigger trigger_advanced events_record trigger_search signals signals_config window} icon=dialog-information-symbolic(ok) hint=12x12; tour=idle steps=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 `dock:`) — the help affordance layer as the last tick found it. **`hints=` must equal
the number of ids**, and the ids are the surfaces the contextual help is on: eleven of the window the number of ids**, and the ids are the surfaces the contextual help is on: eleven of the window
itself, and `math` only while the math editor is open (its heading lives in a builder that is rebuilt itself, and `math` only while the math editor is open (its heading lives in a builder that is rebuilt
on every opening, so it is not one of the window's own). Four of the eleven are not a pane's *heading* on every opening, so it is not one of the window's own). Four of the eleven are not a pane's *heading*
@@ -538,7 +534,7 @@ Two lines of the self-check output are assertions in disguise:
[invariants-gtk-and-chrome.md](invariants-gtk-and-chrome.md)). The writer chains to [invariants-gtk-and-chrome.md](invariants-gtk-and-chrome.md)). The writer chains to
`g_log_writer_default()`, so every message still reaches stderr exactly as before and the documented `g_log_writer_default()`, so every message still reaches stderr exactly as before and the documented
baselines are read the same way. baselines are read the same way.
- `help figures: trigger_head=X,Y,W,H trigger_summary=… trigger_panel=… signal_list=… side_switcher=… side_stack=… window_button=… status_label=… header_bar=… plot=…` (in the exit - `help figures: trigger_head=X,Y,W,H trigger_summary=… trigger_panel=… signal_list=… side_stack=… window_button=… status_label=… header_bar=… plot=…` (in the exit
dump, beside `help layer:`) — the **window-coordinate** rectangle of each surface the help's annotated dump, beside `help layer:`) — the **window-coordinate** rectangle of each surface the help's annotated
figures label, `x,y,width,height`, or `-1,-1,0,0` for a surface that is **not on screen**. It is figures label, `x,y,width,height`, or `-1,-1,0,0` for a surface that is **not on screen**. It is
measured on the tick while the window is alive (nothing in the dump may call `gtk_widget_*` — see measured on the tick while the window is alive (nothing in the dump may call `gtk_widget_*` — see
@@ -919,9 +915,9 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
selectable into it (`gtk_widget_get_mapped()` -- a wrong *page* is kept, because selecting its selectable into it (`gtk_widget_get_mapped()` -- a wrong *page* is kept, because selecting its
page reveals it, while a *collapsed pane* is still dropped; see the surface bullets in page reveals it, while a *collapsed pane* is still dropped; see the surface bullets in
[invariants-gtk-and-chrome.md](invariants-gtk-and-chrome.md)). [invariants-gtk-and-chrome.md](invariants-gtk-and-chrome.md)).
- `[helpui] step=1/5 visible=1 side_tab=signals spot=[x,y WxH] target=[…] contains=1 card=[…] clear=C` - `[helpui] step=1/5 visible=1 slideover=none spot=[x,y WxH] target=[…] contains=1 card=[…] clear=C`
— **in window coordinates**, the frame a screenshot is cropped in, with the sidebar's page beside — **in window coordinates**, the frame a screenshot is cropped in, with the slide-over's open page
it (a stop the walk had to change the page for is readable as such; see `[toursurface]` below). (`none`, or a page id) beside it; see `[toursurface]` below.
**`contains` must be 1** on every step: the spotlight is the target's own rectangle grown three **`contains` must be 1** on every step: the spotlight is the target's own rectangle grown three
pixels. `clear` is the card missing the target, and it is 1 wherever the window has room for the pixels. `clear` is the card missing the target, and it is 1 wherever the window has room for the
card beside or above/below the hole — at the documented run's window (see `help figures:` below) card beside or above/below the hole — at the documented run's window (see `help figures:` below)
@@ -949,34 +945,30 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
read `mapped=0` (and `0x0`), which is exactly why the header box exists (`search_head` in read `mapped=0` (and `0x0`), which is exactly why the header box exists (`search_head` in
`src/ui/trigger-panel.ui`) and why the hint joins it as its last child. `mapped_header=` is the `src/ui/trigger-panel.ui`) and why the hint joins it as its last child. `mapped_header=` is the
header box beside it, and `hint=WxH` is the same 12x12 the layer's own line reads. header box beside it, and `hint=WxH` is the same 12x12 the layer's own line reads.
- `[toursurface] …` (stderr, `UDPSCOPE_PROBE_TOUR_SURFACE_MS`) — a stop that stands on a page it is - `[toursurface] …` (stderr, `UDPSCOPE_PROBE_TOUR_SURFACE_MS`) -- a stop that stands on a surface it is
not on, entered from the page it is not on. It puts the sidebar on **Measure**, starts the window's not on, entered from the state it is not in. Since the sidebar's tabs became the dock the probe opens the
tour, and walks it a stop per tick (400 ms), because a stack allocates the new page's widgets on `trigger` slide-over page instead of switching a tab, starts the window's tour and walks it a stop per tick
the frame *after* the switch — so each reading is of the page the step left standing, not of the (400 ms). **The tour's own rework for the slide-over is Task 5's**: until its page hook exists the tour
one the switch has just left behind. The lines: has no page stack, so a stop whose target is on a slide-over page is dropped, and the lines below read the
dock only. The lines:
- `[toursurface] before: side_tab=signals signal_list_mapped=1 running=1` — the state a first run - `[toursurface] before: slideover=none signal_list_mapped=1 running=1` -- the state a first run starts in
starts in (a fresh config's sidebar is on Signals), then the auto-started walk is dismissed. (the dock is on screen), then the auto-started walk is dismissed.
- `[toursurface] on measure: side_tab=measure signal_list_mapped=0 side_stack_mapped=1` — a frame - `[toursurface] on trigger: slideover=trigger signal_list_mapped=1 side_box_mapped=1` -- a frame after the
after the switch: **`signal_list_mapped=0` beside `side_stack_mapped=1`** is the whole premise, page was opened: the dock is still on screen under the slide-over (they are not alternatives).
the list is genuinely off screen while the stack that holds it is on screen. - `[toursurface] tour: visible=1 steps=N step=0 signals_stop=1` -- **`signals_stop=1` is the filter's
- `[toursurface] tour: visible=1 steps=5 step=0 signals_stop=1` — **`signals_stop=1` is the filter's reading**: the signal list is in the dock, which is mapped, so its stop is in the plan.
reading**: a stop whose target is *not mapped* is still in the plan because it names a page the - `[toursurface] step=N/M slideover=<page> target_mapped=D[ target=signal_list]` -- one per stop;
stack can select. `steps=5` is the full table. **the `signals` stop reads `target_mapped=1`**.
- `[toursurface] step=N/5 side_tab=<page> target_mapped=D[ target=signal_list]` — one per stop. The - `[toursurface] signals stop=N/M visible=1 slideover=… spot=[…] target=[…] contains=1 card=[…] clear=1` --
page moves from `measure` to `signals` at the `trigger` stop (the first stop whose target is on the `tour_geometry_line()` the helpui probe uses: **`contains=1` is the assertion** that the spotlight
the Signals page) and stays there; **the `signals` stop reads `target_mapped=1`**. and the card are on the list.
- `[toursurface] signals stop=4/5 visible=1 side_tab=signals spot=[…] target=[…] contains=1 - `[toursurface] collapsed: side_box_mapped=0 steps=2 signals_stop=0 side_stop=0` -- the other half of
card=[…] clear=1` — the `tour_geometry_line()` the helpui probe uses, read on the frame *after* the filter, read: with `sidebar_wanted` off (`update_side_visibility()` re-asserts it, so this is how a
the switch: **`side_tab=signals` with `contains=1` is the assertion** that the spotlight and the probe puts the sidebar away for good) the split view unmaps the column and the stops that need the
card are on the list, not on the page the walk came from. dock are dropped while the page-less ones stay. This is what says a *collapsed pane* is dropped.
- `[toursurface] collapsed: side_stack_mapped=0 steps=2 signals_stop=0 side_stop=0` — the other half - `[keys] a: armed 0->1 | space: paused 0->1 | t: trigger panel: 0->1 | c: cursors: 0->1 | m: dock 0->243 (cursors_y=243 reach=2256 at_cursors=1) | g: dock 243->0 at_top=1 | … | editable focused: 'm' refused=1` (stderr, `UDPSCOPE_PROBE_KEYS_MS`) — **every `X->Y` pair must differ** (`g` returns, so its pair reads `243->0`) and **`refused` must be 1**.
of the filter, read: with `sidebar_wanted` off (`update_side_visibility()` re-asserts it, so this The `m`/`g` readings are the dock scroller's vertical adjustment before and after (the probe pads the dock to 3000 px so there is something to scroll, and undoes it at the end): **`m` must land on `cursors_y`, the cursors panel's y in the dock (`at_cursors=1`), and `g` must return to 0 (`at_top=1`)**, which is what says those two keys scroll to a section rather than toggling a pane; a `reach` of 0 would mean the dock fit its viewport and the probe's padding was lost; `t`/`c` read the toolbar toggle's own state, because those two enable a feature and the button is where that state is visible. The refusal is the whole reason the controller is in the bubble phase: every bare-letter accel is also a character, so an accel that fires while a `GtkEditable` has focus makes the window entry unusable. A pair reading `0->0` is an action whose dispatch does not reach the handler a click reaches. Note the format is `<key>: <widget-description>: <before>-><after>` — the name passed to `before_after()` includes the widget's description, so `t: trigger panel: 0->1` is the correct form (not `t: trigger panel 0->1` without the colon before the values).
is how a probe puts the sidebar away for good) the split view unmaps the column, the stack with
it, and the two stops that need the sidebar are dropped while the page-less ones stay. This is
what says a *wrong page* and a *collapsed pane* are not the same case.
- `[keys] a: armed 0->1 | space: paused 0->1 | t: trigger panel: 0->1 | c: cursors: 0->1 | m: tab signals->measure | g: tab measure->signals | … | editable focused: 'm' refused=1` (stderr, `UDPSCOPE_PROBE_KEYS_MS`) — **every `X->Y` pair must differ** and **`refused` must be 1**.
The `m`/`g` pairs are the *tab name* before and after, which is what says those two keys select a tab rather than toggling a pane; `t`/`c` read the toolbar toggle's own state, because those two enable a feature and the button is where that state is visible. The refusal is the whole reason the controller is in the bubble phase: every bare-letter accel is also a character, so an accel that fires while a `GtkEditable` has focus makes the window entry unusable. A pair reading `0->0` is an action whose dispatch does not reach the handler a click reaches. Note the format is `<key>: <widget-description>: <before>-><after>` — the name passed to `before_after()` includes the widget's description, so `t: trigger panel: 0->1` is the correct form (not `t: trigger panel 0->1` without the colon before the values).
- `[color] '<name>' at row R slot S: trace=#RRGGBB chip_agrees=1 (want 1)`, then `[color] override: trace=#123456 chip_agrees=1`, `[color] session: entries=1 rgb=#123456` and `[color] default: trace=#RRGGBB chip_agrees=1` (stderr, `UDPSCOPE_PROBE_COLOR_MS`) — **all three `chip_agrees` must be 1**, the override line's `trace` must be `#123456`, the session line must read `entries=1 rgb=#123456`, and the default line's `trace` must be back to the first line's. Run it with `UDPSCOPE_ENABLE_ALL_MS` ahead of it, or there is no bound trace and the probe prints `no bound trace to recolour` instead of asserting anything; on the standard mock the first line reads `'sine' at row 0 slot 0: trace=#4FA3FF`. Two things make this an assertion rather than a tautology. The colours are read out of **`plot::Plot::trace().color`** — what the renderer will stroke, reached through `Row::plot` *after* `relayout_areas()`, since a new colour is a new figure — and out of the chip's own `<span foreground="…">` markup, not by asking `RowSet::signal_color()` twice, which would pass against a chip that never reached the trace. And the override is `#123456`, deliberately in no palette slot: an override equal to the entry it replaces passes against an implementation that ignores overrides entirely. The session leg round-trips `collect_layout()` → `save_layout()` → `load_layout()` on a temp path and deletes it, because a probe must not leave a colour in the real session file for a later run to inherit. - `[color] '<name>' at row R slot S: trace=#RRGGBB chip_agrees=1 (want 1)`, then `[color] override: trace=#123456 chip_agrees=1`, `[color] session: entries=1 rgb=#123456` and `[color] default: trace=#RRGGBB chip_agrees=1` (stderr, `UDPSCOPE_PROBE_COLOR_MS`) — **all three `chip_agrees` must be 1**, the override line's `trace` must be `#123456`, the session line must read `entries=1 rgb=#123456`, and the default line's `trace` must be back to the first line's. Run it with `UDPSCOPE_ENABLE_ALL_MS` ahead of it, or there is no bound trace and the probe prints `no bound trace to recolour` instead of asserting anything; on the standard mock the first line reads `'sine' at row 0 slot 0: trace=#4FA3FF`. Two things make this an assertion rather than a tautology. The colours are read out of **`plot::Plot::trace().color`** — what the renderer will stroke, reached through `Row::plot` *after* `relayout_areas()`, since a new colour is a new figure — and out of the chip's own `<span foreground="…">` markup, not by asking `RowSet::signal_color()` twice, which would pass against a chip that never reached the trace. And the override is `#123456`, deliberately in no palette slot: an override equal to the entry it replaces passes against an implementation that ignores overrides entirely. The session leg round-trips `collect_layout()` → `save_layout()` → `load_layout()` on a temp path and deletes it, because a probe must not leave a colour in the real session file for a later run to inherit.
- `[reseq-probe] reseq=N late=N stale=N overflows=N holes=N hole_slots=N backfilled=N - `[reseq-probe] reseq=N late=N stale=N overflows=N holes=N hole_slots=N backfilled=N
gaps_refused=N open_holes=N bands=N bands_repaired=N nan_slots=N scanned=N trigger_in_hole=D` gaps_refused=N open_holes=N bands=N bands_repaired=N nan_slots=N scanned=N trigger_in_hole=D`
@@ -1796,18 +1788,16 @@ and `regained` reads 400 ms later.
## `[side-tab-probe]` ## `[side-tab-probe]`
``` ```
[side-tab-probe] page=signals accums=4 baseline recorded [side-tab-probe] section=measure accums=4 baseline recorded (measure at scroll 243 of 2256)
[side-tab-probe] page=signals rows=4 accums=4 advanced=4 min_n=18 [side-tab-probe] section=signals rows=4 accums=4 advanced=4 min_n=18
``` ```
`UDPSCOPE_PROBE_SIDE_TAB_MS` is the one reading for the rule that **the statistics accumulator keeps running `UDPSCOPE_PROBE_SIDE_TAB_MS` (the variable and the tag keep their pre-dock names; the function is `dock_section_probe`) is the one reading for the rule that **the statistics accumulator keeps running
on the hidden page**. It switches *to* the Measure tab, forces one pass so there is a baseline, then switches while the Measure section is out of view**. It scrolls the dock *to* the Measure section (`dock_show_section("measure")`), forces one pass so there is a baseline, then scrolls back to Signals and reads again two seconds later. To make the scroll observable on a short window the probe pads `dock_page` to 3000 px (probe only), and it re-arms once after 400 ms so the first reading has the layout.
away to Signals and reads again two seconds later -- with the measurements' page not current, so its widgets
are unrealized and measure 0x0.
- **`advanced=4` is the assertion**: every accumulator gained passes while its page was off screen, and - **`advanced=4` is the assertion**: every accumulator gained passes while its section was out of view, and
`min_n` (the smallest `n` seen) must be non-zero too. **`advanced=0` is the defect**: something gated the `min_n` (the smallest `n` seen) must be non-zero too. **`advanced=0` is the defect**: something gated the
accumulation on the visible tab, so the readout would gain a gap in its min/max/avg for exactly as long as accumulation on the visible section, so the readout would gain a gap in its min/max/avg for exactly as long as
the trigger was on screen. A `Gtk-CRITICAL` here is the other half of the same rule -- a label or an the trigger was on screen. A `Gtk-CRITICAL` here is the other half of the same rule -- a label or an
allocation read on the unrealized page. allocation read on the unrealized page.
- The probe enables a signal **and assigns it to a plot** itself, both deliberately. A signal starts disabled - The probe enables a signal **and assigns it to a plot** itself, both deliberately. A signal starts disabled
@@ -1849,7 +1839,7 @@ kind, so a run without it never shows that kind. The `form` line appears once pe
`drained=0` is a queue that `poll()` does not drain on some path; `after` greater than `before + 1` is a `drained=0` is a queue that `poll()` does not drain on some path; `after` greater than `before + 1` is a
command run twice. command run twice.
The UDPS run has no `source-` page in `side_stack`, because the UDPS kind has no panels. The UDPS run has no `source-` slide-over page, because the UDPS kind has no panels.
## `[e1608]` ## `[e1608]`
@@ -2591,3 +2581,19 @@ The same probe then puts five measurements on the same spiked `sine` as **two wi
- **`measure windows=2`** -- the five legs ask for corners `0, 50, 0, 0, 50` on one channel, which is two - **`measure windows=2`** -- the five legs ask for corners `0, 50, 0, 0, 50` on one channel, which is two
distinct windows. **`windows=1` means the corner is not part of the key**: the five collapsed onto one walk distinct windows. **`windows=1` means the corner is not part of the key**: the five collapsed onto one walk
and two of them read a signal they did not ask for. and two of them read a signal they did not ask for.
## `[dock-session]`
```
[dock-session] hidden: wish=0 save_scheduled=1 collected=0 saved=1 loaded=1 read=0 applied=1 split_follows=1 ok=1
[dock-session] shown: wish=1 save_scheduled=1 collected=1 saved=1 loaded=1 read=1 applied=1 split_follows=1 ok=1
[dock-session] old file (side_tab only): loaded=1 dock_shown=1 default=1 ok=1
```
`UDPSCOPE_PROBE_DOCK_SESSION_MS` reads the one thing the dock persists, the Dock button's state, as `dock = true|false` in the
session file (it replaced `side_tab`). Each of the first two lines drives the toggle, then follows the wish through: `save_scheduled=1` is
`on_sidebar_toggled` having scheduled a save (a user's wish that was not saved is lost at the next start), `collected` is
`collect_layout()`'s `dock_shown`, `saved`/`loaded` the file round trip on a temp path, `read` the loaded value, `applied`
the wish after `apply_layout()`, and `split_follows=1` the split view following it. **`ok=1` on all three lines is the assertion.** The third line
loads a file as the previous version wrote it (`side_tab = "measure"`): it must load with no error and leave `dock_shown` at its default
(`true`) -- the parser ignores unknown keys, and this is what says the old key still does not break a session. The probe writes only to a temp path.
+15 -8
View File
@@ -412,12 +412,19 @@ precedent, not an exception.
pauses, reports again with `dn=` per measurement, then resets and reports a third time. Three pauses, reports again with `dn=` per measurement, then resets and reports a third time. Three
phases: `phase=0`, `phase=1` (after pause) and `phase=2` (after Reset). Needs phases: `phase=0`, `phase=1` (after pause) and `phase=2` (after Reset). Needs
`UDPSCOPE_ENABLE_ALL_MS` ahead of it and `UDPSCOPE_PROBE_MEASURE_MS` to add measurements first. `UDPSCOPE_ENABLE_ALL_MS` ahead of it and `UDPSCOPE_PROBE_MEASURE_MS` to add measurements first.
- `UDPSCOPE_PROBE_SIDE_TAB_MS=3000` with `UDPSCOPE_EXIT_AFTER_MS=11000` — the measurements on the - `UDPSCOPE_PROBE_SIDE_TAB_MS=3000` with `UDPSCOPE_EXIT_AFTER_MS=11000` — the measurements in the
Measure tab, a forced pass, then the tab switched away and read again two seconds later. **`advanced=4` dock's Measure section, a forced pass, then the dock scrolled back to the signal list (the Measure
is the assertion** (every accumulator gained passes while its page was off screen); `advanced=0` is the section out of view) and read again two seconds later. The probe pads the dock to 3000 px so a scroll is
defect where the accumulation is gated on the visible tab, which would leave a gap in min/max/avg for observable on a short window. **`advanced=4`
is the assertion** (every accumulator gained passes while its section was out of view); `advanced=0` is the
defect where the accumulation is gated on the visible section, which would leave a gap in min/max/avg for
exactly as long as the trigger was on screen. It enables *and plots* its own signal, so it needs no other exactly as long as the trigger was on screen. It enables *and plots* its own signal, so it needs no other
probe ahead of it. The reading's own entry is in `probe-readings.md`. probe ahead of it. The reading's own entry is in `probe-readings.md`. (Probe name `dock_section_probe`; the
variable and the `[side-tab-probe]` tag keep their old names so existing scripts keep working.)
- `UDPSCOPE_PROBE_DOCK_SESSION_MS=2500` with `UDPSCOPE_EXIT_AFTER_MS=8000` -- the session file's `dock` key,
end to end: the dock hidden, saved and reloaded; shown again; and a file from the previous version
(`side_tab = "measure"`, no `dock`) loaded without an error. Three `[dock-session]` lines, each `ok=1`;
entries in `probe-readings.md`.
- `UDPSCOPE_PROBE_SLIDEOVER_MS=1500` with `UDPSCOPE_EXIT_AFTER_MS=5000` (and `UDPSCOPE_ENABLE_ALL_MS=500`) -- the - `UDPSCOPE_PROBE_SLIDEOVER_MS=1500` with `UDPSCOPE_EXIT_AFTER_MS=5000` (and `UDPSCOPE_ENABLE_ALL_MS=500`) -- the
slide-over panel over the plot's right edge, opened on a throw-away `probe` page, read 0.5 s later. Two slide-over panel over the plot's right edge, opened on a throw-away `probe` page, read 0.5 s later. Two
`[slideover]` lines. Line 1 must read `panel=360x<overlay height> ... edge_flush=1 full_height=1 `[slideover]` lines. Line 1 must read `panel=360x<overlay height> ... edge_flush=1 full_height=1
@@ -631,12 +638,12 @@ as distinct from the fault injection below, which exercises ingest:
bullet) — so a probe run against a fresh config directory will have it on screen for the first 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 second and a half, and that start *writes* `tour_seen` to whichever config directory the run is
using, which is why the second run against the same one reads `auto=0`. using, which is why the second run against the same one reads `auto=0`.
- `UDPSCOPE_PROBE_TOUR_SURFACE_MS` — the tour's *surface* switch: it puts the sidebar on **Measure**, - `UDPSCOPE_PROBE_TOUR_SURFACE_MS` — the tour's *surface* switch: it opens a slide-over page (`trigger`),
starts the window's tour, and walks it a stop per tick to the signals stop, reading the sidebar's starts the window's tour, and walks it a stop per tick to the signals stop, reading the sidebar's
page and the spotlight/card where the walk arrives. It then puts the sidebar *away* and starts the page and the spotlight/card where the walk arrives. It then puts the sidebar *away* and starts the
tour again, which is the same filter's other case (a collapsed pane drops the stops a page switch tour again, which is the same filter's other case (a collapsed pane drops the stops a page switch
could have revealed). `[toursurface] signals stop=4/5 … side_tab=signals … contains=1` and could have revealed). `[toursurface] signals stop=4/5 … slideover=none … contains=1` and
`[toursurface] collapsed: side_stack_mapped=0 steps=2 signals_stop=0` are the two assertions; its `[toursurface] collapsed: side_box_mapped=0 steps=2 signals_stop=0` are the two assertions; its
lines are in [probe-readings.md](probe-readings.md). Run it alone with lines are in [probe-readings.md](probe-readings.md). Run it alone with
`UDPSCOPE_EXIT_AFTER_MS=9000 UDPSCOPE_EXIT_DUMP=1` against a live source. `UDPSCOPE_EXIT_AFTER_MS=9000 UDPSCOPE_EXIT_DUMP=1` against a live source.
- `UDPSCOPE_PROBE_DEFAULTS_MS` — opens **Calibrate** and reads every spin button in it and in the - `UDPSCOPE_PROBE_DEFAULTS_MS` — opens **Calibrate** and reads every spin button in it and in the
+3 -3
View File
@@ -568,8 +568,8 @@ bool load_layout(const std::string& path, LayoutState& out) {
} }
} else if (key == "measure_stats") { } else if (key == "measure_stats") {
st.measure_stats = value == "true"; st.measure_stats = value == "true";
} else if (key == "side_tab") { } else if (key == "dock") {
st.side_tab = strip_quote(value); st.dock_shown = value == "true";
} else if (key.rfind("trigger_", 0) == 0) { } else if (key.rfind("trigger_", 0) == 0) {
trigger_key(st.trigger, key.substr(8), strip_quote(value)); trigger_key(st.trigger, key.substr(8), strip_quote(value));
} else if (key == "multicast") { } else if (key == "multicast") {
@@ -740,7 +740,7 @@ bool save_layout(const std::string& path, const LayoutState& state) {
o << "]\n"; o << "]\n";
} }
o << "measure_stats = " << (state.measure_stats ? "true" : "false") << "\n"; o << "measure_stats = " << (state.measure_stats ? "true" : "false") << "\n";
o << "side_tab = \"" << state.side_tab << "\"\n"; o << "dock = " << (state.dock_shown ? "true" : "false") << "\n";
// The trigger, settings and state both. Written even when it is off: that is the point of it. // The trigger, settings and state both. Written even when it is off: that is the point of it.
o << "trigger_enabled = " << (state.trigger.enabled ? "true" : "false") << "\n"; o << "trigger_enabled = " << (state.trigger.enabled ? "true" : "false") << "\n";
if (!state.trigger.channel.empty()) { if (!state.trigger.channel.empty()) {
+4 -3
View File
@@ -73,9 +73,10 @@ struct LayoutState {
/// stream and mean nothing restored against another one. /// stream and mean nothing restored against another one.
bool measure_stats = false; bool measure_stats = false;
/// Which sidebar tab was showing: "measure" or "signals". The one thing the panel persists -- /// The dock's shown state (the user's last word on the toolbar's Dock button). The one thing the dock
/// no revealer states remain, and the 320 px width is the `.ui`'s. /// persists -- its 320 px width is the `.ui`'s. An older file's `side_tab` key is not read: the parser
std::string side_tab = "signals"; /// ignores keys it does not know.
bool dock_shown = true;
/// The measurements pane, by signal *name* and with the type and options a row was built with. /// The measurements pane, by signal *name* and with the type and options a row was built with.
std::vector<Measure> measures; std::vector<Measure> measures;
+2 -2
View File
@@ -14,8 +14,8 @@ constexpr Accel kAccels[] = {
{GDK_KEY_a, 0, AppAction::TriggerArm, "a", "Arm or disarm the trigger", "Acquisition"}, {GDK_KEY_a, 0, AppAction::TriggerArm, "a", "Arm or disarm the trigger", "Acquisition"},
{GDK_KEY_t, 0, AppAction::TriggerPanel, "t", "Enable or disable trigger mode", "Acquisition"}, {GDK_KEY_t, 0, AppAction::TriggerPanel, "t", "Enable or disable trigger mode", "Acquisition"},
{GDK_KEY_c, 0, AppAction::CursorsPane, "c", "Enable or disable the cursors", "Panes and editors"}, {GDK_KEY_c, 0, AppAction::CursorsPane, "c", "Enable or disable the cursors", "Panes and editors"},
{GDK_KEY_m, 0, AppAction::SideTabMeasure, "m", "Show the Measure tab", "Panes and editors"}, {GDK_KEY_m, 0, AppAction::ShowMeasure, "m", "Go to Measure", "Panes and editors"},
{GDK_KEY_g, 0, AppAction::SideTabSignals, "g", "Show the Signals tab", "Panes and editors"}, {GDK_KEY_g, 0, AppAction::ShowSignals, "g", "Go to Signals", "Panes and editors"},
{GDK_KEY_l, 0, AppAction::LayoutEditor, "l", "Show or hide the layout editor", {GDK_KEY_l, 0, AppAction::LayoutEditor, "l", "Show or hide the layout editor",
"Panes and editors"}, "Panes and editors"},
{GDK_KEY_s, 0, AppAction::SidePanel, "s", "Show or hide the side panel", {GDK_KEY_s, 0, AppAction::SidePanel, "s", "Show or hide the side panel",
+2 -2
View File
@@ -10,8 +10,8 @@ enum class AppAction {
TriggerArm, TriggerArm,
TriggerPanel, TriggerPanel,
CursorsPane, CursorsPane,
SideTabMeasure, ShowMeasure,
SideTabSignals, ShowSignals,
LayoutEditor, LayoutEditor,
SidePanel, SidePanel,
Export, Export,
+79 -109
View File
@@ -2864,35 +2864,38 @@ void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer
open_row_menu(*app, row, x, y, widget); open_row_menu(*app, row, x, y, widget);
} }
void on_side_tab_changed(GObject* o, GParamSpec*, gpointer user) { /// Scrolls the dock to a section. The dock is one scroller (`App::dock_scroller`) holding the signal list,
App* app = static_cast<App*>(user); /// the cursors and the measurements top to bottom, so "measure" is the cursors panel's offset in the
const char* name = adw_view_stack_get_visible_child_name(ADW_VIEW_STACK(o)); /// page, and "signals" the top. Nothing is hidden or removed: both stay in the page. An unknown name is a
app->side_tab = name != nullptr ? name : "signals"; /// no-op, so a key bound to a section a later version drops cannot blank anything.
// Not while the layout is being applied: the restore would schedule a save of what it just read. void dock_show_section(App& app, const char* section) {
if (!app->updating_panels) schedule_layout_save(*app); if (app.dock_scroller == nullptr || app.dock_page == nullptr || section == nullptr) return;
GtkAdjustment* adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(app.dock_scroller));
if (std::strcmp(section, "signals") == 0) {
// The list is the first section: its own offset in the page is the page's margin, and "go to Signals"
// means the top of the dock.
gtk_adjustment_set_value(adj, 0.0);
return;
}
if (std::strcmp(section, "measure") != 0 || app.cursors_panel == nullptr) return;
graphene_point_t zero = GRAPHENE_POINT_INIT(0.f, 0.f);
graphene_point_t at;
if (!gtk_widget_compute_point(app.cursors_panel, app.dock_page, &zero, &at)) return;
gtk_adjustment_set_value(adj, at.y);
} }
void select_side_tab(App& app, const char* name) { /// The dock is on when the user's sidebar button is in -- from the start, with or without signals; the
if (app.side_stack == nullptr || name == nullptr) return; /// two panel buttons appear as soon as a CONFIG describes any signal at all.
// An unknown name leaves the stack where it is -- a session file from a later version must not
// be able to blank the sidebar.
if (adw_view_stack_get_child_by_name(ADW_VIEW_STACK(app.side_stack), name) == nullptr) return;
adw_view_stack_set_visible_child_name(ADW_VIEW_STACK(app.side_stack), name);
app.side_tab = name;
}
/// The side column is on when it has something to show *and* the user's sidebar
/// button is in; the two panel buttons appear as soon as a CONFIG describes any
/// signal at all.
void update_side_visibility(App& app) { void update_side_visibility(App& app) {
if (app.side_stack == nullptr) return; if (app.side_box == nullptr) return;
const bool have_signals = !app.model->signals().empty(); const bool have_signals = !app.model->signals().empty();
// `have_signals` decides whether the sidebar *can* be on; `sidebar_wanted` is the user's last word, // `sidebar_wanted` is the user's last word, which is why it is kept apart from the button's own state:
// which is why it is kept apart from the button's own state -- that state follows the panel now, and // a swipe that hides the sidebar fires the button's handler (the binding is bidirectional), so a gesture
// the panel is off while there is nothing to show. A swipe that hides the sidebar fires the button's // is a choice too. "Disable, do not hide": the dock is on screen from the start and does nothing until
// handler (the binding is bidirectional), so a gesture is a choice too. // the stream has signals, so the signals decide its *sensitivity* and never whether it is there.
gtk_widget_set_sensitive(app.sidebar_toggle, have_signals); gtk_widget_set_sensitive(app.side_box, have_signals);
const bool want_column = have_signals && app.sidebar_wanted; gtk_widget_set_sensitive(app.sidebar_toggle, TRUE);
const bool want_column = app.sidebar_wanted;
// The split view owns the sidebar's visibility: it animates it, and when the window is too narrow it // The split view owns the sidebar's visibility: it animates it, and when the window is too narrow it
// shows it as an overlay rather than letting the column be squeezed to nothing. // shows it as an overlay rather than letting the column be squeezed to nothing.
if (app.side_view != nullptr) { if (app.side_view != nullptr) {
@@ -2908,7 +2911,12 @@ void on_sidebar_toggled(GtkToggleButton* button, gpointer user) {
App* app = static_cast<App*>(user); App* app = static_cast<App*>(user);
// Only a state we did not write is a wish: `update_side_visibility` sets this button (and the panel) // Only a state we did not write is a wish: `update_side_visibility` sets this button (and the panel)
// from `sidebar_wanted`, so an unguarded change here is the user's. // from `sidebar_wanted`, so an unguarded change here is the user's.
if (!app->sidebar_syncing) app->sidebar_wanted = gtk_toggle_button_get_active(button) != FALSE; if (!app->sidebar_syncing) {
app->sidebar_wanted = gtk_toggle_button_get_active(button) != FALSE;
// The wish is persisted (`SessionState::dock_shown`), so a change of it is a change of the session --
// but not one made while a layout is being applied, which would save what it just read.
if (!app->updating_panels) schedule_layout_save(*app);
}
update_side_visibility(*app); update_side_visibility(*app);
} }
@@ -2994,8 +3002,8 @@ void note_panel_controls(App& app) {
// `HeaderBar::pack_end()` parents into an internal box, so the bar is a level up, not the parent. // `HeaderBar::pack_end()` parents into an internal box, so the bar is a level up, not the parent.
char text[256]; char text[256];
std::snprintf(text, sizeof(text), std::snprintf(text, sizeof(text),
"cursors=%d in the toolbar, switcher=%d, reset=%d in the cursors pane", "cursors=%d in the toolbar, dock=%d, reset=%d in the cursors pane",
in_bar(app.cursors_toggle), app.side_switcher != nullptr ? 1 : 0, in_bar(app.cursors_toggle), app.dock_page != nullptr ? 1 : 0,
in_cursors_pane(app.cursors_reset)); in_cursors_pane(app.cursors_reset));
// ... and the order the bar actually shows, which is what "the menu should be last" is about. A // ... and the order the bar actually shows, which is what "the menu should be last" is about. A
// widget tree walked in child order *is* the layout order, so this is the reading order of the // widget tree walked in child order *is* the layout order, so this is the reading order of the
@@ -3051,42 +3059,6 @@ void note_panel_controls(App& app) {
" " + " " +
icon_report(theme, "org.udpscope.app-symbolic", icon_report(theme, "org.udpscope.app-symbolic",
"/org/udpscope/icons/scalable/apps/org.udpscope.app-symbolic.svg"); "/org/udpscope/icons/scalable/apps/org.udpscope.app-symbolic.svg");
// What the switcher is drawing: the sidebar's tabs are words (`wide` policy, a title per page, no
// icon). The pills are `AdwViewSwitcher`'s own buttons, built when the stack is mapped, so this
// reads on the first tick. `mapped`, not `visible`: visibility is per widget, so a label whose
// *parent* is hidden still reads visible; `mapped` requires every ancestor, which is what "on
// screen" means -- and it is why the pill count is read the same way: if nothing is mapped the
// pill count reads 0 and the label count means nothing, rather than passing vacuously.
int pills = 0;
int drawn_labels = 0;
int drawn_images = 0;
const std::function<void(GtkWidget*)> count_drawn = [&](GtkWidget* w) {
if (GTK_IS_TOGGLE_BUTTON(w) && gtk_widget_get_mapped(w)) ++pills;
// A *mapped* image is an icon (or its missing-icon placeholder) on a tab: tabs are words only.
if (GTK_IS_IMAGE(w) && gtk_widget_get_mapped(w)) ++drawn_images;
// A *mapped* label with text: an empty label would be counted by "mapped" alone.
if (GTK_IS_LABEL(w) && gtk_widget_get_mapped(w) && gtk_label_get_text(GTK_LABEL(w))[0] != '\0') {
++drawn_labels;
}
for (GtkWidget* c = gtk_widget_get_first_child(w); c != nullptr;
c = gtk_widget_get_next_sibling(c)) {
count_drawn(c);
}
};
if (app.side_switcher != nullptr) count_drawn(app.side_switcher);
app.dbg_switcher_pills = pills;
app.dbg_switcher_labels = drawn_labels;
app.dbg_switcher_images = drawn_images;
// Walked through `get_pages()`, not the widget tree: an `AdwViewStackPage` is a plain `GObject`.
app.dbg_switcher_pages =
app.side_stack != nullptr
? static_cast<int>(g_list_model_get_n_items(
G_LIST_MODEL(adw_view_stack_get_pages(ADW_VIEW_STACK(app.side_stack)))))
: 0;
if (app.side_switcher != nullptr) {
g_object_get(G_OBJECT(app.side_switcher), "policy", &app.dbg_switcher_policy, nullptr);
}
} }
/// The event-zoom sweep's two halves: a gesture zoom, then the same walk through the window /// The event-zoom sweep's two halves: a gesture zoom, then the same walk through the window
@@ -4022,7 +3994,7 @@ gboolean tick(gpointer user) {
// follow a resize and the gutter settling without being told. // follow a resize and the gutter settling without being told.
if (app->layout_editing) sync_layout_editor(*app); if (app->layout_editing) sync_layout_editor(*app);
// The one thing that has to be decided *per frame* about the sidebar: whether there is room for it // The one thing that has to be decided *per frame* about the dock: whether there is room for it
// beside the plot. Collapsed, the split view puts it *over* the plot rather than giving it a share of // beside the plot. Collapsed, the split view puts it *over* the plot rather than giving it a share of
// a width that is not there -- so a small window shows fewer pixels of plot, never a panel squeezed // a width that is not there -- so a small window shows fewer pixels of plot, never a panel squeezed
// to nothing. `kSidebarRoom` is the sidebar's 320 plus the width below which the plot stops being // to nothing. `kSidebarRoom` is the sidebar's 320 plus the width below which the plot stops being
@@ -4030,8 +4002,12 @@ gboolean tick(gpointer user) {
if (app->side_view != nullptr && app->window != nullptr) { if (app->side_view != nullptr && app->window != nullptr) {
// The split view writes `show-sidebar` itself (at startup and when it collapses), so the wish is // The split view writes `show-sidebar` itself (at startup and when it collapses), so the wish is
// re-asserted here -- but only when they disagree, so a settled frame touches nothing. This is also // re-asserted here -- but only when they disagree, so a settled frame touches nothing. This is also
// why a hide *gesture* does not stick: the panel is where the button says it is. // why a hide *gesture* does not stick: the panel is where the button says it is. The wish alone decides
const bool want = !app->model->signals().empty() && app->sidebar_wanted; // whether the dock is there; having signals decides only whether it does anything ("disable, do not
// hide"): a dock that is on screen from the start does not jump in when the first CONFIG arrives.
const bool want = app->sidebar_wanted;
gtk_widget_set_sensitive(app->side_box, !app->model->signals().empty());
gtk_widget_set_sensitive(app->sidebar_toggle, TRUE);
if (adw_overlay_split_view_get_show_sidebar(ADW_OVERLAY_SPLIT_VIEW(app->side_view)) != want) { if (adw_overlay_split_view_get_show_sidebar(ADW_OVERLAY_SPLIT_VIEW(app->side_view)) != want) {
adw_overlay_split_view_set_show_sidebar(ADW_OVERLAY_SPLIT_VIEW(app->side_view), want); adw_overlay_split_view_set_show_sidebar(ADW_OVERLAY_SPLIT_VIEW(app->side_view), want);
if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app->sidebar_toggle)) != want) { if (gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(app->sidebar_toggle)) != want) {
@@ -4702,8 +4678,8 @@ void run_app_action(App& app, AppAction action) {
case AppAction::CursorsPane: case AppAction::CursorsPane:
toggle(app.cursors_toggle); toggle(app.cursors_toggle);
break; break;
case AppAction::SideTabMeasure: select_side_tab(app, "measure"); break; case AppAction::ShowMeasure: dock_show_section(app, "measure"); break;
case AppAction::SideTabSignals: select_side_tab(app, "signals"); break; case AppAction::ShowSignals: dock_show_section(app, "signals"); break;
case AppAction::LayoutEditor: case AppAction::LayoutEditor:
toggle(app.layout_toggle); toggle(app.layout_toggle);
break; break;
@@ -4745,6 +4721,9 @@ gboolean on_app_key_pressed(GtkEventControllerKey*, guint keyval, guint, GdkModi
tour_dismiss(*app); tour_dismiss(*app);
return TRUE; return TRUE;
} }
// ... and then the slide-over: Escape closes it, ahead of the editable check for the same reason -- the
// key that dismisses a panel must work from inside one of its own spin buttons.
if (keyval == GDK_KEY_Escape && slideover_escape(*app)) return TRUE;
GtkWidget* focus = gtk_window_get_focus(GTK_WINDOW(app->window)); GtkWidget* focus = gtk_window_get_focus(GTK_WINDOW(app->window));
if (focus != nullptr && GTK_IS_EDITABLE(focus)) return FALSE; if (focus != nullptr && GTK_IS_EDITABLE(focus)) return FALSE;
@@ -4998,8 +4977,8 @@ void on_activate(GtkApplication* application, gpointer user) {
install_css(); install_css();
// The window's skeleton is a `.ui` file: the window, its two panes, the plot column with the status // The window's skeleton is a `.ui` file: the window, its two panes, the plot column with the status
// line under it, and the sidebar's tab switcher and two-page stack. Everything that depends on the // line under it, and the dock's scroller. Everything that depends on the
// stream -- the events pane, the two plot areas, the sidebar's panels -- is built below and put // stream -- the events pane, the two plot areas, the dock's panels -- is built below and put
// into the slots by id. // into the slots by id.
g_clear_object(&app->window_ui); g_clear_object(&app->window_ui);
app->window_ui = gtk_builder_new_from_resource("/org/udpscope/ui/main-window.ui"); app->window_ui = gtk_builder_new_from_resource("/org/udpscope/ui/main-window.ui");
@@ -5018,10 +4997,8 @@ void on_activate(GtkApplication* application, gpointer user) {
// help's annotated figures label. // help's annotated figures label.
app->status_row = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_row")); app->status_row = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_row"));
GtkWidget* side = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_column")); GtkWidget* side = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_column"));
GtkWidget* side_stack = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_stack")); app->dock_scroller = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "dock_scroller"));
GtkWidget* side_switcher = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_switcher")); app->dock_page = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "dock_page"));
GtkWidget* measure_page = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "measure_page"));
GtkWidget* signals_page = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "signals_page"));
app->badge_bar = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "badge_bar")); app->badge_bar = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "badge_bar"));
app->badge_signals = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "badge_signals")); app->badge_signals = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "badge_signals"));
app->badge_system = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "badge_system")); app->badge_system = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "badge_system"));
@@ -5033,8 +5010,7 @@ void on_activate(GtkApplication* application, gpointer user) {
app->status_label == nullptr || app->status_info_button == nullptr || app->status_label == nullptr || app->status_info_button == nullptr ||
app->status_row == nullptr || app->badge_bar == nullptr || app->badge_signals == nullptr || app->status_row == nullptr || app->badge_bar == nullptr || app->badge_signals == nullptr ||
app->badge_system == nullptr || app->badge_system == nullptr ||
side == nullptr || side_stack == nullptr || side_switcher == nullptr || side == nullptr || app->dock_scroller == nullptr || app->dock_page == nullptr) {
measure_page == nullptr || signals_page == nullptr) {
return; return;
} }
// The window's actions live on the application, so it has to know which one it belongs to. // The window's actions live on the application, so it has to know which one it belongs to.
@@ -5177,28 +5153,28 @@ void on_activate(GtkApplication* application, gpointer user) {
gtk_popover_set_has_arrow(GTK_POPOVER(app->status_popover), TRUE); gtk_popover_set_has_arrow(GTK_POPOVER(app->status_popover), TRUE);
} }
// The right-hand side is two tabs, not five stacked panels: one tab is on screen at a time, so // The right-hand side is the dock: one scroller, the signal list first (it is what is dragged onto the
// no section competes with four others for height. Each page scrolls on its own -- the two are // plot), then the cursors and the measurements. Every section is always there -- "go to Measure" scrolls
// different heights, and one shared scroller would leave a switch scrolled where the new page // to it instead of switching a tab, so the measurements are as live as the list above them.
// never asked to be. // `side` and `dock_page`/`dock_scroller` are resolved above with the rest of the skeleton and checked
// `side` and `side_stack`/`side_switcher`/`measure_page`/`signals_page` are resolved above with // by the null guard; assign the member here.
// the rest of the skeleton and checked by the null guard; assign the members here.
app->side_box = side; app->side_box = side;
app->side_stack = side_stack;
app->side_switcher = side_switcher;
// Measure: what the cursors bracket, and the figures derived from it. // Built in the order the panels have always been (cursors, measurements, trigger, signals) and placed in
// the dock's: the trigger form goes to the slide-over (reached by the Trigger badge).
app->cursors_panel = build_cursors_panel(*app); app->cursors_panel = build_cursors_panel(*app);
gtk_box_append(GTK_BOX(measure_page), app->cursors_panel); GtkWidget* measures_panel = build_measures_panel(*app);
gtk_box_append(GTK_BOX(measure_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
gtk_box_append(GTK_BOX(measure_page), build_measures_panel(*app));
// The trigger form lives in the slide-over (reached by the Trigger badge); the signal list stays in the
// sidebar until the dock replaces it.
app->trigger_panel = build_trigger_panel(*app); app->trigger_panel = build_trigger_panel(*app);
// Top-aligned: the page's scroller gives the child the page's height, and a form that stretched to it
// would spread its rows (or draw its frame) over empty space under the last one.
gtk_widget_set_valign(app->trigger_panel, GTK_ALIGN_START);
slideover_add_page(*app, "trigger", "Trigger", app->trigger_panel); slideover_add_page(*app, "trigger", "Trigger", app->trigger_panel);
app->signal_panel = build_signal_panel(*app); app->signal_panel = build_signal_panel(*app);
gtk_box_append(GTK_BOX(signals_page), app->signal_panel); gtk_box_append(GTK_BOX(app->dock_page), app->signal_panel);
gtk_box_append(GTK_BOX(app->dock_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
gtk_box_append(GTK_BOX(app->dock_page), app->cursors_panel);
gtk_box_append(GTK_BOX(app->dock_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
gtk_box_append(GTK_BOX(app->dock_page), measures_panel);
// The cursors start off, so their readout starts insensitive -- stated here by the one place that // The cursors start off, so their readout starts insensitive -- stated here by the one place that
// knows the default, and the *body* and not the panel, because the panel's heading carries the // knows the default, and the *body* and not the panel, because the panel's heading carries the
@@ -5210,15 +5186,8 @@ void on_activate(GtkApplication* application, gpointer user) {
sync_arm_buttons(*app); sync_arm_buttons(*app);
gtk_widget_set_sensitive(app->cursors_reset, FALSE); gtk_widget_set_sensitive(app->cursors_reset, FALSE);
if (app->side_stack != nullptr) { // The source kind's own panels (a generator, digital I/O), pages of the slide-over, added before the
adw_view_stack_set_visible_child_name(ADW_VIEW_STACK(app->side_stack), "signals"); // first tick.
g_signal_connect(app->side_stack, "notify::visible-child-name",
G_CALLBACK(on_side_tab_changed), app);
}
// The source kind's own tabs (a generator, digital I/O), added before the first tick so the
// pages-versus-pills reading below counts them too. The tabs are words, so the switcher's icons
// are hidden first and kept hidden for the pages this call adds.
keep_tabs_icon_free(*app);
build_source_panels(*app); build_source_panels(*app);
// The badge bar reads `trigger_toggle` (made by `build_toolbar()` above) and the source's pages, which // The badge bar reads `trigger_toggle` (made by `build_toolbar()` above) and the source's pages, which
// `build_source_panels()` has just registered: the Source badge opens the first of them. // `build_source_panels()` has just registered: the Source badge opens the first of them.
@@ -5915,7 +5884,7 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
g_timeout_add(static_cast<guint>(std::atoi(env)), stats_probe, &app); g_timeout_add(static_cast<guint>(std::atoi(env)), stats_probe, &app);
} }
if (const char* env = g_getenv("UDPSCOPE_PROBE_SIDE_TAB_MS")) { if (const char* env = g_getenv("UDPSCOPE_PROBE_SIDE_TAB_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), side_tab_probe, &app); g_timeout_add(static_cast<guint>(std::atoi(env)), dock_section_probe, &app);
} }
if (const char* env = g_getenv("UDPSCOPE_PROBE_SLIDEOVER_MS")) { if (const char* env = g_getenv("UDPSCOPE_PROBE_SLIDEOVER_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), slideover_probe, &app); g_timeout_add(static_cast<guint>(std::atoi(env)), slideover_probe, &app);
@@ -6028,6 +5997,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
if (const char* env = g_getenv("UDPSCOPE_PROBE_MENUS_MS")) { if (const char* env = g_getenv("UDPSCOPE_PROBE_MENUS_MS")) {
menus_probe_next(app, 0, static_cast<guint>(std::atoi(env))); menus_probe_next(app, 0, static_cast<guint>(std::atoi(env)));
} }
if (const char* env = g_getenv("UDPSCOPE_PROBE_DOCK_SESSION_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), dock_session_probe, &app);
}
if (const char* env = g_getenv("UDPSCOPE_PROBE_KEYS_MS")) { if (const char* env = g_getenv("UDPSCOPE_PROBE_KEYS_MS")) {
g_timeout_add(static_cast<guint>(std::atoi(env)), keys_probe, &app); g_timeout_add(static_cast<guint>(std::atoi(env)), keys_probe, &app);
} }
@@ -6237,12 +6209,12 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
inside(app.events_list, app.events_revealer)); inside(app.events_list, app.events_revealer));
} }
std::printf("[udpscope] panels: side=%dx%d (visible=%d) signals=%dx%d row_h=%d " std::printf("[udpscope] panels: side=%dx%d (visible=%d) signals=%dx%d row_h=%d "
"(page_current=%d) trigger=%dx%d (sensitive=%d) switcher=%d " "(page_current=%d) trigger=%dx%d (sensitive=%d) "
"cursors=%d window=%.3fs window_cap=%.1fs\n", "cursors=%d window=%.3fs window_cap=%.1fs\n",
app.dbg_side_w, app.dbg_side_h, app.dbg_side_column_after_hide, app.dbg_signal_w, app.dbg_side_w, app.dbg_side_h, app.dbg_side_column_after_hide, app.dbg_signal_w,
app.dbg_signal_h, app.dbg_signal_row_h, app.dbg_signal_page_current, app.dbg_signal_h, app.dbg_signal_row_h, app.dbg_signal_page_current,
app.dbg_trigger_w, app.dbg_trigger_h, app.dbg_trigger_w, app.dbg_trigger_h,
app.dbg_trigger_sensitive, app.dbg_switcher_visible, app.dbg_trigger_sensitive,
rows.cursors_visible() ? 1 : 0, rows.time_window(), rows.cursors_visible() ? 1 : 0, rows.time_window(),
app.window_limit_s); app.window_limit_s);
{ {
@@ -6267,10 +6239,8 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
app.dbg_panel_switch_trigger, app.dbg_toolbar_switch_trigger, app.dbg_panel_switch_trigger, app.dbg_toolbar_switch_trigger,
app.dbg_panel_switch_cursors, app.dbg_toolbar_switch_cursors, app.dbg_panel_switch_cursors, app.dbg_toolbar_switch_cursors,
app.dbg_panel_drove_toolbar); app.dbg_panel_drove_toolbar);
std::printf("[udpscope] switcher: policy=%s pills=%d labels=%d images=%d pages=%d size=%dx%d\n", std::printf("[udpscope] dock: shown=%d sensitive=%d w=%d scroller_pages=%d\n", app.dbg_dock_shown,
app.dbg_switcher_policy == 0 ? "narrow" : (app.dbg_switcher_policy == 1 ? "wide" : "?"), app.dbg_dock_sensitive, app.dbg_dock_w, app.dbg_dock_scrollers);
app.dbg_switcher_pills, app.dbg_switcher_labels, app.dbg_switcher_images,
app.dbg_switcher_pages, app.dbg_switcher_w, app.dbg_switcher_h);
// The help affordance layer: what the contextual-help toggle left on the surfaces, and where the // The help affordance layer: what the contextual-help toggle left on the surfaces, and where the
// guided tour stood when the last capture ran. Both are captured on the tick (`capture_help_state`), // guided tour stood when the last capture ran. Both are captured on the tick (`capture_help_state`),
// so neither asks a destroyed widget anything. // so neither asks a destroyed widget anything.
+13 -56
View File
@@ -4,7 +4,7 @@
<!-- <!--
The main window's skeleton: the window, the split view that puts the sidebar beside the plot, the The main window's skeleton: the window, the split view that puts the sidebar beside the plot, the
events pane's divider, the plot column (`plot_overlay` > `plot_box`, the slide-over panel being an overlay child added in code) events pane's divider, the plot column (`plot_overlay` > `plot_box`, the slide-over panel being an overlay child added in code)
with the status line under it, and the sidebar's tab stack. with the status line under it, and the sidebar, which is the dock.
The sidebar is an `AdwOverlaySplitView`, which is what a utility sidebar is in GNOME (as against The sidebar is an `AdwOverlaySplitView`, which is what a utility sidebar is in GNOME (as against
`AdwNavigationSplitView`, which is for navigation lists). Three things follow from that, and they are `AdwNavigationSplitView`, which is for navigation lists). Three things follow from that, and they are
@@ -21,7 +21,7 @@
What is *not* here is as deliberate as what is. Everything this file nests is fixed — the shape of What is *not* here is as deliberate as what is. Everything this file nests is fixed — the shape of
the window does not depend on the stream, the trigger or the layout — while the subtrees that do are the window does not depend on the stream, the trigger or the layout — while the subtrees that do are
built by code and put in place: the events pane, into `events_divider`'s start side; the two plot built by code and put in place: the events pane, into `events_divider`'s start side; the two plot
areas, into the split inside `plot_box`; the sidebar's panels, appended to `measure_page` and `signals_page`; and the areas, into the split inside `plot_box`; the dock's panels (signals, cursors, measurements), appended to `dock_page`; and the
status popover, attached to `status_info`. status popover, attached to `status_info`.
The header bar is not here either: it is the window's own titlebar, built by `build_toolbar()` The header bar is not here either: it is the window's own titlebar, built by `build_toolbar()`
@@ -49,66 +49,23 @@
<property name="min-sidebar-width">320</property> <property name="min-sidebar-width">320</property>
<property name="max-sidebar-width">320</property> <property name="max-sidebar-width">320</property>
<property name="sidebar"> <property name="sidebar">
<!-- A switcher above a stack, and a scroller *per page*: the two pages are different <!-- The dock: one scroller holding the signal list, then the cursors and the measurements.
heights, so one shared scroller around the stack would leave a tab switch scrolled It replaces the tab switcher (four text tabs did not fit 320 px and a source can add
to somewhere the new page never asked for. The switcher is the sibling above, not a more); it is the temporary home of Measure and Signals until steps 2 and 3 turn them
child of either scroller: the one control that chooses what you are looking at must into the badge bar's signals and the compact readouts. -->
not be the first thing to scroll away.
The tabs are words: the policy is `wide` and every page has a title, no icon. (They
were icon-only glyphs once; the glyphs were not understandable enough.) The pages'
*names* are what identify them to `select_side_tab()` and the session file; a source
kind's own tabs are added in code with their `SourcePanel::title`. -->
<object class="GtkBox" id="side_column"> <object class="GtkBox" id="side_column">
<property name="orientation">vertical</property> <property name="orientation">vertical</property>
<property name="width-request">300</property> <property name="width-request">300</property>
<child> <child>
<object class="AdwViewSwitcher" id="side_switcher"> <object class="GtkScrolledWindow" id="dock_scroller">
<property name="stack">side_stack</property>
<property name="policy">wide</property>
<property name="margin-top">4</property>
<property name="margin-bottom">4</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
</object>
</child>
<child>
<object class="AdwViewStack" id="side_stack">
<property name="vexpand">1</property> <property name="vexpand">1</property>
<child> <property name="hscrollbar-policy">never</property>
<object class="AdwViewStackPage"> <property name="vscrollbar-policy">automatic</property>
<property name="name">measure</property> <property name="child">
<property name="title">Measure</property> <object class="GtkBox" id="dock_page">
<property name="child"> <property name="orientation">vertical</property>
<object class="GtkScrolledWindow">
<property name="hscrollbar-policy">never</property>
<property name="vscrollbar-policy">automatic</property>
<property name="child">
<object class="GtkBox" id="measure_page">
<property name="orientation">vertical</property>
</object>
</property>
</object>
</property>
</object> </object>
</child> </property>
<child>
<object class="AdwViewStackPage">
<property name="name">signals</property>
<property name="title">Signals</property>
<property name="child">
<object class="GtkScrolledWindow">
<property name="hscrollbar-policy">never</property>
<property name="vscrollbar-policy">automatic</property>
<property name="child">
<object class="GtkBox" id="signals_page">
<property name="orientation">vertical</property>
</object>
</property>
</object>
</property>
</object>
</child>
</object> </object>
</child> </child>
</object> </object>
+2 -2
View File
@@ -73,14 +73,14 @@
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="visible">1</property> <property name="visible">1</property>
<property name="accelerator">m</property> <property name="accelerator">m</property>
<property name="title" translatable="yes">Show the Measure tab</property> <property name="title" translatable="yes">Go to Measure</property>
</object> </object>
</child> </child>
<child> <child>
<object class="GtkShortcutsShortcut"> <object class="GtkShortcutsShortcut">
<property name="visible">1</property> <property name="visible">1</property>
<property name="accelerator">g</property> <property name="accelerator">g</property>
<property name="title" translatable="yes">Show the Signals tab</property> <property name="title" translatable="yes">Go to Signals</property>
</object> </object>
</child> </child>
<child> <child>
+22 -34
View File
@@ -241,13 +241,11 @@ struct App {
GtkWidget* trigger_panel = nullptr; GtkWidget* trigger_panel = nullptr;
GtkWidget* signal_panel = nullptr; GtkWidget* signal_panel = nullptr;
GtkWidget* side_box = nullptr; GtkWidget* side_box = nullptr;
/// The sidebar's tab stack and its switcher. The stack is what chooses which panels are on /// The dock: the sidebar's one scroller and the box inside it, which holds the signal list, the
/// screen; a feature's own on/off never does (it sets its panel's sensitivity instead). /// cursors and the measurements (`dock_show_section()` scrolls it). A feature's own on/off never
GtkWidget* side_stack = nullptr; /// removes a section (it sets its panel's sensitivity instead).
GtkWidget* side_switcher = nullptr; GtkWidget* dock_page = nullptr;
/// The page name the stack is showing, and the one value the session file persists about the GtkWidget* dock_scroller = nullptr;
/// sidebar. "signals" on a first run.
std::string side_tab = "signals";
GtkWidget* arm_button = nullptr; GtkWidget* arm_button = nullptr;
/// The Force button, beside the Arm/Disarm one: one acquisition now, with the configured pre/post /// The Force button, beside the Arm/Disarm one: one acquisition now, with the configured pre/post
/// window. It carries no state of its own -- `sync_arm_buttons()` states the one it has /// window. It carries no state of its own -- `sync_arm_buttons()` states the one it has
@@ -477,18 +475,6 @@ struct App {
/// works, and every other line of the dump still passes. /// works, and every other line of the dump still passes.
std::string dbg_toolbar_icons; std::string dbg_toolbar_icons;
std::string dbg_app_icons; std::string dbg_app_icons;
/// What the switcher is: how many pills it holds, and how many labels it is *drawing*. The tabs
/// are words (`wide` policy, a title per page), so the label count must equal the page count.
int dbg_switcher_pills = -1;
int dbg_switcher_labels = -1;
/// How many `GtkImage`s the switcher has *mapped*: a tab is its title and nothing else, so this
/// must be 0 -- an icon, or the missing-icon placeholder GTK draws for a page with no `icon-name`.
int dbg_switcher_images = -1;
/// How many pages the sidebar stack holds: `pills` and `labels` must both equal it.
int dbg_switcher_pages = -1;
/// The switcher's own size, beside it: what the tab bar costs the column.
int dbg_switcher_w = -1;
int dbg_switcher_h = -1;
/// The panes' own switches and the header bar's buttons, read together by the exit probe: the two /// The panes' own switches and the header bar's buttons, read together by the exit probe: the two
/// must agree, and `dbg_panel_drove_toolbar` is the pane's switch having moved the button. /// must agree, and `dbg_panel_drove_toolbar` is the pane's switch having moved the button.
int dbg_panel_switch_trigger = -1; int dbg_panel_switch_trigger = -1;
@@ -504,7 +490,6 @@ struct App {
/// The Arm button, read with the trigger off: it is the pane's one runtime control, so it is the one /// The Arm button, read with the trigger off: it is the pane's one runtime control, so it is the one
/// thing in the body that *is* disabled with the mode. /// thing in the body that *is* disabled with the mode.
int dbg_arm_sensitive = -1; int dbg_arm_sensitive = -1;
int dbg_switcher_policy = -1;
/// The header bar, so the dump can report the order the controls are in: a header bar is a /// The header bar, so the dump can report the order the controls are in: a header bar is a
/// *titlebar*, not a child of the window, so walking down from the window does not reach it. /// *titlebar*, not a child of the window, so walking down from the window does not reach it.
GtkWidget* toolbar = nullptr; GtkWidget* toolbar = nullptr;
@@ -881,7 +866,7 @@ struct App {
std::string source_kind = "udps"; std::string source_kind = "udps";
SourceConfig source_opts; SourceConfig source_opts;
std::string source_title; std::string source_title;
/// The panels the source kind added to `side_stack`, in tab order. Empty for UDPS and in file mode. /// The panels the source kind added to the slide-over, in page order. Empty for UDPS and in file mode.
std::vector<SourcePanelPage> source_panels; std::vector<SourcePanelPage> source_panels;
bool source_has_datagrams = true; ///< the kind reports raw datagrams; false leaves the capture button disabled bool source_has_datagrams = true; ///< the kind reports raw datagrams; false leaves the capture button disabled
guint layout_save_source = 0; guint layout_save_source = 0;
@@ -1139,7 +1124,14 @@ struct App {
int dbg_signal_row_h = 0; int dbg_signal_row_h = 0;
int dbg_trigger_w = 0; int dbg_trigger_w = 0;
int dbg_trigger_h = 0; int dbg_trigger_h = 0;
/// The signal list has rows (the dock has no tab to be "current" on any more).
int dbg_signal_page_current = 0; int dbg_signal_page_current = 0;
/// The dock as the exit probe found it: the split view showing the sidebar, the column sensitive (it is
/// not until the stream has signals), its width, and how many `GtkScrolledWindow`s it holds directly.
int dbg_dock_shown = -1;
int dbg_dock_sensitive = -1;
int dbg_dock_w = -1;
int dbg_dock_scrollers = -1;
int dbg_trigger_sensitive = 0; int dbg_trigger_sensitive = 0;
int dbg_trigger_items = 0; int dbg_trigger_items = 0;
int dbg_trigger_selected = -1; int dbg_trigger_selected = -1;
@@ -1161,7 +1153,6 @@ struct App {
double dbg_trigger_time = 0.0; double dbg_trigger_time = 0.0;
int dbg_trigger_time_valid = -1; int dbg_trigger_time_valid = -1;
std::string dbg_mode_live; std::string dbg_mode_live;
int dbg_switcher_visible = 0;
int dbg_trigger_sensitive_default = 0; int dbg_trigger_sensitive_default = 0;
/// The detail string the popover and the exit dump read. The dump used to read the label, /// The detail string the popover and the exit dump read. The dump used to read the label,
@@ -1572,9 +1563,9 @@ constexpr int kEventThumbH = 46;
constexpr int kEventThumbMaxH = 104; constexpr int kEventThumbMaxH = 104;
inline int g_stats_phase = 0; inline int g_stats_phase = 0;
inline std::vector<std::uint64_t> g_stats_n_before; inline std::vector<std::uint64_t> g_stats_n_before;
/// The same for the tab-switch probe: the accumulator counts recorded while the Measure page was /// The same for the dock-section probe: the accumulator counts recorded while the Measure section was
/// still current, so the report can say whether any of them advanced afterwards. /// still scrolled into view, so the report can say whether any of them advanced after it scrolled out.
inline std::vector<std::uint64_t> g_side_tab_n_before; inline std::vector<std::uint64_t> g_dock_section_n_before;
inline double g_trigrenum_edge = 0.0; inline double g_trigrenum_edge = 0.0;
inline std::uint64_t g_trigrenum_latches = 0; inline std::uint64_t g_trigrenum_latches = 0;
inline ConditionProbe g_condition; inline ConditionProbe g_condition;
@@ -1600,11 +1591,6 @@ void config_dialog_refresh(ConfigDialog& d);
void open_configurations(App& app); void open_configurations(App& app);
void save_layout_now(App& app); void save_layout_now(App& app);
/// The sidebar's tabs are words: hide the icon every `AdwViewSwitcherButton` draws (the
/// missing-icon placeholder, for a page with a title and no `icon-name`), for the buttons that exist
/// and for any page added later. Call once, before `build_source_panels()`.
void keep_tabs_icon_free(App& app);
/// Capability panels (`src/ui_source.cpp`). `build_source_panels` adds the source kind's panels as /// Capability panels (`src/ui_source.cpp`). `build_source_panels` adds the source kind's panels as
/// pages of the slide-over (`source-<id>`), once, before the first tick. `apply_source_panels` sets every panel's /// pages of the slide-over (`source-<id>`), once, before the first tick. `apply_source_panels` sets every panel's
/// sensitivity from `connected` -- disabled, never hidden. /// sensitivity from `connected` -- disabled, never hidden.
@@ -1803,8 +1789,9 @@ void row_menu_fill(RowMenuState& st);
GtkWidget* open_row_menu(App& app, int row, double x, double y, GtkWidget* parent, bool autohide = true); GtkWidget* open_row_menu(App& app, int row, double x, double y, GtkWidget* parent, bool autohide = true);
void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer user); void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer user);
gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y, gpointer user); gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y, gpointer user);
void on_side_tab_changed(GObject*, GParamSpec*, gpointer user); /// Scrolls the dock to a section: "signals" (the top) or "measure" (the cursors panel, which the
void select_side_tab(App& app, const char* name); /// measurements follow). An unknown name is a no-op.
void dock_show_section(App& app, const char* section);
/// Reads each pane's own switch back off the header bar's button, and the other way round: the two are /// Reads each pane's own switch back off the header bar's button, and the other way round: the two are
/// one control in two places, and `panel_switch_sync` keeps the write from looking like a click. /// one control in two places, and `panel_switch_sync` keeps the write from looking like a click.
void sync_panel_switches(App& app); void sync_panel_switches(App& app);
@@ -1907,8 +1894,9 @@ gboolean measurepin_live_wait(gpointer user);
gboolean measure_trace(gpointer user); gboolean measure_trace(gpointer user);
gboolean stats_probe_report(gpointer user); gboolean stats_probe_report(gpointer user);
gboolean stats_probe(gpointer user); gboolean stats_probe(gpointer user);
gboolean side_tab_probe_report(gpointer user); gboolean dock_section_probe_report(gpointer user);
gboolean side_tab_probe(gpointer user); gboolean dock_section_probe(gpointer user);
gboolean dock_session_probe(gpointer user); // UDPSCOPE_PROBE_DOCK_SESSION_MS
/// The slide-over panel (`src/ui_slideover.cpp`): one panel at a time over the plot's right edge, a page /// The slide-over panel (`src/ui_slideover.cpp`): one panel at a time over the plot's right edge, a page
/// per setup surface, each page in a scroller of its own. /// per setup surface, each page in a scroller of its own.
void build_slideover(App& app); // once, after the plot_overlay exists void build_slideover(App& app); // once, after the plot_overlay exists
+5 -2
View File
@@ -64,7 +64,7 @@ LayoutState collect_layout(App& app) {
st.math = app.math; st.math = app.math;
st.measures = app.measures; st.measures = app.measures;
st.measure_stats = app.measure_stats_on; st.measure_stats = app.measure_stats_on;
st.side_tab = app.side_tab; st.dock_shown = app.sidebar_wanted;
// The trigger, its settings *and* whether it was on. The channel goes by name, like every identity // The trigger, its settings *and* whether it was on. The channel goes by name, like every identity
// in this file: every rebuild renumbers the channels. // in this file: every rebuild renumbers the channels.
{ {
@@ -427,7 +427,10 @@ void apply_layout(App& app, const LayoutState& st) {
app.measures = st.measures; app.measures = st.measures;
app.measure_stats_on = st.measure_stats; app.measure_stats_on = st.measure_stats;
app.updating_panels = true; app.updating_panels = true;
select_side_tab(app, st.side_tab.c_str()); // The dock's shown state is the wish `update_side_visibility()` re-asserts every frame; setting the
// toggle goes through `sidebar_syncing`, so the restore does not schedule a save of what it just read.
app.sidebar_wanted = st.dock_shown;
update_side_visibility(app);
if (app.measures_stats_check != nullptr) { if (app.measures_stats_check != nullptr) {
gtk_check_button_set_active(GTK_CHECK_BUTTON(app.measures_stats_check), gtk_check_button_set_active(GTK_CHECK_BUTTON(app.measures_stats_check),
app.measure_stats_on); app.measure_stats_on);
+177 -62
View File
@@ -104,12 +104,6 @@ gboolean exit_probe_step3(gpointer user) {
a->dbg_signal_h = gtk_widget_get_height(a->signal_panel); a->dbg_signal_h = gtk_widget_get_height(a->signal_panel);
a->dbg_trigger_w = gtk_widget_get_width(a->trigger_panel); a->dbg_trigger_w = gtk_widget_get_width(a->trigger_panel);
a->dbg_trigger_h = gtk_widget_get_height(a->trigger_panel); a->dbg_trigger_h = gtk_widget_get_height(a->trigger_panel);
// The switcher's size as allocated: read here and not on the first tick, where the window has not
// been allocated yet and every widget still measures 0x0.
if (a->side_switcher != nullptr) {
a->dbg_switcher_w = gtk_widget_get_width(a->side_switcher);
a->dbg_switcher_h = gtk_widget_get_height(a->side_switcher);
}
// How many rows of the signal list carry a drag source: dragging a signal onto a plot is how a plot is // How many rows of the signal list carry a drag source: dragging a signal onto a plot is how a plot is
// filled, so it has to survive both pane modes -- the locked list drops the tick, the menu and the `+`, // filled, so it has to survive both pane modes -- the locked list drops the tick, the menu and the `+`,
// and this is the reading that says it did not drop the drag with them. One row is the `MATH` divider, // and this is the reading that says it did not drop the drag with them. One row is the `MATH` divider,
@@ -187,14 +181,9 @@ gboolean exit_probe_step3(gpointer user) {
} }
if (a->area != nullptr) a->dbg_frames_painted = static_cast<int>(a->area->frames_painted()); if (a->area != nullptr) a->dbg_frames_painted = static_cast<int>(a->area->frames_painted());
a->dbg_reset_sensitive = a->cursors_reset != nullptr && gtk_widget_get_sensitive(a->cursors_reset) ? 1 : 0; a->dbg_reset_sensitive = a->cursors_reset != nullptr && gtk_widget_get_sensitive(a->cursors_reset) ? 1 : 0;
// A page, not a revealer: a stack's hidden page measures 0x0 the way an unrevealed revealer // The signal list has rows: a reading about *state* (the dock has no page to be current on), never
// did, so the reading asks about *state* -- is this page the current one -- and never about size. // about size.
const char* page = a->side_stack != nullptr a->dbg_signal_page_current = gtk_widget_get_first_child(a->signal_list) != nullptr ? 1 : 0;
? adw_view_stack_get_visible_child_name(ADW_VIEW_STACK(a->side_stack))
: nullptr;
const int on_signals = page != nullptr && std::strcmp(page, "signals") == 0 ? 1 : 0;
a->dbg_signal_page_current =
on_signals && gtk_widget_get_first_child(a->signal_list) != nullptr ? 1 : 0;
// The trigger form is no longer on a sidebar tab, so the reading is the body's own sensitivity. // The trigger form is no longer on a sidebar tab, so the reading is the body's own sensitivity.
a->dbg_trigger_sensitive = gtk_widget_get_sensitive(a->trigger_body) ? 1 : 0; a->dbg_trigger_sensitive = gtk_widget_get_sensitive(a->trigger_body) ? 1 : 0;
// `gtk_widget_get_visible` on the sidebar's GtkBox is always 1: AdwOverlaySplitView hides its // `gtk_widget_get_visible` on the sidebar's GtkBox is always 1: AdwOverlaySplitView hides its
@@ -203,8 +192,17 @@ gboolean exit_probe_step3(gpointer user) {
const int sidebar_shown = a->side_view != nullptr && const int sidebar_shown = a->side_view != nullptr &&
adw_overlay_split_view_get_show_sidebar(ADW_OVERLAY_SPLIT_VIEW(a->side_view)) ? 1 : 0; adw_overlay_split_view_get_show_sidebar(ADW_OVERLAY_SPLIT_VIEW(a->side_view)) ? 1 : 0;
a->dbg_side_column_after_hide = sidebar_shown; a->dbg_side_column_after_hide = sidebar_shown;
a->dbg_switcher_visible = // The dock, read here with the other sizes (the dump runs after the window is gone). `sensitive` is the
a->side_switcher != nullptr && gtk_widget_get_visible(a->side_switcher) ? 1 : 0; // availability: the column is on screen whether or not the stream has signals, and does nothing until
// it does.
a->dbg_dock_shown = sidebar_shown;
a->dbg_dock_sensitive = gtk_widget_get_sensitive(a->side_box) ? 1 : 0;
a->dbg_dock_w = a->dbg_side_w;
a->dbg_dock_scrollers = 0;
for (GtkWidget* c = gtk_widget_get_first_child(a->side_box); c != nullptr;
c = gtk_widget_get_next_sibling(c)) {
if (GTK_IS_SCROLLED_WINDOW(c)) ++a->dbg_dock_scrollers;
}
if (a->trigger_signal_drop != nullptr) { if (a->trigger_signal_drop != nullptr) {
GListModel* model = gtk_drop_down_get_model(GTK_DROP_DOWN(a->trigger_signal_drop)); GListModel* model = gtk_drop_down_get_model(GTK_DROP_DOWN(a->trigger_signal_drop));
a->dbg_trigger_items = model != nullptr ? static_cast<int>(g_list_model_get_n_items(model)) : 0; a->dbg_trigger_items = model != nullptr ? static_cast<int>(g_list_model_get_n_items(model)) : 0;
@@ -1795,11 +1793,24 @@ gboolean stats_probe(gpointer user) {
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
/// Phase two of the tab-switch probe: the hidden page's accumulator must have advanced. A statistics /// Which section the dock's scroller is showing at its top: "measure" once it has been scrolled to the
/// readout is *history*, so min/max/avg would gain a gap for exactly as long as the trigger was on /// cursors panel or past it, "signals" above that. Read from the adjustment, so it is the state the user
/// screen -- which is the one defect the design's "what runs on a hidden page" rule is there to /// would see and not the last call made.
/// prevent, and the reason no frame-rate code may be gated on which tab is current. static const char* dock_section_in_view(App& a) {
gboolean side_tab_probe_report(gpointer user) { if (a.dock_scroller == nullptr || a.cursors_panel == nullptr || a.dock_page == nullptr) return "none";
graphene_point_t zero = GRAPHENE_POINT_INIT(0.f, 0.f);
graphene_point_t at;
if (!gtk_widget_compute_point(a.cursors_panel, a.dock_page, &zero, &at)) return "none";
GtkAdjustment* adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(a.dock_scroller));
return gtk_adjustment_get_value(adj) > 0.5 * at.y ? "measure" : "signals";
}
/// Phase two of the dock-section probe: the accumulators must have advanced while the Measure section was
/// scrolled out of view. A statistics readout is *history*, so min/max/avg would gain a gap for exactly as
/// long as the section was off screen -- which is the one defect the design's "what runs on a hidden
/// page" rule is there to prevent, and the reason no frame-rate code may be gated on which section is in
/// view. (It was a tab once; the dock is one scroller, and the rule outlived the tabs.)
gboolean dock_section_probe_report(gpointer user) {
App* a = static_cast<App*>(user); App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE; if (a->window == nullptr) return G_SOURCE_REMOVE;
std::uint64_t advanced = 0; std::uint64_t advanced = 0;
@@ -1807,25 +1818,37 @@ gboolean side_tab_probe_report(gpointer user) {
bool first = true; bool first = true;
for (std::size_t i = 0; i < a->measure_stats.size(); ++i) { for (std::size_t i = 0; i < a->measure_stats.size(); ++i) {
const std::uint64_t n = a->measure_stats[i].n; const std::uint64_t n = a->measure_stats[i].n;
if (i < g_side_tab_n_before.size() && n > g_side_tab_n_before[i]) ++advanced; if (i < g_dock_section_n_before.size() && n > g_dock_section_n_before[i]) ++advanced;
if (first || n < min_n) { if (first || n < min_n) {
min_n = n; min_n = n;
first = false; first = false;
} }
} }
if (a->dock_page != nullptr) gtk_widget_set_size_request(a->dock_page, -1, -1);
std::fprintf(stderr, std::fprintf(stderr,
"[side-tab-probe] page=%s rows=%zu accums=%zu advanced=%" G_GUINT64_FORMAT "[side-tab-probe] section=%s rows=%zu accums=%zu advanced=%" G_GUINT64_FORMAT
" min_n=%" G_GUINT64_FORMAT "\n", " min_n=%" G_GUINT64_FORMAT "\n",
a->side_tab.c_str(), a->measures.size(), a->measure_stats.size(), advanced, min_n); dock_section_in_view(*a), a->measures.size(), a->measure_stats.size(), advanced, min_n);
std::fflush(stderr); std::fflush(stderr);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
/// Phase one: four measurements on the first channel, statistics on, then switch *away* from the /// Phase one: four measurements on the first channel, statistics on, then scroll the dock *away* from the
/// Measure tab and come back in two seconds to read what ran while it was not on screen. /// Measure section and come back in two seconds to read what ran while it was not in view. (The probe
gboolean side_tab_probe(gpointer user) { /// keeps its old environment variable, `UDPSCOPE_PROBE_SIDE_TAB_MS`, and its `[side-tab-probe]` tag.)
gboolean dock_section_probe(gpointer user) {
App* a = static_cast<App*>(user); App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE; if (a->window == nullptr) return G_SOURCE_REMOVE;
// A dock whose content fits has nothing to scroll away from, and a tall window does that. The first
// pass makes the page taller than any window (a probe-only minimum height, undone in the report) and
// comes back once it has been laid out, so "scrolled out of view" is a state this run really reaches.
static bool padded = false;
if (!padded && a->dock_page != nullptr) {
padded = true;
gtk_widget_set_size_request(a->dock_page, -1, 3000);
g_timeout_add(400, dock_section_probe, a);
return G_SOURCE_REMOVE;
}
// One signal on, and on a plot, the way `enable_all_probe` does it and a restored session arrives. // One signal on, and on a plot, the way `enable_all_probe` does it and a restored session arrives.
// Both halves matter here: a signal starts disabled (enabling is what starts acquisition), and a // Both halves matter here: a signal starts disabled (enabling is what starts acquisition), and a
// channel with no trace leaves the live window *still* -- and a still window is exactly the state // channel with no trace leaves the live window *still* -- and a still window is exactly the state
@@ -1863,18 +1886,23 @@ gboolean side_tab_probe(gpointer user) {
if (a->measures_stats_check != nullptr) { if (a->measures_stats_check != nullptr) {
gtk_check_button_set_active(GTK_CHECK_BUTTON(a->measures_stats_check), TRUE); gtk_check_button_set_active(GTK_CHECK_BUTTON(a->measures_stats_check), TRUE);
} }
select_side_tab(*a, "measure"); dock_show_section(*a, "measure");
// Where the dock went: `scrolled` is its offset and `reach` the most it can scroll. A reach of 0 would
// mean the content fits and "away from the section" is not a state this run can reach.
GtkAdjustment* dock_adj = gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(a->dock_scroller));
const double scrolled = gtk_adjustment_get_value(dock_adj);
const double reach = gtk_adjustment_get_upper(dock_adj) - gtk_adjustment_get_page_size(dock_adj);
update_measures(*a, /*force=*/true, /*accumulate=*/true); update_measures(*a, /*force=*/true, /*accumulate=*/true);
g_side_tab_n_before.clear(); g_dock_section_n_before.clear();
for (const MeasureStatsAccum& s : a->measure_stats) g_side_tab_n_before.push_back(s.n); for (const MeasureStatsAccum& s : a->measure_stats) g_dock_section_n_before.push_back(s.n);
// Away from the page the measurements are on: its widgets are now unrealized and measure 0x0, and // Away from the section the measurements are in: they are scrolled out of the viewport (the widgets
// the tick keeps calling `update_measures()` regardless. // stay mapped, only clipped), and the tick keeps calling `update_measures()` regardless.
select_side_tab(*a, "signals"); dock_show_section(*a, "signals");
std::fprintf(stderr, "[side-tab-probe] page=%s accums=%zu baseline recorded\n", std::fprintf(stderr, "[side-tab-probe] section=%s accums=%zu baseline recorded (measure at scroll %.0f of %.0f)\n",
a->side_tab.c_str(), g_side_tab_n_before.size()); dock_section_in_view(*a), g_dock_section_n_before.size(), scrolled, reach);
std::fflush(stderr); std::fflush(stderr);
// Long enough for several throttled recomputes (one per 100 ms while the window moves). // Long enough for several throttled recomputes (one per 100 ms while the window moves).
g_timeout_add(2000, side_tab_probe_report, a); g_timeout_add(2000, dock_section_probe_report, a);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
@@ -3573,6 +3601,63 @@ gboolean sidebar_probe(gpointer user) {
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
/// `UDPSCOPE_PROBE_DOCK_SESSION_MS`: the dock's shown state through the session file, both ways.
///
/// The Dock button's state is the one thing the dock persists (`dock = true|false`, replacing `side_tab`).
/// Leg 1 hides the dock through the button (a wish, so it must schedule a save), collects the session and
/// writes it, reads the file back and applies it; leg 2 does the same shown. Leg 3 loads a file written by
/// the previous version (`side_tab = "measure"`, no `dock`): it must load without a complaint and leave
/// `dock_shown` at its default, true. Read as `ok=1` on every line.
gboolean dock_session_probe(gpointer user) {
App& a = *static_cast<App*>(user);
if (a.window == nullptr || a.sidebar_toggle == nullptr) return G_SOURCE_REMOVE;
const std::string dir = g_get_tmp_dir();
const std::string path = dir + "/udpscope-dock-session-probe.toml";
const auto leg = [&](const char* name, bool shown) {
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a.sidebar_toggle), shown);
const bool wish = a.sidebar_wanted;
const bool save_pending = a.layout_save_source != 0;
LayoutState st = collect_layout(a);
const bool collected = st.dock_shown;
const bool saved = save_layout(path, st);
LayoutState back;
const bool loaded = load_layout(path, back);
// Apply the *opposite* first so the restore has something to undo, then the loaded state.
a.sidebar_wanted = !shown;
apply_layout(a, back);
const bool applied = a.sidebar_wanted == shown;
const bool split_follows =
(adw_overlay_split_view_get_show_sidebar(ADW_OVERLAY_SPLIT_VIEW(a.side_view)) != FALSE) == shown;
std::fprintf(stderr,
"[dock-session] %s: wish=%d save_scheduled=%d collected=%d saved=%d loaded=%d read=%d "
"applied=%d split_follows=%d ok=%d\n",
name, wish ? 1 : 0, save_pending ? 1 : 0, collected ? 1 : 0, saved ? 1 : 0,
loaded ? 1 : 0, back.dock_shown ? 1 : 0, applied ? 1 : 0, split_follows ? 1 : 0,
(wish == shown && save_pending && collected == shown && saved && loaded &&
back.dock_shown == shown && applied)
? 1 : 0);
};
leg("hidden", false);
leg("shown", true);
{
std::FILE* f = std::fopen(path.c_str(), "w");
if (f != nullptr) {
std::fputs("side_tab = \"measure\"\nmeasure_stats = false\n", f);
std::fclose(f);
}
LayoutState old_file;
old_file.dock_shown = false; // anything but the default, so "left at the default" is a reading
const bool loaded = load_layout(path, old_file);
LayoutState fresh;
std::fprintf(stderr, "[dock-session] old file (side_tab only): loaded=%d dock_shown=%d default=%d ok=%d\n",
loaded ? 1 : 0, old_file.dock_shown ? 1 : 0, fresh.dock_shown ? 1 : 0,
loaded && fresh.dock_shown ? 1 : 0);
}
std::remove(path.c_str());
std::fflush(stderr);
return G_SOURCE_REMOVE;
}
gboolean prefs_probe(gpointer user) { gboolean prefs_probe(gpointer user) {
App& a = *static_cast<App*>(user); App& a = *static_cast<App*>(user);
open_preferences(a); open_preferences(a);
@@ -4524,7 +4609,7 @@ gboolean measures_probe_step(gpointer user) {
m.options.show_on_plot = measure_can_show_on_plot(m.type); m.options.show_on_plot = measure_can_show_on_plot(m.type);
a->measures.push_back(std::move(m)); a->measures.push_back(std::move(m));
} }
select_side_tab(*a, "measure"); dock_show_section(*a, "measure");
rebuild_measures_pane(*a); rebuild_measures_pane(*a);
std::fprintf(stderr, "[measures-probe] added %zu measurements on '%s'\n", a->measures.size(), std::fprintf(stderr, "[measures-probe] added %zu measurements on '%s'\n", a->measures.size(),
signal.c_str()); signal.c_str());
@@ -4683,7 +4768,7 @@ gboolean bandavg_probe_step(gpointer user) {
m.options.show_on_plot = measure_can_show_on_plot(m.type); m.options.show_on_plot = measure_can_show_on_plot(m.type);
a->measures.push_back(std::move(m)); a->measures.push_back(std::move(m));
} }
select_side_tab(*a, "measure"); dock_show_section(*a, "measure");
rebuild_measures_pane(*a); rebuild_measures_pane(*a);
g_timeout_add(250, measure_trace, a); g_timeout_add(250, measure_trace, a);
bandavg_probe_next(*a, 1, 1500); bandavg_probe_next(*a, 1, 1500);
@@ -6247,6 +6332,16 @@ gboolean defaults_probe(gpointer user) {
gboolean keys_probe(gpointer user) { gboolean keys_probe(gpointer user) {
App* a = static_cast<App*>(user); App* a = static_cast<App*>(user);
// `m` scrolls the dock, and a dock whose content fits has nothing to scroll: a run on a tall window
// would read 0->0 and prove nothing. So the first pass only makes the page taller than any window
// (a probe-only minimum height, undone below) and comes back once it has been laid out.
static bool padded = false;
if (!padded && a->dock_page != nullptr) {
padded = true;
gtk_widget_set_size_request(a->dock_page, -1, 3000);
g_timeout_add(400, keys_probe, a);
return G_SOURCE_REMOVE;
}
std::string line = "[keys]"; std::string line = "[keys]";
const auto before_after = [&](const char* name, GtkWidget* button, AppAction action) { const auto before_after = [&](const char* name, GtkWidget* button, AppAction action) {
@@ -6282,38 +6377,56 @@ gboolean keys_probe(gpointer user) {
before_after("t: trigger panel", a->trigger_toggle, AppAction::TriggerPanel); before_after("t: trigger panel", a->trigger_toggle, AppAction::TriggerPanel);
before_after("c: cursors", a->cursors_toggle, AppAction::CursorsPane); before_after("c: cursors", a->cursors_toggle, AppAction::CursorsPane);
// `m` and `g` scroll the dock: the reading is the scroller's offset before and after, and `m` must land
// on the cursors panel's own y (clamped to what the page can scroll, which is why the reachable maximum
// is printed beside it), `g` back on 0.
const auto dock_adjustment = [a]() {
return gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(a->dock_scroller));
};
const auto dock_value = [&]() { return gtk_adjustment_get_value(dock_adjustment()); };
{ {
const std::string before_tab = a->side_tab; const double before = dock_value();
run_app_action(*a, AppAction::SideTabMeasure); run_app_action(*a, AppAction::ShowMeasure);
const std::string after_tab = a->side_tab; const double after = dock_value();
char buf[96]; graphene_point_t zero = GRAPHENE_POINT_INIT(0.f, 0.f);
std::snprintf(buf, sizeof(buf), " m: tab %s->%s |", before_tab.c_str(), after_tab.c_str()); graphene_point_t at = GRAPHENE_POINT_INIT(-1.f, -1.f);
if (!gtk_widget_compute_point(a->cursors_panel, a->dock_page, &zero, &at)) at.y = -1.f;
GtkAdjustment* adj = dock_adjustment();
const double reach = gtk_adjustment_get_upper(adj) - gtk_adjustment_get_page_size(adj);
char buf[160];
std::snprintf(buf, sizeof(buf), " m: dock %.0f->%.0f (cursors_y=%.0f reach=%.0f at_cursors=%d) |", before,
after, static_cast<double>(at.y), reach,
std::fabs(after - std::min(static_cast<double>(at.y), reach)) < 1.0 ? 1 : 0);
line += buf; line += buf;
} }
{ {
const std::string before_tab = a->side_tab; const double before = dock_value();
run_app_action(*a, AppAction::SideTabSignals); run_app_action(*a, AppAction::ShowSignals);
const std::string after_tab = a->side_tab; const double after = dock_value();
char buf[96]; char buf[96];
std::snprintf(buf, sizeof(buf), " g: tab %s->%s |", before_tab.c_str(), after_tab.c_str()); std::snprintf(buf, sizeof(buf), " g: dock %.0f->%.0f at_top=%d |", before, after,
std::fabs(after) < 1.0 ? 1 : 0);
line += buf; line += buf;
} }
before_after("l: layout", a->layout_toggle, AppAction::LayoutEditor); before_after("l: layout", a->layout_toggle, AppAction::LayoutEditor);
before_after("s: side panel", a->sidebar_toggle, AppAction::SidePanel); before_after("s: side panel", a->sidebar_toggle, AppAction::SidePanel);
// An accel must be refused while text is being typed. Focus the window entry, send 'm' // An accel must be refused while text is being typed. Focus the window entry, send 'm'
// through the real handler, and require the side tab not to change. // through the real handler, and require the dock not to scroll.
int refused = -1; int refused = -1;
if (a->window_entry != nullptr) { if (a->window_entry != nullptr) {
gtk_menu_button_popup(GTK_MENU_BUTTON(a->window_button)); gtk_menu_button_popup(GTK_MENU_BUTTON(a->window_button));
gtk_widget_grab_focus(a->window_entry); gtk_widget_grab_focus(a->window_entry);
const std::string before_tab = a->side_tab; // The dock has to be somewhere other than where `m` would take it, or "did not scroll" proves nothing.
gtk_adjustment_set_value(dock_adjustment(), 0.0);
const double before_dock = dock_value();
const gboolean handled = on_app_key_pressed(nullptr, GDK_KEY_m, 0, const gboolean handled = on_app_key_pressed(nullptr, GDK_KEY_m, 0,
static_cast<GdkModifierType>(0), a); static_cast<GdkModifierType>(0), a);
const std::string after_tab = a->side_tab; const double after_dock = dock_value();
refused = (handled == FALSE && before_tab == after_tab) ? 1 : 0; refused = (handled == FALSE && before_dock == after_dock) ? 1 : 0;
gtk_menu_button_popdown(GTK_MENU_BUTTON(a->window_button)); gtk_menu_button_popdown(GTK_MENU_BUTTON(a->window_button));
} }
if (a->dock_page != nullptr) gtk_widget_set_size_request(a->dock_page, -1, -1);
char tail[64]; char tail[64];
std::snprintf(tail, sizeof(tail), " editable focused: 'm' refused=%d\n", refused); std::snprintf(tail, sizeof(tail), " editable focused: 'm' refused=%d\n", refused);
line += tail; line += tail;
@@ -6570,10 +6683,10 @@ void tour_geometry_line(App& a, const char* probe, const char* what) {
p.origin.y < q.origin.y + q.size.height && q.origin.y < p.origin.y + p.size.height; p.origin.y < q.origin.y + q.size.height && q.origin.y < p.origin.y + p.size.height;
}; };
std::fprintf(stderr, std::fprintf(stderr,
"[%s] %s=%d/%d visible=%d side_tab=%s spot=[%.0f,%.0f %.0fx%.0f] " "[%s] %s=%d/%d visible=%d slideover=%s spot=[%.0f,%.0f %.0fx%.0f] "
"target=[%.0f,%.0f %.0fx%.0f] contains=%d card=[%.0f,%.0f %.0fx%.0f] clear=%d\n", "target=[%.0f,%.0f %.0fx%.0f] contains=%d card=[%.0f,%.0f %.0fx%.0f] clear=%d\n",
probe, what, tour_step_index(a) + 1, tour_step_count(a), tour_active(a) ? 1 : 0, probe, what, tour_step_index(a) + 1, tour_step_count(a), tour_active(a) ? 1 : 0,
a.side_tab.c_str(), spot.origin.x, spot.origin.y, spot.size.width, spot.size.height, (slideover_page(a).empty() ? "none" : slideover_page(a).c_str()), spot.origin.x, spot.origin.y, spot.size.width, spot.size.height,
target.origin.x, target.origin.y, target.size.width, target.size.height, target.origin.x, target.origin.y, target.size.width, target.size.height,
have_spot && have_target && contains(spot, target) ? 1 : 0, card.origin.x, have_spot && have_target && contains(spot, target) ? 1 : 0, card.origin.x,
card.origin.y, card.size.width, card.size.height, card.origin.y, card.size.width, card.size.height,
@@ -6818,10 +6931,10 @@ gboolean tour_surface_probe_step(gpointer user) {
case 0: { case 0: {
// Where the run starts: a fresh config's sidebar is on Signals, so this is a *first* run's // Where the run starts: a fresh config's sidebar is on Signals, so this is a *first* run's
// state. The auto-start's walk (if one began) would only confuse the reading; it goes first. // state. The auto-start's walk (if one began) would only confuse the reading; it goes first.
std::fprintf(stderr, "[toursurface] before: side_tab=%s signal_list_mapped=%d running=%d\n", std::fprintf(stderr, "[toursurface] before: slideover=%s signal_list_mapped=%d running=%d\n",
a->side_tab.c_str(), mapped(a->signal_list), tour_active(*a) ? 1 : 0); slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str(), mapped(a->signal_list), tour_active(*a) ? 1 : 0);
tour_dismiss(*a); tour_dismiss(*a);
select_side_tab(*a, "measure"); slideover_open(*a, "trigger");
a->dbg_tour_surface_step = 1; a->dbg_tour_surface_step = 1;
tour_surface_probe_next(*a, 500); tour_surface_probe_next(*a, 500);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
@@ -6830,8 +6943,9 @@ gboolean tour_surface_probe_step(gpointer user) {
// A frame after the switch, so the Measure page has been laid out and the list really is off // A frame after the switch, so the Measure page has been laid out and the list really is off
// screen. The tour is started *here*, from this state. // screen. The tour is started *here*, from this state.
std::fprintf(stderr, std::fprintf(stderr,
"[toursurface] on measure: side_tab=%s signal_list_mapped=%d side_stack_mapped=%d\n", "[toursurface] on trigger: slideover=%s signal_list_mapped=%d side_box_mapped=%d\n",
a->side_tab.c_str(), mapped(a->signal_list), mapped(a->side_stack)); slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str(),
mapped(a->signal_list), mapped(a->side_box));
start_tour(*a); start_tour(*a);
std::fprintf(stderr, "[toursurface] tour: visible=%d steps=%d step=%d signals_stop=%d\n", std::fprintf(stderr, "[toursurface] tour: visible=%d steps=%d step=%d signals_stop=%d\n",
tour_active(*a) ? 1 : 0, tour_step_count(*a), tour_step_index(*a), tour_active(*a) ? 1 : 0, tour_step_count(*a), tour_step_index(*a),
@@ -6848,8 +6962,9 @@ gboolean tour_surface_probe_step(gpointer user) {
} }
GtkWidget* target = tour_target(*a); GtkWidget* target = tour_target(*a);
const bool is_list = target != nullptr && target == a->signal_list; const bool is_list = target != nullptr && target == a->signal_list;
std::fprintf(stderr, "[toursurface] step=%d/%d side_tab=%s target_mapped=%d%s\n", std::fprintf(stderr, "[toursurface] step=%d/%d slideover=%s target_mapped=%d%s\n",
tour_step_index(*a) + 1, tour_step_count(*a), a->side_tab.c_str(), mapped(target), tour_step_index(*a) + 1, tour_step_count(*a),
slideover_page(*a).empty() ? "none" : slideover_page(*a).c_str(), mapped(target),
is_list ? " target=signal_list" : ""); is_list ? " target=signal_list" : "");
if (is_list) { if (is_list) {
// The frame after the switch: the page has been laid out and `update_tour()` has placed the // The frame after the switch: the page has been laid out and `update_tour()` has placed the
@@ -6876,9 +6991,9 @@ gboolean tour_surface_probe_step(gpointer user) {
default: { default: {
start_tour(*a); start_tour(*a);
std::fprintf(stderr, std::fprintf(stderr,
"[toursurface] collapsed: side_stack_mapped=%d steps=%d signals_stop=%d " "[toursurface] collapsed: side_box_mapped=%d steps=%d signals_stop=%d "
"side_stop=%d\n", "side_stop=%d\n",
mapped(a->side_stack), tour_step_count(*a), mapped(a->side_box), tour_step_count(*a),
tour_plan_holds(*a, a->signal_list) ? 1 : 0, tour_plan_holds(*a, a->signal_list) ? 1 : 0,
tour_plan_holds(*a, a->side_box) ? 1 : 0); tour_plan_holds(*a, a->side_box) ? 1 : 0);
tour_dismiss(*a); tour_dismiss(*a);
-42
View File
@@ -11,48 +11,6 @@
namespace udpscope { namespace udpscope {
namespace ui_impl { namespace ui_impl {
namespace {
/// Hide every `GtkImage` inside `w`, and keep it hidden. `AdwViewSwitcherButton` always builds an
/// icon -- in both of its layouts -- and a page with no `icon-name` gets `image-missing`, which is
/// the placeholder the sidebar's text tabs used to draw. There is no property or policy that turns
/// the icon off, so the image is hidden (a hidden child takes no space: the label gets the pill) and
/// a `notify::visible` handler puts it back to hidden if the button ever shows it again.
void hide_tab_icons(GtkWidget* w) {
if (GTK_IS_IMAGE(w)) {
if (g_object_get_data(G_OBJECT(w), "udpscope-icon-hidden") == nullptr) {
g_object_set_data(G_OBJECT(w), "udpscope-icon-hidden", GINT_TO_POINTER(1));
g_signal_connect(w, "notify::visible", G_CALLBACK(+[](GObject* o, GParamSpec*, gpointer) {
GtkWidget* img = GTK_WIDGET(o);
if (gtk_widget_get_visible(img)) gtk_widget_set_visible(img, FALSE);
}),
nullptr);
}
gtk_widget_set_visible(w, FALSE);
return;
}
for (GtkWidget* c = gtk_widget_get_first_child(w); c != nullptr; c = gtk_widget_get_next_sibling(c)) {
hide_tab_icons(c);
}
}
void on_tab_pages_changed(GListModel*, guint, guint, guint, gpointer sw) {
hide_tab_icons(GTK_WIDGET(sw));
}
} // namespace
void keep_tabs_icon_free(App& app) {
if (app.side_switcher == nullptr || app.side_stack == nullptr) return;
hide_tab_icons(app.side_switcher);
// A page added later gets a button when the model reports it; this handler is connected *after*
// the switcher's own, so the button already exists when it runs.
g_signal_connect_after(adw_view_stack_get_pages(ADW_VIEW_STACK(app.side_stack)), "items-changed",
G_CALLBACK(on_tab_pages_changed), app.side_switcher);
// And once more when the switcher is (re)mapped, which also covers a rebuild of the buttons.
g_signal_connect_swapped(app.side_switcher, "map", G_CALLBACK(hide_tab_icons), app.side_switcher);
}
void build_source_panels(App& app) { void build_source_panels(App& app) {
if (app.slideover.stack == nullptr || app.acq == nullptr || app.acq->source() == nullptr) return; if (app.slideover.stack == nullptr || app.acq == nullptr || app.acq->source() == nullptr) return;
SourceKind* kind = find_kind(app.source_kind); SourceKind* kind = find_kind(app.source_kind);
+4 -7
View File
@@ -422,11 +422,9 @@ void start_tour(App& app) {
app.settings.tour_seen = true; app.settings.tour_seen = true;
(void)save_settings(app.settings); (void)save_settings(app.settings);
} }
// The sidebar's own stack, so a stop can name the page it stands on. Set here and not in // The main window has no page stack any more (the dock is one scroller): `page_stack` stays null, so
// `build_tour()`: the overlay is inserted above the window's content *before* `App::side_stack` is // a stop's `page` selects nothing and a stop whose target is unmapped is dropped. The slide-over hook
// assigned, so the member does not exist yet at build time -- and the walk cannot start before the // that lets a stop open the trigger page is the next step's.
// window is up anyway.
app.tour.page_stack = app.side_stack;
// The whole table, in walk order: `tour_begin()` drops the stops whose surface is not on screen. // The whole table, in walk order: `tour_begin()` drops the stops whose surface is not on screen.
std::vector<TourStop> stops; std::vector<TourStop> stops;
stops.reserve(kTourStepCount); stops.reserve(kTourStepCount);
@@ -486,8 +484,7 @@ constexpr FigureSurface kFigureSurfaces[] = {
{"trigger_summary", &App::trigger_summary}, {"trigger_summary", &App::trigger_summary},
{"trigger_panel", &App::trigger_panel}, {"trigger_panel", &App::trigger_panel},
{"signal_list", &App::signal_list}, {"signal_list", &App::signal_list},
{"side_switcher", &App::side_switcher}, {"side_stack", &App::side_box},
{"side_stack", &App::side_stack},
{"window_button", &App::window_button}, {"window_button", &App::window_button},
{"status_label", &App::status_label}, {"status_label", &App::status_label},
{"header_bar", &App::toolbar}, {"header_bar", &App::toolbar},