Feature/source plugins #1

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

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
udpscope is a **GTK4 oscilloscope-style viewer for a UDPStreamer source**. It receives one udpscope is a **GTK4 oscilloscope-style viewer for a UDPStreamer source**. It receives one
data source -- a UDPStreamer stream through `deps/udpsclient` by default, or another registered data source -- a UDPStreamer stream through `deps/udpsclient` by default, or another registered
source kind (see `docs/claude/architecture.md`) -- buffers it in a large ring (default 10 M samples per signal, under an 8 GiB memory limit), and draws it source kind (see `docs/claude/architecture.md`) -- buffers it in a large ring (default 10 M samples per signal, under an 8 GiB memory limit), and draws it
with `deps/plotter` — a figure of up to eight stacked rows sharing one x window. On top of with `deps/plotter` — a figure of up to eight plots in stacked rows (a row can be split into up to four side by side) sharing one x window. On top of
the live view it provides the things a bench scope provides: a trigger with a pre/post event the live view it provides the things a bench scope provides: a trigger with a pre/post event
window, a pause that freezes the visible window, cursors, statistical measurements, window, a pause that freezes the visible window, cursors, statistical measurements,
user-defined math channels built in a node editor, and an export of the visible window to user-defined math channels built in a node editor, and an export of the visible window to
+51 -7
View File
@@ -26,8 +26,8 @@ Options: `--server`, `--port`, `--multicast`, `--interface`, `--buffer-samples`
(samples kept per acquired signal, default 10M; see "The sample buffer is budgeted in samples" below), (samples kept per acquired signal, default 10M; see "The sample buffer is budgeted in samples" below),
`--memory-limit-mb` (hard ceiling on the ring storage, default 8192), `--max-window` (largest time `--memory-limit-mb` (hard ceiling on the ring storage, default 8192), `--max-window` (largest time
window the toolbar offers, default 600), `--holdoff` (default trigger hold-off), window the toolbar offers, default 600), `--holdoff` (default trigger hold-off),
`--recv-buffer`, `--plots` (the number of stacked plots to start with, 1..8; rows are `--recv-buffer`, `--plots` (the number of plots to start with, one per row, 1..8; rows are
added and removed from the **Layout editor** afterwards), `--relative-time` (see below), added and removed and a row can be split into side-by-side plots from the **Layout editor** afterwards), `--relative-time` (see below),
`--source NAME` and the repeatable `--source-opt k=v` (the source kind and its settings; see "Data sources" below), `--source NAME` and the repeatable `--source-opt k=v` (the source kind and its settings; see "Data sources" below),
`--capture-packets N` (see "Packet capture" below), `--renderer NAME` (GTK's renderer: `--capture-packets N` (see "Packet capture" below), `--renderer NAME` (GTK's renderer:
`cairo`, `gl` or `vulkan`; `cairo` is the software preset that exports both `cairo`, `gl` or `vulkan`; `cairo` is the software preset that exports both
@@ -1105,6 +1105,9 @@ plots = 1
measure_stats = false measure_stats = false
[[plot]] [[plot]]
row = 0
width = 1
weight = 1
signals = ["Ch1", "", "", ""] signals = ["Ch1", "", "", ""]
[[calibration]] [[calibration]]
@@ -1113,6 +1116,14 @@ gain = 2
offset = 0.5 offset = 0.5
``` ```
Each `[[plot]]` table is one **plot** (one cell of the layout), in left-to-right, top-to-bottom order.
`row` is the figure row the plot sits in (0-based) and `width` its share of that row's width; `weight` is
the *row's* share of the figure's height, written on every plot of the row and read from the row's first
one. A session with no `row` keys -- every file written before a row could be split -- loads as one plot per
row, and so does one whose `row` numbers cannot be a layout (a number that skips or goes back, more than 4
plots in a row or 8 in all): the file is never refused for it, and the signals stay where they were. See
"Splitting a row".
It also stores `measure_stats` (the measurements statistics toggle, a boolean, absent means It also stores `measure_stats` (the measurements statistics toggle, a boolean, absent means
`false`) and `dock` (whether the dock is shown, a boolean, absent means `true`; it replaced the `false`) and `dock` (whether the dock is shown, a boolean, absent means `true`; it replaced the
sidebar's `side_tab`, and a file that still carries `side_tab` loads, the key being ignored). The sidebar's `side_tab`, and a file that still carries `side_tab` loads, the key being ignored). The
@@ -2568,7 +2579,8 @@ out of the plot **while a gesture is on that row**, because a drag writes the *p
the only place it is visible. Outside a gesture the plot is not read at all: `cursor.x` is the the only place it is visible. Outside a gesture the plot is not read at all: `cursor.x` is the
snapped value then, which is what the app wrote itself a frame earlier, and adopting that back snapped value then, which is what the app wrote itself a frame earlier, and adopting that back
is what made a dragged cursor spring to where it had been. A ruler is per row: each row has its own y axis, so two rows' rulers are is what made a dragged cursor spring to where it had been. A ruler is per row: each row has its own y axis, so two rows' rulers are
two different values, and the pane names the row. two different values, and the pane names the row. In a split row (see "Splitting a row") the vertical pair is drawn in
every plot and a ruler belongs to one plot, which the pane names `plot 2a`.
`mix` needs no rulers: it reports the value the trace actually has at each marker, read `mix` needs no rulers: it reports the value the trace actually has at each marker, read
through the library's `Plot::crosshair_value()` — **the nearer of the two bracketing through the library's `Plot::crosshair_value()` — **the nearer of the two bracketing
@@ -2761,8 +2773,9 @@ full range — the snapshot's own span, which is legitimately *narrower* than th
when the ring had not filled it. A fit of a frozen view keeps it frozen rather than throwing it when the ring had not filled it. A fit of a frozen view keeps it frozen rather than throwing it
back onto the stream. back onto the stream.
**Shared x axis.** The rows are the rows of **one `plot::Figure`**, drawn by one **Shared x axis.** The plots are the plots of **one `plot::Figure`** (one per row, or several side by
`plot::gtk4::PlotArea` in figure mode, and they all join a single x link group. side in a split row, see "Splitting a row"), drawn by one `plot::gtk4::PlotArea` in figure mode, and they
all join a single x link group.
A gesture changes the row it landed on; `Figure::layout()` adopts that change A gesture changes the row it landed on; `Figure::layout()` adopts that change
into the group and pushes it to the others, so panning or zooming any row moves into the group and pushes it to the others, so panning or zooming any row moves
them all with nothing to propagate by hand. The live follow is the *group's* them all with nothing to propagate by hand. The live follow is the *group's*
@@ -2776,8 +2789,8 @@ The self-check's layout line carries `x-desync N`, the number of frames in which
any row's x range differed from row 0's by more than a pixel of data. **It must any row's x range differed from row 0's by more than a pixel of data. **It must
read 0.** read 0.**
`Figure`'s row count is fixed at construction, so there is one structural path: `Figure`'s shape (its cells per row) is fixed at construction, so there is one structural path:
a changed row count, or a changed legend name (the one thing a `Plot` cannot a changed row or cell count, or a changed legend name (the one thing a `Plot` cannot
express), builds a new figure and the area is handed it. That costs no GL state — express), builds a new figure and the area is handed it. That costs no GL state —
the widget keeps its context. A displayed event survives the rebuild because the the widget keeps its context. A displayed event survives the rebuild because the
pinned `[-pre, +post]` window is restated rather than re-derived from the live pinned `[-pre, +post]` window is restated rather than re-derived from the live
@@ -2808,6 +2821,37 @@ above row 2" are the same insertion, and two buttons there would sit one on top
other. The `rows + 1` boundary buttons — above the first row and below the last included — other. The `rows + 1` boundary buttons — above the first row and below the last included —
reach every position the two-per-row reading offers. reach every position the two-per-row reading offers.
### Splitting a row
A row can be split into **two to four plots side by side**, up to **eight plots in all**. A split is
only an arrangement of more plots on the screen: **every plot shares the one time window** -- one x
link group, so a pan or zoom in any plot moves them all; one crosshair, drawn at the same time in every
plot; and one pair of vertical cursors, at the same two times in every plot. What each plot keeps is its
own **y axis** (auto or fixed), its own horizontal rulers and its own fit; a wheel or an axis drag acts on
the plot under the pointer. Only the **bottom** row's plots carry the time labels, and the left margin
holding the y labels is one width for the whole figure, so the plots of a row line up with the rows
above and below it; a plot whose labels are narrower simply has more margin.
In the **Layout editor** (`l`):
- A **`+` at a row's left and right edge, and one between each two plots**, splits the row there: a new,
empty plot is added at that position and the plots after it move right *with their traces*. A one-cell
row can be split from either side. The `+` is dim at 4 plots in the row or 8 plots in all (the row `+`,
which adds a whole row, dims at 8 plots).
- A **drag arrow between two plots** resizes the two neighbours against each other; neither can be
dragged below about 96 px. The plotter keeps 64 px as an absolute floor, and when the window is too
narrow for that it shrinks every plot proportionally and drops the gap, so nothing has a negative size.
The widths are saved when the drag ends.
- The **`×`** at a plot's top left removes that plot (and its traces) from a split row, and removes the
*row* from a one-plot row, as it always did. It is dim when that would leave no plot.
Plots are named by their row and, in a split row, a letter: **`1`**, **`2a`**..**`2d`**. The names are
what the signals page's *Plot on* buttons (tooltip "row 2, cell 1 of 2"), the status notices ("plotting
'sine' on plot 2b") and the cursors pane for a split row (`plot 2a`; a plot of an unsplit row stays
`row N`, counted from 0 as before) show. A signal dragged from its badge lands on the plot under the
pointer, with the same free-slot rule as a row. The session file keeps the arrangement (`row` and `width`
on each `[[plot]]`, see "The session layout file").
The buttons are `GtkOverlay` children placed by their margins over the figure's own row The buttons are `GtkOverlay` children placed by their margins over the figure's own row
rects (`FigureLayout::rows`, divided by the area's device scale), deliberately **not** one rects (`FigureLayout::rows`, divided by the area's device scale), deliberately **not** one
transparent widget covering the plot: an overlay child is picked wherever its allocation transparent widget covering the plot: an overlay child is picked wherever its allocation
+25 -2
View File
@@ -71,6 +71,29 @@ it (type, size, range, sample rate, timing). Below come the colour and the **Plo
rows. Gain and offset are applied at ingest, so the measurements and the export carry the same numbers the rows. Gain and offset are applied at ingest, so the measurements and the export carry the same numbers the
plot shows; a renamed unit is used there too and is saved with the session. plot shows; a renamed unit is used there too and is saved with the session.
### Splitting a row
Press `l` (or pick **Layout editor** from the overflow menu) to arrange the plots: a **`×`** at the top
left of each plot removes it, a **`+`** on each boundary between rows adds a row, and a **Done** button
in the plot's top-right corner leaves the editor. The plots can also be put **side by side**: a row holds
up to **4** plots, and the figure up to **8** in all.
- **To split a row**, click the `+` at its left or right edge, or the `+` between two of its plots. The
new plot is empty and the plots after it keep their traces. The `+` is dim at 4 plots in the row or 8
in the figure.
- **To resize**, drag the arrow between two plots left or right. The two neighbours trade width (each
stays at least about 96 px wide); the sizes are saved with the session.
- **To remove a plot**, click its `×`. In a split row it removes just that plot and its traces; in a row
of one plot it removes the row.
Every plot shares **one time window**: panning or zooming in any of them moves them all, the crosshair
sits at the same time in each, and the vertical cursors are the same two times in each. Each plot has
its **own y axis**, its own horizontal rulers and its own fit, and only the bottom row shows the time
labels. A plot of a split row is named by its row and a letter, `2a` to `2d`: that is what the signals
page's **Plot on** buttons, the status line and the cursors readout call it, and a signal dragged
from its badge goes onto the plot under the pointer. The arrangement is saved per source, and a session
saved before rows could be split opens with one plot per row, as it was.
## 4. The window and navigation ## 4. The window and navigation
The header's window control (`2 s`) sets the time span on screen; the popover takes a number and a The header's window control (`2 s`) sets the time span on screen; the popover takes a number and a
@@ -129,8 +152,8 @@ does not walk it off its snapshot.
The cursors toggle in the header bar (`c`) switches on two instruments per row: The cursors toggle in the header bar (`c`) switches on two instruments per row:
- **vertical** — two times, shared by every row; - **vertical** — two times, shared by every row (and every plot of a split row);
- **horizontal** — two rulers per row; - **horizontal** — two rulers per plot;
- **both** — the two together; - **both** — the two together;
- **mix** — the two times, read off each *trace* where they cross it. - **mix** — the two times, read off each *trace* where they cross it.
+6 -4
View File
@@ -259,10 +259,12 @@ Layers to keep straight:
`ChannelBuffer` is a `plot::data::Series` over a (value, time) ring pair. `ChannelBuffer` is a `plot::data::Series` over a (value, time) ring pair.
- `SlotSeries` (`src/channel.hpp`) — a trace's stable series object, rebound each frame - `SlotSeries` (`src/channel.hpp`) — a trace's stable series object, rebound each frame
to either a `ChannelBuffer` or an `EventSeries`. to either a `ChannelBuffer` or an `EventSeries`.
- `RowSet` (`src/plot_host.hpp`) — **one `plot::Figure`** of up to `kMaxRows` rows (added - `RowSet` (`src/plot_host.hpp`) — **one `plot::Figure`** of up to `kMaxRows` **plots** (rows are added
and removed one at a time by the layout editor), each a and removed one at a time by the layout editor, and a row can be split into up to
borrowed `plot::Plot*` with `kSlotsPerRow` fixed trace slots. The figure owns `kMaxCellsPerRow` cells side by side), each plot a
everything the rows have to agree on: the x window (one x link group every row joins), borrowed `plot::Plot*` with `kSlotsPerRow` fixed trace slots; `row(i)` indexes the plots row-major,
`figure_row_count()` counts the rows. The figure owns
everything the plots have to agree on: the x window (one x link group every plot joins),
the live follow (the *group's* — a member's own axis must be `XFollow::Off`, since the live follow (the *group's* — a member's own axis must be `XFollow::Off`, since
`Figure::link_x()` refuses a `Live` member), the left gutter and the crosshair. `Figure::link_x()` refuses a `Live` member), the left gutter and the crosshair.
+16
View File
@@ -269,6 +269,22 @@ adapter `PlotArea`'s **figure mode** —
`set_figure()`, one `input::Controller` per row, nearest-row event routing, a `set_figure()`, one `input::Controller` per row, nearest-row event routing, a
figure-wide crosshair — plus `PlotArea::row_at()` for naming the row a pointer is over. figure-wide crosshair — plus `PlotArea::row_at()` for naming the row a pointer is over.
A figure row can hold **several plots side by side**, which the host's horizontal split needs and
the library had no concept of: `Figure(std::span<const u32> cells_per_row)` builds one, with
`plot_count()`, `cells_in_row()`, `first_plot_of_row()` and `row_of_plot()` to move between rows and the
flattened plot index, `set_cell_weights()` and `set_col_gap()` to size the cells, and `FigureLayout::cells`
(one rect per plot, flattened) and `FigureLayout::col_gap` to read the result back. `Figure(u32 rows)`
still means one cell per row, so every row-only caller and test is unchanged, and `row_count()` stays the
number of *figure rows* (the host's `RowSet::row_count()` is the plot count, `figure_row_count()` the
rows). `plot(i)`, `link_x()` and the crosshair's hovered index all take the **flattened** plot index.
Cell widths use the height scheme: a floor of **64 px logical** per cell, the weights share the rest,
the last cell closes the row exactly, and a figure too narrow for the floors shrinks the cells
proportionally and drops the column gap. Only the bottom row's cells carry x labels and the x title
(every other plot gets `XStrip::None`), and the left y gutter stays one maximum over the whole figure.
In the GTK4 and GLFW adapters the pointer is routed by **cell rect** (`row_in(rows, x, y)`: the
containing cell, else the nearest by squared distance), one `input::Controller` per plot. The tests are
the `figure_cell*` cases in `tests/test_figure.cpp` and `glfw_host_routes_by_cell`.
`plot::data::RingBuffer` carries two writes a plain SPSC ring has no use for and this app `plot::data::RingBuffer` carries two writes a plain SPSC ring has no use for and this app
does: `amend(absolute, v)` rewrites a retained slot when a late packet's data finally does: `amend(absolute, v)` rewrites a retained slot when a late packet's data finally
arrives, and `clear_to(absolute)` empties a ring and **resumes its count at a given arrives, and `clear_to(absolute)` empties a ring and **resumes its count at a given
+57 -1
View File
@@ -52,7 +52,7 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
up front and rebinding via `SlotSeries`; a *name* change (legend text) is the one up front and rebinding via `SlotSeries`; a *name* change (legend text) is the one
thing that forces a rebuild. Cursors are parked outside the axis range instead of thing that forces a rebuild. Cursors are parked outside the axis range instead of
removed. removed.
- **A `Figure`'s row count is fixed at construction**, so `RowSet::rebuild_all()` builds - **A `Figure`'s shape (its cells per row) is fixed at construction**, so `RowSet::rebuild_all()` builds
a *new* figure and the area is handed it (`PlotArea::set_figure()`); it costs no GL a *new* figure and the area is handed it (`PlotArea::set_figure()`); it costs no GL
state. That makes one path for "a row's traces changed" and "the row count changed", state. That makes one path for "a row's traces changed" and "the row count changed",
and it is why `rebuild_all()` restates a pinned event window (`pinned_pre_`/ and it is why `rebuild_all()` restates a pinned event window (`pinned_pre_`/
@@ -105,6 +105,60 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
an equal division. `sync_layout_editor()` runs every frame on purpose: an equal division. `sync_layout_editor()` runs every frame on purpose:
`gtk_widget_set_margin_*()` returns early on an unchanged value, so a settled layout costs `gtk_widget_set_margin_*()` returns early on an unchanged value, so a settled layout costs
the comparisons and nothing else, and a resize needs no notification. the comparisons and nothing else, and a resize needs no notification.
- **A figure row can be split into cells, and every plot index in the host is the flattened one.**
`RowSet::row_count()` is the number of **plots** and `row(i)`, `Row::plot`, the cursors, the
measurements, `bind()`, the readouts and the persistence layers all index plots row-major (row 0's cells
left to right, then row 1's); `figure_row_count()` is the number of figure rows, and
`cells_in_row()`/`first_plot_of_row()`/`figure_row_of()`/`cell_of()` translate. **Code that means "the
rows of the figure" -- the editor's row `+`, the row drag, the row weights, `fl.rows` -- must say
`figure_row_count()`**: with no split the two are equal and a slip passes every unsplit probe, which
is how a row weight once drifted off its row (see the `row-weights` reading). The caps are
`kMaxCellsPerRow` = 4 per row and `kMaxRows` = 8 **plots** in all (the constant bounds plots now, not
figure rows), so every per-plot structure sized for 8 is unchanged. `plot_label()` is what the UI
prints for a plot: the row's number (`2`) for a one-cell row, the number and a letter (`2a`..`2d`) for
a split one -- never a flat index, which is 0-based and says nothing about where the plot is.
What the cells share and what each keeps:
- **One time window.** Every cell of every row is a member of the one x link group, so a pan or zoom
in any cell moves them all, the crosshair is one x drawn in every cell (the hovered cell shows its
y), and the **vertical cursor pair is one shared time drawn in every cell** (`cursors_aligned`).
A split is an arrangement of more plots, not a second time scale: nothing about the time axis changes
(see [invariants-time-axis-and-window.md](invariants-time-axis-and-window.md)).
- **Per-cell y.** Each cell has its own y axis (auto or fixed), its own horizontal rulers and its own
fit; a wheel or axis drag acts on the cell under the pointer, an x gesture on any bottom-row cell
moves the shared window. The cursors pane's ruler label is `plot 2a` for a plot of a split row and
`row N` (the plot index) for one of an unsplit row, so the pane reads the same as before until a row
is split.
- **Bottom-row x labels only.** The plotter gives every plot that is not in the last figure row
`XStrip::None`; the cells of the bottom row each keep their strip, so a middle-drag on the axis
works in any of them. The `t0` caption goes on those cells too.
- **One left gutter.** The y gutter is one maximum for the whole figure with the same reserved floor
as before (`Figure::set_min_y_gutter()`), **not a per-column one**: a per-column gutter would make a
cell's width depend on its labels, which "layout must not depend on content" forbids. A cell whose y
labels are narrower than the gutter simply has a wider margin.
- **Widths are weights with a floor.** The plotter keeps 64 px logical per cell and a column gap
(`kColGap`, 6 px); the editor's drag is stricter (below). A weight is a share of what is
left *after* the floors, so cells of `{3, 1}` read 533:220 in a 756 px figure, not 3:1, and the
editor's 96 px drag floor is a floor on the *share it asks for*: a cell dragged to the limit realises
`64 + share * remainder` and reads wider (144 px in the split probe's `pre-insert0` line), so the
boundary does not follow the pointer one for one near either extreme.
- **A cell is removed with its traces, and a new cell is empty.** `insert_cell(fr, at)` pushes the
cells at and after `at` right *with their contents* (the same rule as `insert_row()`), and
`remove_cell(plot)` on a one-cell row removes the row. Both return false at a cap or when they would
leave no plot, and never grow past 4 / 8.
**The editor's controls** (`rebuild_layout_editor()`): a cell `+` (`kLayoutCellKey`, tooltip "split
here") at a row's left edge, at its right edge and at every internal boundary -- `n + 1` of them for a
row of `n` cells, so a one-cell row can be split from either side -- inserting at that position; a drag
arrow (`kLayoutColKey`, `go-next-symbolic`) at each internal boundary that moves the boundary between
its two neighbours, **each floored at `kMinCellPx` = 96 px logical** (or half the pair when the pair is
narrower) and measured from the two widths at drag start, so the drag does not compound; and the per-plot
`×`, which removes the cell in a split row ("remove this plot") and the row in a one-cell row ("remove
this row"), insensitive when it would leave no plot. Cell `+` is insensitive at 4 cells in its row or 8
plots in all; the row `+` is insensitive at 8 plots. **The cell `+` is keyed by `kLayoutCellKey` (figure row * 8 + position + 1) and
never by `set_row_key()`**: `row_key()` is a plot or boundary index, `layout_button()` finds a button by it,
and a cell `+` that also carried one would be found as a row button; `sync_layout_editor()` tells the kinds
apart by which key it finds. Like every editor control the
cell buttons are placed from `FigureLayout::cells`, divided by `device_scale()`, and the editor does
not draw while the figure's rects are not yet one per plot.
- **The slide-over is the second thing placed over the plot, and it is one opaque widget, not a - **The slide-over is the second thing placed over the plot, and it is one opaque widget, not a
transparent area.** The same pick rule as the editor's buttons applies, harder: the panel is a transparent area.** The same pick rule as the editor's buttons applies, harder: the panel is a
`GtkRevealer` aligned to the end edge (`halign=END`, `valign=FILL`) at `kSlideOverWidth` = 360 px, so `GtkRevealer` aligned to the end edge (`halign=END`, `valign=FILL`) at `kSlideOverWidth` = 360 px, so
@@ -189,6 +243,8 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
per-channel list, and why `format_elapsed()` steps to a coarser unit instead of per-channel list, and why `format_elapsed()` steps to a coarser unit instead of
letting the counter grow: each is a status field whose *width* would otherwise depend letting the counter grow: each is a status field whose *width* would otherwise depend
on the stream, and the status row is directly above the plot. on the stream, and the status row is directly above the plot.
A split row keeps this rule: cell widths come from the weights and the 64 px floor and never from what
a cell's labels need, and the gutter stays one figure-wide maximum.
The reservation goes on the **figure** (`Figure::set_min_y_gutter()`, added to the The reservation goes on the **figure** (`Figure::set_min_y_gutter()`, added to the
plotter for this), not on the rows: `Figure::layout()` clears every row's plotter for this), not on the rows: `Figure::layout()` clears every row's
`set_min_y_gutter()` to measure what its own labels need, so a floor written onto the `set_min_y_gutter()` to measure what its own labels need, so a floor written onto the
@@ -51,6 +51,27 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
was parsed into the last calibration record and the enable state was lost without a word. was parsed into the last calibration record and the enable state was lost without a word.
`mkdirs()` used to create only components with a trailing slash, so `~/.config/udpscope` was never `mkdirs()` used to create only components with a trailing slash, so `~/.config/udpscope` was never
made and the *first* save failed silently -- worth knowing when a layout change "does not stick". made and the *first* save failed silently -- worth knowing when a layout change "does not stick".
- **A `[[plot]]` table is one *plot* (one cell), and carries `row` and `width` for the horizontal split.**
`plots = N` counts plots, as it always did. `row` is the 0-based **figure row** the plot sits in and
`width` the cell's share of that row's width (`LayoutState::Row::row`, `::width`); the tables run in
the flattened order, so a row's cells are consecutive and left to right. The row's height share
`weight` stays per *figure row* but is **written on every cell of it** (so a table read alone is
complete) and **read from the row's first cell**; `collect_layout()` takes it from
`row_weights()[figure_row_of(plot)]`, not from `row_weights()[plot]` -- the latter is a plot index into
a figure-row vector, right until the first split. The file is written `row`, `width`, `weight`,
`signals`; a state built with `row < 0` (nothing in the app does) omits `row` and `width`.
Three loading rules, all in `apply_layout()` and none of them a refusal, because a session is the
user's arrangement and not worth losing to a malformed key:
(1) **a file with no `row` loads as one cell per row** -- every session written before the split --
with the per-plot `weight`s as the row heights, exactly as it opened before;
(2) **a `row` sequence that cannot be a layout loads the same way**: some plot without a `row`, a row
number that goes backwards or skips one, a first row that is not 0, more than 4 cells in a row, or
more than 8 plots; the signals stay on the plots they were on (`invalid-rows` is the reading);
(3) **cell widths are applied after the arrangement**, because `set_cells_per_row()` resets them, and
only for rows of two or more cells -- a one-cell row has no widths and its `width` is ignored.
Both new keys are parsed with the file's other forgiving rules: `row` must be an integer >= 0 and
`width` a number > 0, anything else leaves the default. A save after loading a pre-split file writes
the `row`/`width` keys, which is how an old session is upgraded without anyone asking.
- **Measurement options persist under *named* keys -- `threshold`, `threshold2`, `noise` and `hf` -- so a - **Measurement options persist under *named* keys -- `threshold`, `threshold2`, `noise` and `hf` -- so a
file written before them loads at the defaults**, which are exactly the behaviour it was written against: file written before them loads at the defaults**, which are exactly the behaviour it was written against:
`noise = 10` is the `noise = 10` is the
+74 -5
View File
@@ -424,13 +424,23 @@ Two lines of the self-check output are assertions in disguise:
the group, because `hold_pinned_window()` prefers a member that has moved since `pinned_stated_` the group, because `hold_pinned_window()` prefers a member that has moved since `pinned_stated_`
— that is how it tells a gesture from its own echo — so a fit that wrote the group alone reads — that is how it tells a gesture from its own echo — so a fit that wrote the group alone reads
back as the zoomed range one frame later. back as the zoomed range one frame later.
- `[layout-probe] <action> rows=N buttons=B done=D done_box=[x,y,WxH] rows_y=[..] close=[..] add=[..] traces=[..]` - `[layout-probe] <action> rows=N buttons=B done=D done_box=[x,y,WxH] rows_y=[..] close=[..] add=[..] traces=[..] cells=[..] colweights=[..]`
`side_w` is the sidebar column's width and **must be its own request (280), never 0**: it read 0 while `side_w` is the sidebar column's width and **must be its own request (280), never 0**: it read 0 while
the divider sat at the window's right edge, which is a panel that is not there. It was 280 during the the divider sat at the window's right edge, which is a panel that is not there. It was 280 during the
first frames and 0 once the window had settled, so read it from a `placed` line rather than the first. first frames and 0 once the window had settled, so read it from a `placed` line rather than the first.
(stderr, `UDPSCOPE_PROBE_LAYOUT_MS`) — **`buttons` must be `2N+1`** while the editor is (stderr, `UDPSCOPE_PROBE_LAYOUT_MS`) — **`buttons` must be `3P + 2F + 2`** while the editor is
up (one `×` per row, one `+` per boundary) and **0 on the `editor off` line**: the up, for `P` plots in `F` figure rows: one `×` per plot (`P`), one row `+` and one row resize arrow
buttons exist only while the editor does. `done` follows the same rule for the editor's per row boundary (`2(F+1)`, the arrows of the figure's two outer edges counted although hidden), and
per row of `n` cells `n + 1` cell `+` buttons and `n - 1` column arrows (`3P` in all, since
`sum(n+1) + sum(n-1) = 2P`, plus the `P` of the `×`). With no split that is **7 for one row and 12 for
two** (`5` and `8` before the split, the old `2N+1` having long stopped counting the arrows), and a
split row adds three per extra cell. **0 on the `editor off` line**: the
buttons exist only while the editor does. `rows=` is the plot count and `cells=[..]` the cells of each
figure row, comma-separated (`[1,1]` for two unsplit rows, `[1,2]` for a split second row);
`colweights=[..]` is each figure row's cell weights, `|` between rows and **empty for a row with none**
(every unsplit row, and a split row nobody dragged), so two unsplit rows read `[|]` and a dragged
`{3,1}` second row `[|3,1]`. An unsplit run must read `cells=[1,..,1]` and a `colweights` of
only separators; anything else is a split the run did not ask for. `done` follows the same rule for the editor's
own exit button (1 up, 0 off), and its `done_box` on the `placed` line must sit in the own exit button (1 up, 0 off), and its `done_box` on the `placed` line must sit in the
area's top-right corner: `x + W` within a few pixels of the area width and `y` within a area's top-right corner: `x + W` within a few pixels of the area width and `y` within a
few of 0 (the on-switch line reports a box from before the first frame, so read the few of 0 (the on-switch line reports a box from before the first frame, so read the
@@ -661,7 +671,7 @@ Two lines of the self-check output are assertions in disguise:
than by the first-run timer. than by the first-run timer.
- **The session layout round trip** (`~/.config/udpscope/<host>_<port>.toml`) — run once - **The session layout round trip** (`~/.config/udpscope/<host>_<port>.toml`) — run once
with a drop and a window change, then again with no interaction: the second run's dump with a drop and a window change, then again with no interaction: the second run's dump
must read `layout loaded from ...` and `layout: rows=R window_s=W row0=[...]` with the must read `layout loaded from ...` and `layout: rows=R window_s=W row0=[...] ... cells=[..] colweights=[..]` with the
first run's values, and the signals it names must come back *acquired* (a `row 0 slot 0 first run's values, and the signals it names must come back *acquired* (a `row 0 slot 0
-> channel ... samples=N` line). Delete the file before a run whose layout you did not -> channel ... samples=N` line). Delete the file before a run whose layout you did not
mean to inherit (`127.0.0.1_9540.toml` and friends accumulate otherwise). A **persistence mean to inherit (`127.0.0.1_9540.toml` and friends accumulate otherwise). A **persistence
@@ -2838,3 +2848,62 @@ any is a badge that does not say its panel is open, or one that stays marked aft
the plot's pan, drop and hover; `plot_unchanged=0` is the panel taking room from the plot. **Line 2:** `click_inside_closes=0` with `still_open=1` (a click the plot's pan, drop and hover; `plot_unchanged=0` is the panel taking room from the plot. **Line 2:** `click_inside_closes=0` with `still_open=1` (a click
inside the panel does not close it), `click_outside_closes=1` (a click on the plot does), `esc_closes=1` (`slideover_escape()` closes it) and `closed=1`. inside the panel does not close it), `click_outside_closes=1` (a click on the plot does), `esc_closes=1` (`slideover_escape()` closes it) and `closed=1`.
`esc_closes=1` is the function called directly; the key *through the window's handler* is the `esc:` reading of the `[keys]` line. `esc_closes=1` is the function called directly; the key *through the window's handler* is the `esc:` reading of the `[keys]` line.
## `[split-probe]`
```
[split-probe] start plots=1 frows=1 cells=[1] widths=[756] tile=1 x_equal=1 cursors_aligned=1 weights=[-] traces=[1:sine,square,ramp,noise]
[split-probe] split plots=2 frows=1 cells=[2] widths=[376,376] tile=1 x_equal=1 cursors_aligned=1 weights=[-] traces=[1a:sine|1b:square]
[split-probe] weights 3:1 plots=2 frows=1 cells=[2] widths=[533,220] tile=1 x_equal=1 cursors_aligned=1 weights=[3,1] traces=[1a:sine|1b:square]
[split-probe] caps-calls insert_cell=[1,1,0] refused_cell=1 insert_row=[1,1,0] refused_row=1 plots=8
[split-probe] roundtrip rt=1 expect=r0(h=2.0000)[w=1.0000:sine] r1(h=1.0000)[w=3.0000:square][w=1.0000:ramp] got=r0(h=2.0000)[w=1.0000:sine] r1(h=1.0000)[w=3.0000:square][w=1.0000:ramp]
[split-probe] ruler-labels split=[plot 1a,plot 1b,plot 2a,plot 2b] mixed=[row 0,plot 2a,plot 2b]
```
`UDPSCOPE_PROBE_SPLIT_MS` (stderr; see [verification.md](verification.md)). Nineteen steps, 250 ms apart;
each restructures and the *next* step reports the frame that was laid out with it, so a line's label
names the arrangement the line **reads**, not the call that made it. The state lines share one format:
`plots` (`row_count()`), `frows` (`figure_row_count()`), `cells=[..]` (per figure row), `widths=[..]`
(logical pixels of each cell, `|` between rows), `weights=[..]` (the stored cell weights, `-` for a row with
none), `traces=[label:signals|..]` with the labels `plot_label()` gives (`1`, `1a`, `2b`). Every state line
carries the same three assertions:
| field | value it must read | the defect a wrong reading names |
| --- | --- | --- |
| `tile` | **1** on every state line | a row whose cells do not fill its rect edge to edge, are not at its y and height, or are not one `col_gap` apart, or a `FigureLayout` with `cells.size() != plots` / `rows.size() != frows`: the plotter's placement or a host that did not rebuild the figure |
| `x_equal` | **1** | a plot whose x range differs from plot 0's: a cell that is not in the x link group (a rebuild that linked rows and forgot cells) |
| `cursors_aligned` | **1** | a plot whose two vertical cursors sit elsewhere than plot 0's: a per-cell cursor time instead of the one shared time; the probe turns the cursors on and resets them first so the comparison is of real positions |
The other lines, in order:
| line | value it must read | the defect a wrong reading names |
| --- | --- | --- |
| `start` / `split` | `cells=[1]` then `[2]` with `widths=[376,376]` (equal halves of 756) and the traces on `1a`/`1b`; a second cell starts **empty**, the probe assigns it | a split that moved or dropped the first cell's traces, or unequal default cells |
| `weights 3:1` | `widths=[533,220]`, **not** 3:1, and `tile=1` | not a defect: the plotter's 64 px floor per cell is taken off first and the weights share the remainder, so the shares read compressed; a result of exactly 3:1 would be the floor missing |
| `row+cell` | `cells=[2,2]`, `weights=[3,1\|-]` | row 0's cell weights lost or moved when a row and a cell were inserted below it (they belong to the figure row) |
| `caps-calls` | `insert_cell=[1,1,0] refused_cell=1 insert_row=[1,1,0] refused_row=1 plots=8` | a cap that is not reached or not held: row 0 starts with two cells and takes two more, refusing the third call at 4; two rows then fit before the plot total reaches 8 and the third is refused; `plots` must stop at **8** |
| `caps` | `plots=8 frows=4 cells=[4,2,1,1]`, `tile=1` | a ninth plot, or a fifth cell in a row |
| `remove` | `plots=7 cells=[3,2,1,1]` | `remove_cell()` on a multi-cell row that removed the row, or that dropped a neighbour's traces |
| `roundtrip-io` | `wrote=1 read=1 plots_in_file=3` | a save that was refused, or a file that lost a `[[plot]]` table |
| `roundtrip` (state line) | `cells=[1,2] widths=[756\|533,220] weights=[-\|3,1]` and the three traces on `1`, `2a`, `2b` | the arrangement did not survive `collect_layout()` -> file -> `apply_layout()` |
| `roundtrip rt=` | **`rt=1`** with `got=` equal to `expect=` (row weights `h=`, cell weights `w=` and each cell's trace, read back at the file's `%.4f`) | **`rt=0`** names the difference: a weight written per plot index instead of per figure row, widths applied before the arrangement (which resets them), or a trace that moved to another cell |
| `legacy-io` | `read=1 plots_in_file=3 row_unset=1` | the stripped file still carrying a `row`, which would make the next line test nothing |
| `legacy` | `cells=[1,1,1]`, `frows=3`, `widths=[756\|756\|756]`, `traces=[1:sine\|2:square\|3:ramp]` | a session from before the split that did not open as one cell per row, or lost its signals |
| `invalid-rows` | `ok=1 frows/plots=[5/5 3/3 3/3 3/3]` | one of four unusable `row` sequences (five cells in a row; a skipped row; a row that goes back; a first row that is not 0) that was refused, or kept as a split instead of loading one cell per row |
| `editor-on` -> `cell-split` | `cells=[1]` then `cell-split-ck cells=2` and `cell-split` with `widths=[376,376]`, the new `1b` empty | the editor's cell `+` not reaching `insert_cell()`, or the figure not re-pointed after it |
| `cell-assigned` | `1a:sine\|1b:square` | an assignment into the new cell that landed on another plot |
| `col-drag` | `left_gt=1 both_above_min=1 left_gt_right=1 tile=1`; `pair_sum` constant (753 at the 756 px window) | the column drag not moving the boundary (`left_gt=0`), a cell dragged below `kMinCellPx` (`both_above_min=0`), or a drag that changes the pair's total instead of redistributing it. The probe applies +80 px, then +/-10000 px through `layout_col_apply()` |
| `pre-insert0` (state line) | after the extreme drags `widths=[144,609]` | not a defect: the drag floors the left share at 96 px of the pair, but the plotter realises `64 + share * remainder`, so a cell dragged to the limit reads wider than 96 (see [invariants-plot-and-layout.md](invariants-plot-and-layout.md)) |
| `insert0-move` | `move_ok=1 before=[sine,square] after=[,sine,square]` | an insert at the left edge that renumbered the cells instead of moving their traces right with them |
| `remove-mid` | `rm_ok=1 after=[,square]` | removing a middle cell that dropped or shifted the wrong neighbour |
| `row-weights` | `wt_ok=1 before=[3,1,1] after=[1,1]` | the row weights not following an insert or removal of a *figure row* (the vector had the plot-count length once and drifted off the rows) |
| `sensitivity` | `cell_plus_sensitive=0 row_plus_sensitive=0 plots=8` | `-1` is a button the probe could not find; `1` is a `+` left clickable at the 4-cell cap (cell) or at 8 plots (row) |
| `two-split-rows` | `cells=[2,2]`, labels `1a 1b 2a 2b`, all empty | `set_cells_per_row()` leaving the old plots' contents or labels behind |
| `drop-rowat` | `ok=1 rowat=[1a=0,1b=1,2a=2,2b=3]` | `PlotArea::row_at(x, y)` choosing by y alone, which would put every cell of a row on its first plot: a drop or a right click on `1b` landing in `1a` |
| `plot-on-buttons` | `[1a:row 1, cell 1 of 2\|1b:row 1, cell 2 of 2\|2a:row 2, cell 1 of 2\|2b:row 2, cell 2 of 2]` | a signals-page button labelled with the flat index, or without the cell tooltip; a one-cell row keeps its plain label and the tooltip "draw this signal on this plot" |
| `assign-1b` / `drop-2b` | `assigned=1 notice='plotting 'sine' on plot 1b'`; `dropped=1 notice='plotting 'square' on plot 2b'` | a notice that names the plot by number alone |
| `after-assign` (state line) | `traces=[1a:-\|1b:sine\|2a:-\|2b:square]` | the signal went to a neighbouring cell |
| `ruler-labels` | `split=[plot 1a,plot 1b,plot 2a,plot 2b]` and, with cells `{1,2}`, `mixed=[row 0,plot 2a,plot 2b]` | the cursors pane's per-plot ruler line (a label read through the pane's own widgets): a split row's plots are `plot <label>`, an unsplit row keeps `row <plot index>` (**0-based**, as it always was -- `row 0` is the first plot, not a defect) |
The run ends with the usual `gtk warnings: 0` and no `Gtk-CRITICAL`. `x_equal=1` and `cursors_aligned=1` on every state line are
the only readings of the shared time window across cells.
+22
View File
@@ -26,6 +26,15 @@ shortens `plot=` by the 36 px bar; and the `tour started: steps=` count. A self-
`c572ec2` is no longer the baseline for those lines. The rest is the mock's own jitter (the sign of a `c572ec2` is no longer the baseline for those lines. The rest is the mock's own jitter (the sign of a
`square y=[...]` or a `trig engine:` figure, the position of one `trigger: edge` line), which the `square y=[...]` or a `trig engine:` figure, the position of one `trigger: edge` line), which the
normalisation does not remove -- anything else is a finding. normalisation does not remove -- anything else is a finding.
For the horizontal plot split the same diff is taken against the commit before it (`18b91c8`, with
`deps/plotter` at its pre-split pin `f79a97b`), and it is "unchanged except" exactly two things on the
layout lines: **`buttons=`** grows (the editor now carries the cell `+` buttons and the column arrows:
`3P + 2F + 2` for `P` plots in `F` figure rows, so `5 -> 7` for one row and `8 -> 12` for two), and the
layout probe line and the exit dump's layout line gain the **`cells=[..] colweights=[..]`** fields.
Everything else -- cursors, measurements, readouts, signal pages, math, trigger, help figures, the
E-1608 -- reads as before; a difference there is a finding, not the feature. Take a second baseline run
as a jitter control before believing one (a few mock readings, such as the sine's membership of the
cursors mix list or a V/mV flip, move between two runs of the *same* build).
A clean build emits **one** warning and no more: a `-Woverlength-strings` on the icons' A clean build emits **one** warning and no more: a `-Woverlength-strings` on the icons'
generated GResource `.c`, whose blob is **9248** against C99's 4095-byte string-literal limit with generated GResource `.c`, whose blob is **9248** against C99's 4095-byte string-literal limit with
the five action icons and the two app icons that remain (a note about a generated file, which GCC the five action icons and the two app icons that remain (a note about a generated file, which GCC
@@ -196,6 +205,19 @@ precedent, not an exception.
with, so a synchronous sequence would read every position one action stale. It clicks by with, so a synchronous sequence would read every position one action stale. It clicks by
emitting `::clicked`, never `gtk_widget_activate()`, which holds a `GtkButton` for its emitting `::clicked`, never `gtk_widget_activate()`, which holds a `GtkButton` for its
250 ms `ACTIVATE_TIMEOUT` and then emits into a button the rebuild has already destroyed. 250 ms `ACTIVATE_TIMEOUT` and then emits into a button the rebuild has already destroyed.
- `UDPSCOPE_PROBE_SPLIT_MS` — drive the horizontal plot split, 19 steps 250 ms apart (so a run needs
`UDPSCOPE_PROBE_SPLIT_MS=1500 UDPSCOPE_EXIT_AFTER_MS=7500` at least; it enables and assigns the
signals itself, so `UDPSCOPE_ENABLE_ALL_MS` is not needed). Each step restructures the cells,
re-points the area at the new figure and lets the *next* step read a frame that was actually laid out
with them, which is why every report line describes the previous step's change. It splits and
weights a row, fills the caps, round-trips a session through `collect_layout()` / `save_layout()` /
`load_layout()` / `apply_layout()` (and a copy with its `row`/`width` lines stripped), clicks the layout
editor's real cell `+` button, drives the column drag handler, and checks the signals page, the drop's
`row_at()` and the cursors pane's ruler labels. Run it once, serially, on its own mock port, from a
fresh `XDG_CONFIG_HOME` (the probe deletes the temp files `udpscope-split-probe.toml` and `.legacy` it
writes under `g_get_tmp_dir()`, but the editor's clicks schedule a real session save). Read it against
the `[split-probe]` section of [probe-readings.md](probe-readings.md); the run must still end with
`gtk warnings: 0`.
- `[levels-probe] N measurement(s) over K channel(s), shares='...' -> W window(s), L level - `[levels-probe] N measurement(s) over K channel(s), shares='...' -> W window(s), L level
walk(s)` (stderr, `UDPSCOPE_PROBE_LEVELS_MS`) — **prices a measurement pane**, which is what a walk(s)` (stderr, `UDPSCOPE_PROBE_LEVELS_MS`) — **prices a measurement pane**, which is what a
report of "it is slow with many measurements" has to become before it is a number. It adds one report of "it is slow with many measurements" has to become before it is a number. It adds one
+40 -14
View File
@@ -5,8 +5,8 @@
<info> <info>
<link type="guide" xref="index#main"/> <link type="guide" xref="index#main"/>
<link type="seealso" xref="signals"/> <link type="seealso" xref="signals"/>
<desc>How the figure's rows and traces are arranged, how to drop a signal onto a row, and <desc>How the figure's rows, plots and traces are arranged, how to drop a signal onto a plot, and
how the layout editor adds and removes rows.</desc> how the layout editor adds and removes rows and splits a row into plots side by side.</desc>
<credit type="author copyright"> <credit type="author copyright">
<name>Martino Ferrari</name> <name>Martino Ferrari</name>
</credit> </credit>
@@ -14,9 +14,10 @@
<title>Rows, layout and drag &amp; drop</title> <title>Rows, layout and drag &amp; drop</title>
<p>The figure is a stack of rows, 1 to 8 of them, each holding up to 4 traces. Rows share <p>The figure is a stack of rows holding 1 to 8 plots in all, and a row can be split into up
one time axis and one pair of side margins, so their y labels line up, their data areas to 4 plots side by side. Each plot holds up to 4 traces. Every plot shares one time axis and
start and end on the same two columns, and their windows cannot drift apart. The left the figure has one pair of side margins, so the y labels line up, the data areas start and
end on the same two columns, and the windows cannot drift apart. The left
margin holds the y labels; the right one is empty, and is there so the newest tick's label margin holds the y labels; the right one is empty, and is there so the newest tick's label
and the newest sample's ink have somewhere to land instead of sitting on the frame.</p> and the newest sample's ink have somewhere to land instead of sitting on the frame.</p>
@@ -29,7 +30,7 @@
</item> </item>
<item> <item>
<p>Press <key>l</key>, or pick <gui>Layout editor</gui> from the overflow menu, to add <p>Press <key>l</key>, or pick <gui>Layout editor</gui> from the overflow menu, to add
and remove rows: a <gui>×</gui> appears at the top left of each row and a <gui>+</gui> and remove rows: a <gui>×</gui> appears at the top left of each plot and a <gui>+</gui>
on each boundary between rows, including above the first and below the last.</p> on each boundary between rows, including above the first and below the last.</p>
</item> </item>
<item> <item>
@@ -38,6 +39,29 @@
</steps> </steps>
</section> </section>
<section id="split">
<title>Splitting a row</title>
<steps>
<item>
<p>In the layout editor, click the <gui>+</gui> at a row's left or right edge, or the
<gui>+</gui> between two plots, to add a plot to that row. The new plot starts empty.</p>
</item>
<item>
<p>Drag the arrow on the boundary between two plots to resize them. Neither plot can be
made narrower than about 96 px, and the widths are saved with the session.</p>
</item>
<item>
<p>Click the <gui>×</gui> of a plot in a split row to remove that plot and its traces.
In a row with a single plot the <gui>×</gui> removes the row instead.</p>
</item>
</steps>
<p>All the plots of a row, and of the whole figure, share one time window: zooming or
panning along time in any of them moves all of them, and the vertical cursors are drawn in
every plot. The y axes, the horizontal cursors and the fits belong to each plot. Only the
bottom row carries the time labels. A row holds at most 4 plots and the figure at most 8
in total; the <gui>+</gui> buttons go grey at the limit.</p>
</section>
<section id="knowing"> <section id="knowing">
<title>Worth knowing</title> <title>Worth knowing</title>
<list> <list>
@@ -47,10 +71,11 @@
<gui>Back</gui> row to return. That is how a trace comes off a row without going through <gui>Back</gui> row to return. That is how a trace comes off a row without going through
the layout editor.</p></item> the layout editor.</p></item>
<item><p>A drop <em>adds</em> a trace to the row's next free slot; it does not replace <item><p>A drop <em>adds</em> a trace to the row's next free slot; it does not replace
one. With four traces already on a row the drop is refused.</p></item> one. With four traces already on a plot the drop is refused.</p></item>
<item><p>Inserting a row moves the rows below it <em>with their contents</em>. Inserting <item><p>Inserting a row moves the rows below it <em>with their contents</em>, split
above row 1 puts what row 1 plotted onto row 2 and leaves the new row empty — the rows rows included. Inserting above row 1 puts what row 1 plotted onto row 2 and leaves the
are not renumbered under you.</p></item> new row empty — the rows are not renumbered under you. A split row's plots are named
<em>2a</em>, <em>2b</em> and so on.</p></item>
<item><p>A trace is remembered by the signal's <em>name</em>, not by its channel number, <item><p>A trace is remembered by the signal's <em>name</em>, not by its channel number,
because a new CONFIG or an enable renumbers the channels and an index would silently because a new CONFIG or an enable renumbers the channels and an index would silently
re-point the trace.</p></item> re-point the trace.</p></item>
@@ -62,10 +87,11 @@
<section id="example"> <section id="example">
<title>Example</title> <title>Example</title>
<p>With three traces on row 1, pressing <key>l</key> shows 3 buttons (one <gui>×</gui>, <p>With three traces on row 1, pressing <key>l</key> shows 7 buttons: the <gui>×</gui>,
two <gui>+</gui>). Clicking the <gui>+</gui> above row 1 gives 2 rows and 5 buttons, with the two <gui>+</gui> above and below the row with their two resize arrows, and the two
the three traces now on row 2 — the layout probe reports exactly this as <gui>+</gui> at the row's left and right edges. Clicking the <gui>+</gui> above row 1 gives 2 rows and 12 buttons,
<code>rows=2 buttons=5 traces=[0:-|1:sine,ramp,noise]</code>.</p> with the three traces now on row 2 — the layout probe reports exactly this as
<code>rows=2 buttons=12 traces=[0:-|1:sine,ramp,noise]</code>.</p>
</section> </section>
</page> </page>
+4 -3
View File
@@ -69,9 +69,10 @@ struct HelpHint {
/// the page is on screen, so the dock needs no hint of its own and the layer has one code path. /// the page is on screen, so the dock needs no hint of its own and the layer has one code path.
constexpr HelpHint kHelpHints[] = { constexpr HelpHint kHelpHints[] = {
{"plot", "The plot", {"plot", "The plot",
"Every signal you have switched on is drawn here, one row per group of traces, with all rows " "Every signal you have switched on is drawn here, one plot per group of traces, with all plots "
"sharing this one time axis. The wheel zooms about the pointer, a drag pans, and a right-click " "sharing this one time axis. The layout editor (L) adds rows and splits a row into plots side "
"on a row opens that plot's own menu.", "by side. The wheel zooms about the pointer, a drag pans, and a right-click on a plot opens "
"its own menu.",
"", &App::area_overlay}, "", &App::area_overlay},
{"trigger", "The trigger", {"trigger", "The trigger",
"Freezes the view on an event you describe: a signal, a condition (an edge, a pulse width, a " "Freezes the view on an event you describe: a signal, a condition (an edge, a pulse width, a "
+1 -1
View File
@@ -104,7 +104,7 @@ void clear_box(GtkWidget* box) {
/// The strip's cell widths, in characters. Fixed like the dock's, so the strip's chips are as wide with /// The strip's cell widths, in characters. Fixed like the dock's, so the strip's chips are as wide with
/// `--` as with `-123.456 mV` (what does not fit scrolls). /// `--` as with `-123.456 mV` (what does not fit scrolls).
constexpr int kStripWhoChars = 6; constexpr int kStripWhoChars = 7;
constexpr int kStripTypeChars = 10; constexpr int kStripTypeChars = 10;
/// A flat text button that opens a slide-over page. It only opens a page, so a rebuild can never free the /// A flat text button that opens a slide-over page. It only opens a page, so a rebuild can never free the
+1 -1
View File
@@ -441,7 +441,7 @@ struct TourStep {
constexpr TourStep kTourSteps[] = { constexpr TourStep kTourSteps[] = {
{"plot", nullptr, "The plot", {"plot", nullptr, "The plot",
"Every signal you have switched on is drawn here, all rows sharing one time axis. The wheel " "Every signal you have switched on is drawn here, all plots sharing one time axis. The wheel "
"zooms, a drag pans, and a right-click on a row opens that plot's own menu.", "zooms, a drag pans, and a right-click on a row opens that plot's own menu.",
&App::area_overlay}, &App::area_overlay},
{"badges", nullptr, "The badges", {"badges", nullptr, "The badges",