Compare commits
19
Commits
4235ee6225
...
cc8335c514
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc8335c514 | ||
|
|
d9287ea826 | ||
|
|
8c9c6ff7fd | ||
|
|
574f47f769 | ||
|
|
75aa62cc3c | ||
|
|
dbe6377415 | ||
|
|
e615465366 | ||
|
|
0b146f1d53 | ||
|
|
f9cea4885b | ||
|
|
6492bb910f | ||
|
|
cb866774c6 | ||
|
|
a493737ca1 | ||
|
|
132041f79e | ||
|
|
d2ec89d866 | ||
|
|
619b5e356c | ||
|
|
f224f56c4c | ||
|
|
44d2fd35dc | ||
|
|
e10a92fc52 | ||
|
|
a519e13b67 |
No files matched your search
@@ -2388,6 +2388,25 @@ now — they are listed so they are not mistaken for anything else:
|
||||
progress. What the spec was protecting still holds — a *held* event is bound to its
|
||||
snapshot alone, and the axis is the event's own window from the edge onwards rather
|
||||
than a sliding live view.
|
||||
- **The sidebar is two tabs, and a math channel is a row of the signal list.** `SPECS.md`
|
||||
names the panes as separate collapsible sections — trigger, cursors, measurements, math,
|
||||
signal list — in one scrolling column; with four of them open in a 320 px sidebar every
|
||||
section was short of room, and finding one meant scrolling past the others. The built
|
||||
sidebar is a **Measure** tab (the cursors readout and the measurements) and a **Signals**
|
||||
tab (the trigger and the channels), **each a pill with its own glyph and no text** — the
|
||||
cursors and the ruler for Measure, a sine crossing its trigger level and the channel list
|
||||
for Signals (the same picture the toolbar's trigger button carries), drawn as a
|
||||
32x16 pair so both motifs are legible at a glance — with the words kept as each pill's tooltip and
|
||||
accessible name. 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
|
||||
feature is off is a pane whose controls you would otherwise have to leave to turn it back on. The
|
||||
design spec had the switcher showing *titles* and neither page an
|
||||
`icon-name`; `narrow` alone does not do it (libadwaita still draws a label beside the icon,
|
||||
measured), so the pages carry an empty title and the pills are named in code. 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
|
||||
the model, so the two lists were the same list drawn twice. A tab is one-of-*N*, so no
|
||||
toggle hides a pane any more: trigger-off and cursors-off **disable** their pane and leave
|
||||
it in place, and the measurements' and math panes' toolbar toggles are gone with the panes.
|
||||
- **A plot holds at most eight signals** (`RowSet::kSlotsPerRow`), because a `Plot`
|
||||
cannot give up a trace and the slots are therefore created up front.
|
||||
- **One toggle drives both cursor orientations.** The vertical markers and the
|
||||
|
||||
@@ -49,58 +49,40 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
one of them with reality): it is computed on the first *tick*, because the dump
|
||||
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*
|
||||
(Fit, Export) and the two editors that are not panes (Layout, Signal list). The Arm/Disarm button in the trigger panel still has text.
|
||||
(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.
|
||||
- **Seven 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
|
||||
measurement ruler, and the stock names those buttons used to carry said something else
|
||||
entirely: a *record dot* for the trigger and three different **magnifiers** for cursors,
|
||||
measurements and math. `src/icons/scalable/actions/udpscope-{trigger,cursors,measure,math,events}-symbolic.svg`
|
||||
are the five action icons; beside them in `src/icons/scalable/apps/` are the two
|
||||
**application** icons, `org.udpscope.app.svg` (128px colour artwork) and
|
||||
`org.udpscope.app-symbolic.svg` (16px). The `apps` context is where an icon theme keeps an
|
||||
application's own icon and is where the **in-process** window icon resolves from; it is also
|
||||
the layout that `cmake/install_desktop_entry.cmake` mirrors when it copies the same two files
|
||||
into `$XDG_DATA_HOME/icons/hicolor/scalable/apps` and `.../symbolic/apps` — those installed
|
||||
copies are what makes `Icon=org.udpscope.app` in the desktop entry resolve (a desktop shell
|
||||
cannot read a GResource compiled into the binary).
|
||||
All six are bundled by `src/icons/udpscope.gresource.xml` (CMake runs
|
||||
`glib-compile-resources`) and reached through **one**
|
||||
`gtk_icon_theme_add_resource_path()` in `run_ui()`, right after `adw_init()` — not at the top
|
||||
of `build_toolbar()`, where it used to be: there are two consumers now (the window's own icon,
|
||||
set before the toolbar is built, and the toolbar's buttons) and `run_ui()` is the one place
|
||||
ahead of both. `adw_init()` calls `gtk_init()`, which is what makes
|
||||
`gdk_display_get_default()` valid there.
|
||||
A resource rather than files beside the binary because udpscope is run straight out of the
|
||||
build tree and has no install step. Five authoring rules, each of which fails quietly:
|
||||
* **GTK's symbolic recolour only reaches `fill` on `path`, `rect` and `circle`.** It wraps the
|
||||
SVG in a stylesheet for those selectors, so a `stroke`, a `line` or a `polyline` keeps the
|
||||
colour written in the file and stays dark on a dark theme. **In a symbolic icon everything is
|
||||
therefore a filled `path`** — including anything that wants to look stroked, which is drawn as
|
||||
a closed band (two offset curves joined) instead. The rule is scoped to symbolic icons on
|
||||
purpose: `org.udpscope.app.svg` is 128px colour artwork that is never recoloured, so it strokes
|
||||
its sine freely, which is the sane way to draw a smooth one. Do not "fix" one to match the
|
||||
other.
|
||||
* **Whole-pixel coordinates**, because these are drawn at 16 px far more often than not: the
|
||||
first draft placed features on half-pixels and the whole icon was a grey smear at 1x.
|
||||
* `fill-rule="evenodd"` is how the ruler's body is a hole *and* its ticks still fill -- a tick
|
||||
inside the hole is three crossings deep, which is odd. Nonzero would fill the hole back in.
|
||||
* Overlapping subpaths under the default nonzero rule must be **wound the same way** or they
|
||||
cancel instead of uniting. All of them run clockwise.
|
||||
* **A malformed SVG is a build error, and that is the only place it can be caught.** Every
|
||||
`<file>` in `src/icons/udpscope.gresource.xml` carries `preprocess="xml-stripblanks"`, which
|
||||
makes `glib-compile-resources` pipe it through `xmllint` — so `--` inside an XML comment, which
|
||||
is illegal and which librsvg refuses the whole file for, fails the build with a file, a line
|
||||
and a caret under the offending character (`…app-symbolic.svg:5: parser error : Double hyphen
|
||||
within comment`). It has to be here because **no runtime check can see it**: librsvg refuses
|
||||
the file but the icon *name* still resolves, so `gtk_icon_theme_has_icon()` returns true and
|
||||
the icon draws nothing. `gtk_icon_theme_lookup_icon()` cannot tell either (see the `app icons:` line under
|
||||
Verification), `GMarkup` accepts the double hyphen because it is a lenient subset parser, and
|
||||
`gdk_pixbuf` goes through glycin here, which sandboxes a subprocess per load and refuses this
|
||||
project's own icons outright. The cost is that the `xmllint` binary is a build dependency; it
|
||||
ships with libxml2, which GTK already requires.
|
||||
`gtk_icon_theme_has_icon()` is checked for every toolbar button and reported by the dump (see
|
||||
`toolbar icons:` in [probe-readings.md](probe-readings.md)): a resource path that was never added does not raise an
|
||||
error, it draws `image-missing`.
|
||||
measurements and math. `src/icons/scalable/actions/udpscope-{trigger,cursors,measure,events,resize-rows,tab-measure,tab-signals}-symbolic.svg`
|
||||
are the seven action icons. `udpscope-math-symbolic` left with the toolbar toggle that was its last
|
||||
user; `udpscope-measure-symbolic` **stayed**, because the Preferences window's Plot page carries it --
|
||||
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, not the tab's.
|
||||
|
||||
The two **tabs** got glyphs of their own, and each is a *pair* rather than one of the icons above.
|
||||
`udpscope-tab-measure-symbolic` is two cursors beside the ruler they read against — each a cap over two
|
||||
bold dashes, beside a ruler of two bands with three marks between them and **no end caps** (a closed
|
||||
frame reads as a rectangle, and costs a fifth of the ink on two lines that say nothing) — and
|
||||
`udpscope-tab-signals-symbolic` is a sine crossing its dashed level, beside two bold channel rows. They
|
||||
are not reuses, because a glyph the toolbar already carries means two things the moment a tab shows it
|
||||
too; instead the *trigger's* own icon (`udpscope-trigger-symbolic`) was redrawn to the Signals pill's
|
||||
picture, so the pill and the button that turns the trigger on are recognisably about the same thing.
|
||||
The wave is a **full period and not a hump**: a crest and a trough are what make it read as a wave
|
||||
rather than as a hill, and the level's dashes survive between the crossings. (A hump was tried and
|
||||
reverted — it named the "above the level" half of a trigger and lost the crossings, which is the half
|
||||
that matters.)
|
||||
Both are **32x16** composites drawn **bold** — one thick band, two thick dashes, few features — because
|
||||
at this size a thin wave or a 1px tick reads as *texture* rather than as a shape, which is what "not
|
||||
clear" looks like. Wider than tall on purpose: a 16px square cell carries one motif, not a pair. And
|
||||
**GDK paints an icon into a square box whatever the document's aspect** (measured:
|
||||
`gtk_icon_theme_lookup_icon()` at 32 reports `32x32`, and librsvg fits it to `32x16`), so the pair is
|
||||
letterboxed rather than stretched — and the pill's image is given `pixel_size` 32 in `name_side_tabs()`,
|
||||
which draws the artwork at the size it is written, so its bars stay crisp. Every glyph here is
|
||||
**inset two pixels**, because art flush with the cell edge is what a button's padding or a crop takes a
|
||||
pixel off, and a fractional scale is what would blur it, which is why nothing is drawn at 24 or 48. The
|
||||
two tab glyphs measure 29% and 30% ink, inside the range the icons above them set.
|
||||
- **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
|
||||
with `gtk_accelerator_parse()` — so they are `"space"`, `"<Control>e"`, `"<Control>slash"`,
|
||||
@@ -179,6 +161,39 @@ 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
|
||||
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.
|
||||
**The sidebar is a tab stack now, and has no `GtkRevealer` left in it**: an `AdwViewSwitcher`
|
||||
above an `AdwViewStack`, each page a `GtkScrolledWindow` of its own (the two pages are different
|
||||
heights, so one shared scroller would leave a switch scrolled where the new page never asked to
|
||||
be). The reason is the one this trap teaches: five sections competing for one 320 px column left
|
||||
every one of them short of room. A *feature's* on/off is no longer a visibility at all -- the
|
||||
trigger's and the cursors' toggles set their pane's **body** insensitive, so the pane stays where it
|
||||
is and the settings you are about to switch back on do not vanish while you look for them. The *body*
|
||||
and not the pane, because each pane's heading carries its own switch (see the next bullet). The
|
||||
trap itself does not go away with the widget: a stack's hidden page measures 0x0 too and is still
|
||||
given the leftover width, so **ask about state, not about size** carries over unchanged. Every
|
||||
allocation read in the sidebar is a debug capture, and each reads 0 on the page that is *not*
|
||||
current -- a change in what the reading means, not a defect, and no functional code may depend on
|
||||
those numbers.
|
||||
- **`AdwViewSwitcherPolicy.narrow` still draws a label; an *empty title* is what makes a pill an icon.**
|
||||
Measured on libadwaita 1.10: with `narrow` and a title, the pill holds a `GtkImage` **and** a
|
||||
`GtkLabel` — 48px of "Measure" inside a 54px pill — so the policy alone does not give an icon-only tab
|
||||
bar. The sidebar's pages therefore have **no title** (their `name` is what identifies them to
|
||||
`select_side_tab()` and the session file) and the pills are *named in code* instead: the first tick
|
||||
calls `name_side_tabs()`, which walks the switcher's children — the pills are there in page order, and
|
||||
`AdwViewSwitcherButton` is not a public type, so there is no type check to make — and sets each pill's
|
||||
**tooltip and accessible name** from the page's name, then hides the empty label that would otherwise
|
||||
leave the icon off-centre. An icon-only pill is still a named control for a screen reader, and
|
||||
`switcher: … pills=2 labels=0 names=2` is the reading for all three facts at once.
|
||||
- **A pane's heading switch and the header bar's button are one control in two places.** Each pane's
|
||||
heading (`build_trigger_panel()` builds the trigger's, `cursors-panel.ui` has the cursors' in its
|
||||
`head`) carries a `GtkSwitch` that runs the *same action* the toolbar button runs, and
|
||||
`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:
|
||||
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
|
||||
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
|
||||
sensitive target is the pane's **body** (`trigger_body`, `cursors_body`) and not the pane.
|
||||
- **A list box outlives the children of the pane it sits in, and the teardown order is not yours to
|
||||
choose.** Tearing the window down finalizes the events pane's buttons, then the list box, and the
|
||||
list's own `selected-rows-changed` then reaches a handler holding the freed pointers: four
|
||||
|
||||
@@ -301,6 +301,17 @@ with at most the transient a re-start costs. The exit dump's `math '<name>': fin
|
||||
the assertion: `finite` must be about `M` minus the hole share, and `finite=0` beside non-zero
|
||||
`hole_slots` is this defect returned.
|
||||
|
||||
- **A math channel is a row of the *signal* list, under a `MATH` divider, and there is no math pane.**
|
||||
`append_math_signals_locked()` already puts every channel in `signals()`, so the pane was the same
|
||||
list drawn twice; the divider is what the pane uniquely contributed. It is non-selectable and
|
||||
non-activatable -- a heading that highlighted under the pointer would read as a row you can plot --
|
||||
and it carries the `+` that creates a channel, appended even when there are none, because a `+` that
|
||||
waited for a channel to exist could never create the first one. A math row's menu is *Edit graph…*,
|
||||
*Default colour*, *Delete*: no Information and no Calibrate, because calibration is applied at
|
||||
ingest and a derived channel has no stream to describe, and no rename, because the node editor owns
|
||||
a channel's name and unit and a second place to change them would be a second answer to the same
|
||||
question. A row's tick is the channel's own switch and must reach `App::math` -- the copy the
|
||||
session file is written from -- or the list and the file disagree about what "enabled" was.
|
||||
- **A graph *edit* does not move the plan; adding or removing a channel does.** The plan is a partition over
|
||||
the channels, so `set_math_channels()` takes a surgical path whenever the *set* is unchanged -- same channels,
|
||||
names and enables: the store is never touched, every other channel keeps its samples **and** its runtimes, and
|
||||
|
||||
@@ -54,6 +54,12 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
|
||||
unit and a spaced `m` alone reads as metres; with one it stays spaced and the prefix joins the
|
||||
unit (`636.62 mV`). Below `1e-12` the ladder runs out, and `%.2f` would print `0.00` again, so
|
||||
that range falls back to `%.3g`.
|
||||
- **With the instruments off the readout blanks, and the panel stays on screen.** Cursors-off makes the
|
||||
pane *insensitive* rather than hiding it -- a panel you can still see is a panel you can read, and the
|
||||
settings you are about to switch back on must not vanish while you look for them -- and every cell in
|
||||
it is written `"--"`, the spelling an unavailable cell already uses. A readout quotes a stored sample;
|
||||
with nothing bracketing one there is no sample to quote, and keeping the last values would be quoting
|
||||
a sample the cursors no longer name. Two spellings of one state in one grid would be the defect.
|
||||
- **The cursors are read back from the plots only while a gesture is on that row.** `Cursor::x`
|
||||
is the *free* pointer position while a drag runs and the snapped value otherwise -- which is
|
||||
what the app itself wrote a frame earlier, or the snap of it. A tick that reads a row on
|
||||
|
||||
@@ -161,10 +161,12 @@ Two lines of the self-check output are assertions in disguise:
|
||||
*adoption* half -- a gesture that does move a cursor, and the shared value reaching every row
|
||||
-- is asserted in the `check_host` harness, which can call `sync_cursors(row)` directly; a
|
||||
synthetic press here cannot reach the adapter's own event pump.)
|
||||
- `[cursors-probe] <action> mode=.. on=D pane=D t0=.. t1=.. dt=.. T=[..] lines=N [..]`
|
||||
- `[cursors-probe] <action> mode=.. on=D pane_sensitive=D t0=.. t1=.. dt=.. T=[..] lines=N [..]`
|
||||
(stderr, `UDPSCOPE_PROBE_CURSORS_MS`) — walks `vertical` → `horizontal` → `both` →
|
||||
`mix`, then moves a marker *through the plot* (`Plot::set_cursor_x(..., Free)`, which is
|
||||
what a drag does) and reports again. **The times must survive every mode change**: they
|
||||
what a drag does) and reports again. `pane_sensitive` is the cursors *panel body's* sensitivity, not a
|
||||
revealer's child: with the cursors off the panel stays exactly where it is and its cells blank to `--`,
|
||||
because a readout whose instruments are off has no sample to quote. **The times must survive every mode change**: they
|
||||
are the `RowSet`'s one pair, and a mode that parks the markers must not read them back
|
||||
(a parked cursor reports a real number, so a read-back would adopt it as a gesture — the
|
||||
bug that made `T0` read 0). **`lines`** must be 0 for `vertical`, one per row for
|
||||
@@ -195,7 +197,7 @@ Two lines of the self-check output are assertions in disguise:
|
||||
numbers and the plot's state agreeing is the difference between "the app placed them" and
|
||||
"the app placed them and they are on screen".
|
||||
- `[measures-probe]` + the dump's `measure N: 'sig' type thr=..% above=D show=D -> ok|invalid value text=[..]
|
||||
[duty=..] [marks=..]` and `measures pane: n=N revealed=D marks_shown=M pane_rows=R pane_w=W
|
||||
[duty=..] [marks=..]` and `measures pane: n=N marks_shown=M pane_rows=R pane_w=W
|
||||
pane_h=H switch_on=X reset_sensitive=Y`
|
||||
(stderr, `UDPSCOPE_PROBE_MEASURES_MS`) — adds one measurement of every type on the first
|
||||
plottable signal and lets the tick compute them, so what it prints is what a user reads.
|
||||
@@ -345,9 +347,9 @@ Two lines of the self-check output are assertions in disguise:
|
||||
and an expanding one is measured into the title's centre box, so it stretched from the
|
||||
label to the title (516 px at a 1270 px bar before the fix, 121 px after). A `W` that
|
||||
grows with the window width is that bug back.
|
||||
- `toolbar icons: trigger=NAME(ok) pause=... cursors=... measures=... math=... sidebar=...
|
||||
menu=...` (in the exit dump, beside `panel controls:`) — the icon name every toolbar button
|
||||
carries, with `gtk_icon_theme_has_icon()`'s verdict. **Every entry must read `(ok)`.** Four
|
||||
- `toolbar icons: trigger=NAME(ok) events=... pause=... cursors=... sidebar=... menu=...` (in the
|
||||
exit dump, beside `panel controls:`) — the icon name every toolbar button
|
||||
carries, with `gtk_icon_theme_has_icon()`'s verdict. **Every entry must read `(ok)`.** Three
|
||||
of the names are this app's own (`udpscope-trigger-symbolic` and friends, a `GResource`
|
||||
compiled into the binary — see the toolbar-icon invariant in
|
||||
[invariants-gtk-and-chrome.md](invariants-gtk-and-chrome.md)), and a `(MISSING)` on one of
|
||||
@@ -355,6 +357,27 @@ 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
|
||||
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.
|
||||
- `switcher: policy=narrow pills=2 labels=0 names=2 icon=32x32 size=308x36` (in the exit dump, beside
|
||||
`panels:`) — the two
|
||||
sidebar tabs as the switcher actually draws them. **`pills=2 labels=0`** is the claim: two pills are on
|
||||
screen and **no text is drawn** (`labels` counts *mapped labels with text*, because `narrow` keeps an
|
||||
empty label in the pill's layout and a plain mapped count would read 2 either way). `names=2` is the
|
||||
other half — both pills carry a tooltip and an accessible name set by `name_side_tabs()`, so an
|
||||
icon-only pill is not an anonymous button — and `pills=0` means nothing is mapped, which makes the
|
||||
rest of the line meaningless rather than a pass. `policy=narrow` is read rather than assumed. `icon=`
|
||||
is the box the pills' `GtkImage` is *rendered* in, which proves the 32px override stuck: the tab
|
||||
glyphs are 32x16 documents, and GDK paints an icon into a square box whatever the document's aspect,
|
||||
so the artwork is drawn 32x16 at 1:1 inside that 32x32 box — letterboxed, not stretched, which is what
|
||||
keeps its 2px bars crisp. It reads `0x0` on the first tick, where nothing has been allocated yet, and
|
||||
is therefore read by the exit probe with the rest of the sidebar's geometry. `size=` is the bar's own
|
||||
size, and it is the answer to "what did the wider glyphs cost": `36` is what it was with 16px icons,
|
||||
because the pill's fixed height takes a 32px box without growing — the glyph gets twice the width for
|
||||
no vertical room at all.
|
||||
- `page icons: measure=udpscope-tab-measure-symbolic(ok) signals=udpscope-tab-signals-symbolic(ok)` (in the exit
|
||||
dump, beside `toolbar icons:`) — each tab's own glyph and `gtk_icon_theme_has_icon()`'s verdict, read
|
||||
on the first tick for the same reason the toolbar's are. **Both must read `(ok)`**: the page titles are
|
||||
empty on purpose, so an icon that does not resolve leaves a *blank* pill, and a blank pill looks
|
||||
exactly like a page with nothing in it.
|
||||
- `app icons: org.udpscope.app(ok 128x128) org.udpscope.app-symbolic(ok 16x16)` (in the exit dump,
|
||||
beside `toolbar icons:`) — the application icon, named and present at the size its own root
|
||||
`<svg>` element declares. **Both entries must read `ok` at those sizes.** `ok` is
|
||||
@@ -598,7 +621,7 @@ two also catch a row whose accelerator went missing. `settings=` must be a real
|
||||
every row then reports `h=0`. It turns autohide off for its own measurement — a popover dismisses
|
||||
itself when it loses the grab, and `xvfb-run` has no window manager to hand it one — and restores
|
||||
it afterwards.
|
||||
- `[keys] a: armed 0->1 | space: paused 0->1 | t: trigger panel: 0->1 | … | editable focused: 'm' refused=1` (stderr, `UDPSCOPE_PROBE_KEYS_MS`) — **every `X->Y` pair must differ** and **`refused` must be 1**. 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).
|
||||
- `[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.
|
||||
- `[reseq-probe] reseq=N late=N stale=N overflows=N holes=N hole_slots=N backfilled=N
|
||||
open_holes=N bands=N bands_repaired=N nan_slots=N scanned=N trigger_in_hole=D` (stderr,
|
||||
@@ -1116,6 +1139,29 @@ library measures and draws the rect in tests of its own.
|
||||
defect this reading exists for, and it was invisible from the frame alone: the file view pinning its
|
||||
axis from the ring each frame is what swallowed the gesture.
|
||||
|
||||
## `panel switches:` and the trigger panel's default sensitivity
|
||||
|
||||
```
|
||||
[udpscope] trigger panel sensitive before the probe toggled it: 0; signal list items=1 selected=0; column visible after sidebar toggle off=1; pane switches live while their panes are off=1
|
||||
[udpscope] panel switches: trigger=1/1 cursors=1/1 drove_toolbar=1 (pane switch / header bar button; `drove_toolbar` is the pane's own switch having moved the button)
|
||||
```
|
||||
|
||||
Each pane's heading carries its own switch, and it is the *same* control as the header bar's button:
|
||||
both run the same action and both are re-read from the toolbar's state afterwards, so they cannot
|
||||
disagree. The two lines are the claim, in both directions:
|
||||
|
||||
- **`pane switches live while their panes are off=1`** — read before anything turns either on, because
|
||||
it is the whole point of putting the switches in the headings: a control inside an insensitive parent
|
||||
cannot be clicked, so a switch that went insensitive *with* its own pane could never turn it back on.
|
||||
**`0` is that defect**: the switch is drawn, greyed, and useless.
|
||||
- **`trigger=` and `cursors=` are `pane switch / header bar button`**, and the pairs must agree. The
|
||||
trigger's is moved from the *toolbar* end (these steps toggle that button to arm the trigger) and the
|
||||
cursors' from the *pane* end, so one line carries both directions of the sync. **`drove_toolbar=1`**
|
||||
is the second direction's assertion: the pane's own switch turned the cursors on and the header bar's
|
||||
button followed.
|
||||
- `trigger panel sensitive …=0` is the *panel body*, not the panel: the heading row, with its switch,
|
||||
stays live. `1` here while trigger mode is off is the pane not being disabled at all.
|
||||
|
||||
## `[sidebar-probe]`
|
||||
|
||||
```
|
||||
@@ -1144,6 +1190,30 @@ and `regained` reads 400 ms later.
|
||||
- `collapsed=1` means the window is narrower than `kSidebarRoom` (900), which is the expected reading on a
|
||||
scaled display, where the logical width is half what the screen looks like.
|
||||
|
||||
## `[side-tab-probe]`
|
||||
|
||||
```
|
||||
[side-tab-probe] page=signals accums=4 baseline recorded
|
||||
[side-tab-probe] page=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
|
||||
on the hidden page**. It switches *to* the Measure tab, forces one pass so there is a baseline, then switches
|
||||
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
|
||||
`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
|
||||
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.
|
||||
- The probe enables a signal **and assigns it to a plot** itself, both deliberately. 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 the state `update_measures()` is *supposed* to skip, so a probe that left it that way would
|
||||
read `advanced=0` for a reason that has nothing to do with the tab.
|
||||
- `rows` is the measurement count and `accums` the accumulator count; the two must agree, because one
|
||||
accumulator per measurement row is what makes `advanced` a statement about every row.
|
||||
|
||||
## `[prefs-probe]`
|
||||
|
||||
```
|
||||
@@ -1252,25 +1322,20 @@ the two lines together read what the file gave and what the code put in it.
|
||||
## `signals panel:`
|
||||
|
||||
```
|
||||
[udpscope] signals panel: labels=2 list=1
|
||||
[udpscope] signals panel: labels=2 list=1 rows=5 signals=4 math=0 divider=1
|
||||
[udpscope] signals panel: labels=2 list=1 rows=9 signals=4 math=4 divider=1
|
||||
```
|
||||
|
||||
The signals panel's frame read from the window: the heading and the hint the `.ui` gives it, and the list
|
||||
the rows go in. `labels=1` is a `.ui` that lost one of the two, `list=0` one that lost the list -- both of
|
||||
which otherwise show as a panel quietly missing a piece. `panels: … signals=WxH row_h=26` is the other half:
|
||||
the list's own geometry and the height a row really takes, which is what the compact-row CSS rule is for.
|
||||
|
||||
## `math panel:`
|
||||
|
||||
```
|
||||
[udpscope] math panel: head=2 buttons=1 list=1 title='Math channels'
|
||||
```
|
||||
|
||||
The math panel's frame read from the window: the head row the `.ui` gives it (the title and the add
|
||||
button), and the list the channel rows go in. `buttons=0` is a `.ui` that lost the `+` -- a panel that
|
||||
cannot add a channel, with nothing else complaining -- and `title=` is the file's own text read back, so a
|
||||
`.ui` that lost the label shows as `title='-'`. `[math-probe]` is the other half of the reading: it drives
|
||||
the add and the editor through the same code a user does.
|
||||
The signals panel's frame read from the window, and in the same line the *shape* of the one channel list:
|
||||
the heading and the hint the `.ui` gives it, the list the rows go in, how many rows that list holds
|
||||
(`rows`), how many of them are streamed signals (`signals`), how many math channels the app has (`math`),
|
||||
and `divider`, which is 1 when `rows == signals + 1 + math` and 0 otherwise. `labels=1` is a `.ui` that lost
|
||||
one of the two labels, `list=0` one that lost the list -- both of which otherwise show as a panel quietly
|
||||
missing a piece -- and **`divider=0` is the merged list that lost its `+`**, the one control that can create
|
||||
a math channel. The first line above is a run with no math channels at all: the divider is there anyway,
|
||||
because a `+` that only appeared once a channel existed could never create the first one. `panels: …
|
||||
signals=WxH row_h=26` is the other half: the list's own geometry and the height a row really takes, which
|
||||
is what the compact-row CSS rule is for.
|
||||
|
||||
## `events pane:`
|
||||
|
||||
|
||||
@@ -15,9 +15,12 @@ xvfb-run -a env UDPSCOPE_EXIT_AFTER_MS=8000 UDPSCOPE_ENABLE_ALL_MS=1000 \
|
||||
|
||||
Two baselines every run is read against. A clean build emits **four** warnings and no more: two
|
||||
`-Wmissing-field-initializers` on `MathNode::source` and `-Woverlength-strings` on **both**
|
||||
generated GResource `.c` files — the help pages' and the icons', whose blob crossed C99's
|
||||
4095-byte string-literal limit when the two app icons joined the four action ones (a note about a
|
||||
generated file, which GCC itself supports). The fifth warning that used to appear here was an unused
|
||||
generated GResource `.c` files — the help pages' (**514660** bytes) and the icons', whose blob is
|
||||
**12352** against C99's 4095-byte string-literal limit with the seven action icons and the two app icons
|
||||
that remain (a note about a generated file, which GCC itself supports) -- re-measured when
|
||||
the tab work changed the set (that icon left, the two tab glyphs arrived, and this file's
|
||||
help page was rewritten), which did not move the count; re-measure by touching each `*.gresource.xml`
|
||||
under `src/` and rebuilding. The fifth warning that used to appear here was an unused
|
||||
`output_built` in `math_probe()`; it was **deleted** as dead code — redundant with the
|
||||
`output_after_add` already printed on the same line — and that deletion is what dropped the baseline
|
||||
from five to four. Nothing started reading it. And an exit-dump run emits **no** `Gtk-CRITICAL` at
|
||||
@@ -205,6 +208,12 @@ precedent, not an exception.
|
||||
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
|
||||
`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
|
||||
Measure tab, a forced pass, then the tab switched away and read again two seconds later. **`advanced=4`
|
||||
is the assertion** (every accumulator gained passes while its page was off screen); `advanced=0` is the
|
||||
defect where the accumulation is gated on the visible tab, 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
|
||||
probe ahead of it. The reading's own entry is in `probe-readings.md`.
|
||||
- `UDPSCOPE_PROBE_SEARCH_MS` — runs a buffer search for the armed condition, cross-checks the live
|
||||
trigger's own `t0` against the nearest hit (`agree=1` is the load-bearing reading), and jumps to
|
||||
hit 0. Reports the resulting `mode=`, `axis=` and `marks placed=`.
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,199 @@
|
||||
<!-- SPDX-License-Identifier: MIT -->
|
||||
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||||
|
||||
# A tabbed side panel, and one channel list: design
|
||||
|
||||
The side panel stacks five sections in one scrolling column — trigger, cursors, measurements,
|
||||
math, signals — each behind a `GtkRevealer` opened by its own toolbar toggle. With trigger,
|
||||
measurements, cursors and math all open the column is unusable: every section is short of room,
|
||||
and finding one means scrolling past three others.
|
||||
|
||||
Two changes, aimed at that one problem:
|
||||
|
||||
1. **The column becomes two tabs.** *Measure* holds the cursors readout and the measurements;
|
||||
*Signals* holds the trigger and the channel list. One tab is on screen at a time, so no
|
||||
section competes with four others for height.
|
||||
2. **The math list is folded into the signal list.** A math channel already *is* a channel in
|
||||
the model, and the two lists were the same list drawn twice. One list, with a `MATH`
|
||||
divider in it.
|
||||
|
||||
A third change falls out of the first. A tab is one-of-N, so it can no longer carry an on/off:
|
||||
**no toggle hides a pane any more.** Trigger-off and cursors-off put their pane in a disabled
|
||||
state and leave it where it is.
|
||||
|
||||
## The decisions taken, and what each one rules out
|
||||
|
||||
| Decision | Alternative rejected |
|
||||
| --- | --- |
|
||||
| `AdwViewStack` + `AdwViewSwitcher` | `GtkStack` + `GtkStackSwitcher`: plain buttons beside an `AdwOverlaySplitView`, which is what the sidebar already is. `GtkNotebook`: draws its own tab strip, which costs width in a 320 px sidebar and reads as off-platform. libadwaita ≥ 1.8 is already a hard requirement (`CMakeLists.txt:22`), so the platform widget costs nothing new |
|
||||
| **A scroller per page**, inside the stack | One shared scroller around the stack: the two pages are different heights, so a tab switch would leave the scroll position somewhere the new page did not ask for |
|
||||
| The switcher sits **above** the scrolled area | Inside it: the one control that chooses what you are looking at must not be the first thing to scroll away |
|
||||
| **Two** tabs, exactly as grouped | A tab per section (five tabs): that is the stack again, laid sideways, and a 320 px sidebar has no room for five switcher pills. Measurements read what the cursors bracket, and the trigger picks a channel from the list beside it — the grouping follows the way the panes are used |
|
||||
| Nothing inside a tab collapses | Keeping a revealer per section inside the tab, or a `GtkPaned` between them: the first reintroduces exactly the state-management the tabs remove, two sections at a time instead of five; the second adds a divider position to persist per tab for a column that already scrolls |
|
||||
| Trigger-off and cursors-off **disable** their pane | Hiding it, which is today's behaviour: a pane you can see is a pane you can read, and the settings you are about to turn back on should not vanish while you look for them |
|
||||
| The `measurements` and `math` toolbar toggles are **deleted** | Keeping them as tab selectors: two buttons that select the same tab, whose pressed state duplicates the switcher's |
|
||||
| The switcher shows **titles only**, and the two icons the deleted toggles used are **removed** | Giving each page an `icon-name`: the measurements icon would carry over, but *Signals* has no glyph — reusing the trigger icon for a page that is mostly a channel list misnames it, and drawing a new one is new artwork under the icon authoring rules for a label two words long |
|
||||
| One row widget for both kinds of channel, differing **only in its menu** | Inline edit and delete buttons on math rows (today's math pane): math rows then carry visibly more controls than signal rows, and the list stops reading as one column |
|
||||
| A `+` on the `MATH` divider **only** | A `+` on the signals header too: a signal comes from CONFIG and cannot be added, so the button would have nothing to do |
|
||||
| The statistics accumulator keeps running on the **hidden** page | Gating `update_measures()` on the visible tab: the accumulator is history, so min/max/avg would gain a gap for exactly as long as you looked at the trigger |
|
||||
| The probe reads the **page name and sensitivity** | Dropping the two revealer readings: the defect they catch — a panel that is built but not on screen — still exists, it is just spelled differently |
|
||||
|
||||
## 1. The container
|
||||
|
||||
The sidebar child of `side_view` becomes a vertical `GtkBox`:
|
||||
|
||||
```
|
||||
AdwOverlaySplitView.side_view
|
||||
sidebar: GtkBox (vertical)
|
||||
AdwViewSwitcher <- never scrolls
|
||||
AdwViewStack
|
||||
page "measure" GtkScrolledWindow -> GtkBox: cursors, separator, measurements
|
||||
page "signals" GtkScrolledWindow -> GtkBox: trigger, separator, signal list
|
||||
```
|
||||
|
||||
`side_scroll` moves inside the pages; `App::side_box` becomes the stack, and the five revealer
|
||||
fields in `ui_app.hpp` are removed with the code that sets them (`ui.cpp:4728-4767`).
|
||||
|
||||
The switcher carries the two page titles and no icons. `udpscope-measure-symbolic` and
|
||||
`udpscope-math-symbolic` lose their last user with the toggles that used them, so both leave
|
||||
`src/icons/scalable/actions/` and `udpscope.gresource.xml`: the bundled set goes from six
|
||||
action icons to four, which `invariants-gtk-and-chrome.md` counts and must be corrected with
|
||||
the change.
|
||||
|
||||
The pages are built by the functions that build them today — `build_trigger_panel()`,
|
||||
`build_cursors_panel()`, `build_measures_panel()`, `build_signal_panel()` — unchanged except
|
||||
that they are appended to a page's box instead of to a revealer.
|
||||
|
||||
## 2. State instead of visibility
|
||||
|
||||
| Control | Today | After |
|
||||
| --- | --- | --- |
|
||||
| `trigger_toggle` | trigger mode on/off **and** reveals the trigger pane (`ui_trigger.cpp:198`) | trigger mode on/off; the pane's body goes sensitive/insensitive |
|
||||
| `cursors_toggle` | cursors on/off **and** reveals the readout (`ui_cursors.cpp:124-126`) | cursors on/off; the readout goes insensitive and its values blank to `—` |
|
||||
| `cursors_reset` | hidden while cursors are off (`ui_cursors.cpp:122`) | insensitive while cursors are off |
|
||||
| `measures_toggle` | reveals the measurements (`ui.cpp:4541-4550`) | **deleted** |
|
||||
| `math_toggle` | reveals the math pane (`ui.cpp:4551-4558`) | **deleted** |
|
||||
| `sidebar_toggle`, `events_toggle`, `pause_button` | — | unchanged |
|
||||
|
||||
The two deleted toggles leave `src/ui/header-bar.ui` with their handlers, so the bar is
|
||||
declared with five controls and no id resolves to a button nothing listens to.
|
||||
|
||||
`update_side_visibility()` (`ui.cpp:2830-2850`) keeps its job — the whole sidebar is still the
|
||||
split view's to show, still gated on a CONFIG having named a signal — minus the line that showed
|
||||
`signal_toggle`, which no longer exists.
|
||||
|
||||
Everything else those handlers do is untouched: `tc.armed`, the epoch rule and the
|
||||
`set_time_window()` call in `on_trigger_toggled()`, `sync_events_availability()`, the
|
||||
Arm/Disarm label, and `rows->set_cursors_visible()` with its `reset_cursors()`.
|
||||
|
||||
Blanking the cursor readout to `—` rather than leaving the last values is the readout invariant
|
||||
restated: a readout quotes a stored sample, and with the instruments off there is no sample it
|
||||
quotes.
|
||||
|
||||
## 3. One channel list
|
||||
|
||||
**The duplication is literal, and the merge is mostly a deletion.**
|
||||
`rebuild_signal_list()` is handed `model.signals()`, and `append_math_signals_locked()`
|
||||
(`model.cpp:252-269`) has already appended one synthetic entry per math channel — *"they go
|
||||
through the ordinary signal path on purpose: a derived channel is a channel"*, says its own
|
||||
comment. Nothing in the list filters `sig.math`, so **every math channel is already a row in
|
||||
the signal list**, with its tick, chip, name, unit and menu. The math pane listed the same
|
||||
channels a second time.
|
||||
|
||||
What the list gains:
|
||||
|
||||
- a non-selectable `MATH` divider row before the first math row, carrying the `+` that opens
|
||||
the node editor on a new channel;
|
||||
- two items in a math row's menu.
|
||||
|
||||
| | signal row | math row |
|
||||
| --- | --- | --- |
|
||||
| tick | acquire (hidden in a file view) | enabled |
|
||||
| colour chip | clickable while plotted | unchanged |
|
||||
| unit | from CONFIG | the channel's own |
|
||||
| menu | Information, Default colour, Calibrate… | **Edit graph…**, Default colour, **Delete** |
|
||||
|
||||
Information and Calibrate… are a *streamed* signal's — calibration is applied at ingest — so a
|
||||
math row carries neither. No rename item either: the node editor owns a math channel's name and
|
||||
unit (`src/ui/math-editor.ui:37-43`), so *Edit graph…* is where both are changed, and a second
|
||||
place to change them would be a second answer to the same question.
|
||||
|
||||
Two things already work and must not be rebuilt: the tick is already the right switch —
|
||||
`enable_locked()` (`model.cpp:626-639`) mirrors it into the `MathChannel`'s own `enabled` flag —
|
||||
and the row's `GtkDragSource` already lets a math channel be dropped onto a plot row, which is
|
||||
how `math1` reached row 0 in the session this work started from.
|
||||
|
||||
One defect to fix while merging: `App::math` is the copy the session file is written from, so a
|
||||
tick on a math row must mirror `enabled` into it before `schedule_layout_save()`. Today the pane's
|
||||
checkbox did that (`ui.cpp:2276`) and the list's tick did not, so the two controls disagreed
|
||||
about what gets saved.
|
||||
|
||||
Deleted: `build_math_panel()` (`ui_math.cpp:1044`), `rebuild_math_pane()` (`ui.cpp:2251-2319`),
|
||||
`src/ui/math-panel.ui` and its entry in `src/ui/udpscope-ui.gresource.xml`, `App::math_revealer`,
|
||||
`App::math_list`, `App::math_toggle`, `App::measures_toggle`, `App::signal_revealer`, and
|
||||
`App::signal_toggle` with `on_signals_toggled()` (`ui_signals.cpp:303-307`) and its overflow-menu
|
||||
row (`ui.cpp:4452-4455`) — the list is a page now, so "show or hide the signal list" has nothing
|
||||
to hide. The node editor dialog itself is untouched; only the button that opens it moves.
|
||||
|
||||
## 4. Persistence
|
||||
|
||||
One key, top level in the session file beside `window_s` and `measure_stats`
|
||||
(`layout_store.cpp`):
|
||||
|
||||
```toml
|
||||
side_tab = "signals" # or "measure"
|
||||
```
|
||||
|
||||
Written on save; applied once where the rest of the layout is applied, so an unknown or missing
|
||||
value leaves the stack on its first page. Nothing else about the panel is persisted today and
|
||||
nothing else needs to be: no revealer states remain, and the 320 px width is still the `.ui`'s.
|
||||
|
||||
## 5. What runs on a hidden page
|
||||
|
||||
`update_cursors_pane()` and `update_measures(force=false, accumulate=true)` are called every
|
||||
frame from the tick (`ui.cpp:3275-3276`) and keep being called whichever page is current.
|
||||
Writing a label on an unrealized widget is legal; **reading its allocation is not**. Every
|
||||
allocation read in the sidebar is a debug capture — `dbg_measures_pane_w/h`
|
||||
(`ui.cpp:3237-3239`), `dbg_signal_w/h` and `dbg_trigger_w/h` (`ui_probe.cpp:77-80`) — and each
|
||||
reads 0 on the page that is not current. That is a change in what the reading means, which §6
|
||||
handles; it is not a defect, and no functional code may start depending on those numbers.
|
||||
|
||||
This is the revealer trap in a new widget. `invariants-gtk-and-chrome.md:174-181` records that
|
||||
an unrevealed `GtkRevealer` measures 0×0 and is still given the leftover width; a stack's
|
||||
hidden page measures 0×0 too, and the rule it teaches — ask about *state*, not about size —
|
||||
carries over unchanged.
|
||||
|
||||
## 6. Verification
|
||||
|
||||
Readings that change, and what each must say instead:
|
||||
|
||||
| Reading | Was | Becomes |
|
||||
| --- | --- | --- |
|
||||
| `dbg_signal_revealed` (`ui_probe.cpp:90`) | the signal revealer's child is revealed | the `signals` page is current **and** its list is non-empty |
|
||||
| `dbg_trigger_revealed` (`ui_probe.cpp:92`, `:2190`) | the trigger revealer's child is revealed | the `signals` page is current **and** the trigger body is sensitive |
|
||||
| `dbg_measures_pane_w/h` | the pane's size while revealed | the pane's size while its page is current; 0 otherwise, and read as such |
|
||||
| the panel-toggle reading (`ui_probe.cpp:104`) | both menu toggles are visible | the switcher is visible, which is the one control those two became |
|
||||
|
||||
One new probe, `UDPSCOPE_PROBE_SIDE_TAB_MS`: select the page that is not current, then dump the
|
||||
page name, the measurement row count and the accumulator's sample count. The count must have
|
||||
advanced across the switch — that is the §5 rule under test, and the defect it names is a
|
||||
statistics pane that silently restarts its history every time you look at the trigger.
|
||||
|
||||
New reading for the merged list, on the existing signal-panel probe: the row count must be
|
||||
signals + 1 + math channels, and the math rows must carry the same widget shape as a signal
|
||||
row. A count short by the divider is a list that lost its `+`.
|
||||
|
||||
Baselines, unchanged and re-read on every run: exactly four warnings on a clean build, no
|
||||
`Gtk-CRITICAL`, `test-ui` 15/15.
|
||||
|
||||
Docs to update with the work: `probe-readings.md` (the three changed lines and the two new
|
||||
ones), `verification.md` (a bullet for the tab switch), `invariants-gtk-and-chrome.md` (the
|
||||
sidebar is a view stack, the hidden-page trap replaces the unrevealed-revealer one for it, the
|
||||
bar carries five controls and the bundled set four action icons),
|
||||
`invariants-math-channels.md` (the list lives in the signal panel), `invariants-readouts-and-cursors.md`
|
||||
(the readout blanks when the instruments are off), and a README note recording the deviation
|
||||
from `SPECS.md` where it names the panes.
|
||||
|
||||
## Out of scope
|
||||
|
||||
The events pane left of the plot, export, the node editor's internals, and the status bar.
|
||||
@@ -31,13 +31,14 @@ panel is working correctly, not failing to connect.
|
||||
<b>The window</b>
|
||||
|
||||
• <b>The header bar</b> -- left to right: the time window (<tt>2 s</tt>), the trigger and the
|
||||
pause, then the pane switches -- signal list, cursors, measurements, math, side panel -- and
|
||||
the menu, which holds Fit to data, the layout editor, Export, Keyboard shortcuts, Help and
|
||||
About.
|
||||
pause, the events, cursors and side-panel switches, and the menu, which holds Fit to data, the
|
||||
layout editor, Export, Keyboard shortcuts, Help and About.
|
||||
|
||||
• <b>The side panel</b> -- the trigger, cursors, measurements and math panes at the top, then
|
||||
the signal list: one row per signal with its colour, an enable switch, its name and its unit.
|
||||
Signals are dragged from here onto a plot row.
|
||||
• <b>The side panel</b> -- two tabs. <b>Measure</b> holds the cursors readout and the
|
||||
measurements derived from it; <b>Signals</b> holds the trigger and the channel list, where a
|
||||
math channel is a row of that one list under a <tt>MATH</tt> heading whose <tt>+</tt> creates
|
||||
one. One row per channel: its colour, an enable switch, its name and its unit. Channels are
|
||||
dragged from here onto a plot row.
|
||||
|
||||
• <b>The plot</b> -- up to 8 rows, each with its own y axis and up to 4 traces, all on one
|
||||
shared time axis. Right-click a row for its own menu (fit, add a signal, remove a trace).
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- SPDX-License-Identifier: MIT -->
|
||||
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||||
<!-- A sigma: the one pane that computes rather than reads. Four filled subpaths, all
|
||||
wound clockwise so the default nonzero rule unions them where they overlap; the
|
||||
diagonals run from the bars' left ends to a vertex right of centre, which is the
|
||||
way the glyph actually points. -->
|
||||
<svg height="16px" width="16px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#2e3436" d="M3 2 H13 V4 H3 Z
|
||||
M3 12 H13 V14 H3 Z
|
||||
M4.64 2.23 L10.64 7.23 L9.36 8.77 L3.36 3.77 Z
|
||||
M10.64 8.77 L4.64 13.77 L3.36 12.23 L9.36 7.23 Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 762 B |
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- SPDX-License-Identifier: MIT -->
|
||||
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||||
<!-- The Measure tab: two cursors beside the ruler they read against. Each cursor is a cap over two
|
||||
bold dashes — the mark the toolbar's cursors button carries, at a weight that survives this size.
|
||||
The ruler is two bands with three marks between them and *no end caps*: a closed frame reads as a
|
||||
rectangle, and it costs a fifth of the ink on two lines that say nothing. Both motifs are centred
|
||||
on the same line and share one weight, and everything is inset two pixels. -->
|
||||
<svg height="16px" width="32px" viewBox="0 0 32 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#2e3436" d="M2 2 H7 V5 H2 Z
|
||||
M3 6 H6 V10 H3 Z
|
||||
M3 11 H6 V14 H3 Z
|
||||
M9 2 H14 V5 H9 Z
|
||||
M10 6 H13 V10 H10 Z
|
||||
M10 11 H13 V14 H10 Z
|
||||
M17 4 H30 V6 H17 Z
|
||||
M17 10 H30 V12 H17 Z
|
||||
M18 6 H20 V10 H18 Z
|
||||
M22 6 H24 V10 H22 Z
|
||||
M26 6 H28 V10 H26 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- SPDX-License-Identifier: MIT -->
|
||||
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||||
<!-- The Signals tab: the toolbar's trigger picture — a sine crossing its dashed level — beside the
|
||||
channel list. Sharing that picture is the point: the pill and the button that turns the trigger on
|
||||
should be recognisably about the same thing. A full period and not a hump: the crest and the trough
|
||||
are what make it read as a wave rather than as a hill, and the level's dashes survive between the
|
||||
crossings. Two channel rows on the right, drawn bold like the wave, and everything inset two
|
||||
pixels, because art flush with the cell edge is what a button or a crop clips. -->
|
||||
<svg height="16px" width="32px" viewBox="0 0 32 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#2e3436" d="M2 6 H3 V10 H2 Z
|
||||
M3 4 H4 V8 H3 Z
|
||||
M4 3 H5 V7 H4 Z
|
||||
M5 2 H6 V6 H5 Z
|
||||
M6 3 H7 V7 H6 Z
|
||||
M7 4 H8 V8 H7 Z
|
||||
M8 6 H9 V10 H8 Z
|
||||
M9 8 H10 V12 H9 Z
|
||||
M10 9 H11 V13 H10 Z
|
||||
M11 10 H12 V14 H11 Z
|
||||
M12 9 H13 V13 H12 Z
|
||||
M13 8 H14 V12 H13 Z
|
||||
M14 6 H15 V10 H14 Z
|
||||
M2 7 H5 V9 H2 Z
|
||||
M7 7 H10 V9 H7 Z
|
||||
M12 7 H15 V9 H12 Z
|
||||
M18 3 H23 V8 H18 Z
|
||||
M24 4 H30 V7 H24 Z
|
||||
M18 10 H23 V15 H18 Z
|
||||
M24 11 H30 V14 H24 Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1,16 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- SPDX-License-Identifier: MIT -->
|
||||
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||||
<!-- A rising edge crossing a dashed trigger level: what the trigger *is*. The dash
|
||||
rhythm is 2 on, 1 off (tight enough to read as one line rather than as a row of
|
||||
dots), with the one wide gap at x 5..9 that the edge rises through. Every
|
||||
coordinate is a whole pixel of the 16px cell so nothing blurs at 1x. -->
|
||||
<!-- The trigger: a sine and the dashed level it is looked for at, which is what a trigger *is* on a
|
||||
waveform that is not a step. It used to be a rising edge, and one edge names only one of the four
|
||||
shape triggers the panel offers — a wave crossing a level names the trigger's job instead. One
|
||||
period, one-pixel stairs, the way the plot rasterises its own traces, with the level through the
|
||||
centre so the crest and the trough stand clear and the crossings fall on the steep parts. Inset
|
||||
two pixels, like the tabs' glyphs, so no art is flush with an edge a button might clip. -->
|
||||
<svg height="16px" width="16px" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#2e3436" d="M0 11 H8 V13 H0 Z
|
||||
M6 3 H8 V13 H6 Z
|
||||
M6 3 H16 V5 H6 Z
|
||||
M0 7 H2 V9 H0 Z
|
||||
M3 7 H5 V9 H3 Z
|
||||
M9 7 H11 V9 H9 Z
|
||||
M12 7 H14 V9 H12 Z"/>
|
||||
<path fill="#2e3436" d="M2 7 H3 V10 H2 Z
|
||||
M3 5 H4 V8 H3 Z
|
||||
M4 3 H5 V6 H4 Z
|
||||
M5 3 H6 V6 H5 Z
|
||||
M6 4 H7 V7 H6 Z
|
||||
M7 6 H8 V9 H7 Z
|
||||
M8 8 H9 V11 H8 Z
|
||||
M9 10 H10 V13 H9 Z
|
||||
M10 11 H11 V14 H10 Z
|
||||
M11 11 H12 V14 H11 Z
|
||||
M12 9 H13 V12 H12 Z
|
||||
M13 7 H14 V10 H13 Z
|
||||
M2 7 H5 V9 H2 Z
|
||||
M7 7 H10 V9 H7 Z
|
||||
M11 7 H14 V9 H11 Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 857 B After Width: | Height: | Size: 1.5 KiB |
@@ -20,7 +20,8 @@
|
||||
<file preprocess="xml-stripblanks">scalable/actions/udpscope-trigger-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">scalable/actions/udpscope-cursors-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">scalable/actions/udpscope-measure-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">scalable/actions/udpscope-math-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">scalable/actions/udpscope-tab-measure-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">scalable/actions/udpscope-tab-signals-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">scalable/actions/udpscope-events-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">scalable/actions/udpscope-resize-rows-symbolic.svg</file>
|
||||
<file preprocess="xml-stripblanks">scalable/apps/org.udpscope.app.svg</file>
|
||||
|
||||
@@ -496,6 +496,8 @@ bool load_layout(const std::string& path, LayoutState& out) {
|
||||
}
|
||||
} else if (key == "measure_stats") {
|
||||
st.measure_stats = value == "true";
|
||||
} else if (key == "side_tab") {
|
||||
st.side_tab = strip_quote(value);
|
||||
} else if (key == "multicast") {
|
||||
st.multicast = strip_quote(value);
|
||||
} else if (key == "interface") {
|
||||
@@ -645,6 +647,7 @@ bool save_layout(const std::string& path, const LayoutState& state) {
|
||||
o << "]\n";
|
||||
}
|
||||
o << "measure_stats = " << (state.measure_stats ? "true" : "false") << "\n";
|
||||
o << "side_tab = \"" << state.side_tab << "\"\n";
|
||||
if (!state.comment.empty()) o << "comment = " << toml_escape(state.comment) << "\n";
|
||||
if (state.buffer_mb > 0.0) o << "buffer_mb = " << state.buffer_mb << "\n";
|
||||
o << "relative_time = " << (state.relative_time ? "true" : "false") << "\n";
|
||||
|
||||
@@ -66,6 +66,10 @@ struct LayoutState {
|
||||
/// stream and mean nothing restored against another one.
|
||||
bool measure_stats = false;
|
||||
|
||||
/// Which sidebar tab was showing: "measure" or "signals". The one thing the panel persists --
|
||||
/// no revealer states remain, and the 320 px width is the `.ui`'s.
|
||||
std::string side_tab = "signals";
|
||||
|
||||
/// The measurements pane, by signal *name* and with the type and options a row was built with.
|
||||
std::vector<Measure> measures;
|
||||
|
||||
|
||||
+4
-8
@@ -12,14 +12,10 @@ constexpr Accel kAccels[] = {
|
||||
{GDK_KEY_space, 0, AppAction::PauseToggle, "space", "Pause or resume the display",
|
||||
"Acquisition"},
|
||||
{GDK_KEY_a, 0, AppAction::TriggerArm, "a", "Arm or disarm the trigger", "Acquisition"},
|
||||
{GDK_KEY_t, 0, AppAction::TriggerPanel, "t", "Show or hide the trigger panel",
|
||||
"Acquisition"},
|
||||
{GDK_KEY_c, 0, AppAction::CursorsPane, "c", "Show or hide the cursors pane",
|
||||
"Panes and editors"},
|
||||
{GDK_KEY_m, 0, AppAction::MeasuresPane, "m", "Show or hide the measurements pane",
|
||||
"Panes and editors"},
|
||||
{GDK_KEY_g, 0, AppAction::MathPane, "g", "Show or hide the math pane",
|
||||
"Panes and editors"},
|
||||
{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_m, 0, AppAction::SideTabMeasure, "m", "Show the Measure tab", "Panes and editors"},
|
||||
{GDK_KEY_g, 0, AppAction::SideTabSignals, "g", "Show the Signals tab", "Panes and editors"},
|
||||
{GDK_KEY_l, 0, AppAction::LayoutEditor, "l", "Show or hide the layout editor",
|
||||
"Panes and editors"},
|
||||
{GDK_KEY_s, 0, AppAction::SidePanel, "s", "Show or hide the side panel",
|
||||
|
||||
+2
-2
@@ -10,8 +10,8 @@ enum class AppAction {
|
||||
TriggerArm,
|
||||
TriggerPanel,
|
||||
CursorsPane,
|
||||
MeasuresPane,
|
||||
MathPane,
|
||||
SideTabMeasure,
|
||||
SideTabSignals,
|
||||
LayoutEditor,
|
||||
SidePanel,
|
||||
Export,
|
||||
|
||||
+255
-200
@@ -2248,76 +2248,6 @@ void open_math_dialog(App& app, int edit) {
|
||||
}
|
||||
|
||||
|
||||
void rebuild_math_pane(App& app) {
|
||||
if (app.math_list == nullptr) return;
|
||||
while (GtkWidget* child = gtk_widget_get_first_child(app.math_list)) {
|
||||
gtk_box_remove(GTK_BOX(app.math_list), child);
|
||||
}
|
||||
if (app.math.empty()) {
|
||||
GtkWidget* none = gtk_label_new("— none —");
|
||||
gtk_widget_add_css_class(none, "dim-label");
|
||||
gtk_widget_set_halign(none, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(app.math_list), none);
|
||||
return;
|
||||
}
|
||||
for (std::size_t i = 0; i < app.math.size(); ++i) {
|
||||
const MathChannel& m = app.math[i];
|
||||
GtkWidget* row = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
GtkWidget* on = gtk_check_button_new();
|
||||
gtk_check_button_set_active(GTK_CHECK_BUTTON(on), m.enabled);
|
||||
gtk_widget_set_tooltip_text(on, "enable this math channel: off means it is not acquired, not\n"
|
||||
"plotted and not measured, exactly like a signal's switch");
|
||||
const std::size_t index = i;
|
||||
g_signal_connect(on, "toggled", G_CALLBACK(+[](GtkToggleButton* b, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
const std::size_t k = GPOINTER_TO_SIZE(
|
||||
g_object_get_data(G_OBJECT(b), "udpscope-math-index"));
|
||||
if (k >= a->math.size()) return;
|
||||
a->math[k].enabled = gtk_toggle_button_get_active(b);
|
||||
apply_math_channels(*a);
|
||||
}),
|
||||
&app);
|
||||
g_object_set_data(G_OBJECT(on), "udpscope-math-index", GSIZE_TO_POINTER(index));
|
||||
gtk_box_append(GTK_BOX(row), on);
|
||||
|
||||
std::string text = m.name;
|
||||
if (!m.unit.empty()) text += " [" + m.unit + "]";
|
||||
GtkWidget* label = gtk_label_new(text.c_str());
|
||||
gtk_label_set_ellipsize(GTK_LABEL(label), PANGO_ELLIPSIZE_MIDDLE);
|
||||
gtk_widget_set_hexpand(label, true);
|
||||
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
||||
gtk_box_append(GTK_BOX(row), label);
|
||||
|
||||
GtkWidget* edit = gtk_button_new_from_icon_name("document-edit-symbolic");
|
||||
gtk_widget_add_css_class(edit, "flat");
|
||||
gtk_widget_set_tooltip_text(edit, "edit this math channel's graph");
|
||||
g_object_set_data(G_OBJECT(edit), "udpscope-math-index", GSIZE_TO_POINTER(index));
|
||||
g_signal_connect(edit, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
const std::size_t k = GPOINTER_TO_SIZE(
|
||||
g_object_get_data(G_OBJECT(b), "udpscope-math-index"));
|
||||
open_math_dialog(*a, static_cast<int>(k));
|
||||
}),
|
||||
&app);
|
||||
gtk_box_append(GTK_BOX(row), edit);
|
||||
|
||||
GtkWidget* del = gtk_button_new_from_icon_name("window-close-symbolic");
|
||||
gtk_widget_add_css_class(del, "flat");
|
||||
gtk_widget_set_tooltip_text(del, "delete this math channel");
|
||||
g_object_set_data(G_OBJECT(del), "udpscope-math-index", GSIZE_TO_POINTER(index));
|
||||
g_signal_connect(del, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
const std::size_t k = GPOINTER_TO_SIZE(
|
||||
g_object_get_data(G_OBJECT(b), "udpscope-math-index"));
|
||||
if (k < a->math.size()) a->math.erase(a->math.begin() + k);
|
||||
apply_math_channels(*a);
|
||||
}),
|
||||
&app);
|
||||
gtk_box_append(GTK_BOX(row), del);
|
||||
gtk_box_append(GTK_BOX(app.math_list), row);
|
||||
}
|
||||
}
|
||||
|
||||
GtkWidget* build_measures_panel(App& app) {
|
||||
// The panel's frame is a UI file; the readings are not, because each row is one measurement and they are
|
||||
// rebuilt whenever the set or the mode changes. `make test-ui` checks the file's types against the GIR;
|
||||
@@ -2824,11 +2754,28 @@ void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer
|
||||
open_row_menu(*app, row, x, y, widget);
|
||||
}
|
||||
|
||||
void on_side_tab_changed(GObject* o, GParamSpec*, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
const char* name = adw_view_stack_get_visible_child_name(ADW_VIEW_STACK(o));
|
||||
app->side_tab = name != nullptr ? name : "signals";
|
||||
// Not while the layout is being applied: the restore would schedule a save of what it just read.
|
||||
if (!app->updating_panels) schedule_layout_save(*app);
|
||||
}
|
||||
|
||||
void select_side_tab(App& app, const char* name) {
|
||||
if (app.side_stack == nullptr || name == nullptr) return;
|
||||
// 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) {
|
||||
if (app.side_scroll == nullptr) return;
|
||||
if (app.side_stack == nullptr) return;
|
||||
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,
|
||||
// which is why it is kept apart from the button's own state -- that state follows the panel now, and
|
||||
@@ -2837,15 +2784,13 @@ void update_side_visibility(App& app) {
|
||||
gtk_widget_set_sensitive(app.sidebar_toggle, have_signals);
|
||||
const bool want_column = have_signals && app.sidebar_wanted;
|
||||
// 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. Hiding the scroller
|
||||
// instead left an empty column where the panels should be.
|
||||
// shows it as an overlay rather than letting the column be squeezed to nothing.
|
||||
if (app.side_view != nullptr) {
|
||||
app.sidebar_syncing = true;
|
||||
adw_overlay_split_view_set_show_sidebar(ADW_OVERLAY_SPLIT_VIEW(app.side_view), want_column);
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.sidebar_toggle), want_column);
|
||||
app.sidebar_syncing = false;
|
||||
}
|
||||
gtk_widget_set_visible(app.signal_toggle, have_signals);
|
||||
gtk_widget_set_visible(app.trigger_toggle, have_signals);
|
||||
}
|
||||
|
||||
@@ -2920,6 +2865,59 @@ std::string icon_report(GtkIconTheme* theme, const char* name, const char* resou
|
||||
/// and Reset is inside the cursors pane. Recorded on the first tick -- the widgets exist and the
|
||||
/// window is alive, neither of which is true when the dump runs -- so the dump can print a fact
|
||||
/// instead of asking GTK about a destroyed widget.
|
||||
/// The words the pills do not draw. The pages' `title` is empty on purpose (see the `.ui`), so the
|
||||
/// human name of a tab lives here and is put on the pill — its tooltip and its accessible name — by
|
||||
/// `name_side_tabs()`. That keeps an icon-only pill a *named* control for a screen reader.
|
||||
static const char* side_tab_title(const char* name) {
|
||||
if (name != nullptr && std::strcmp(name, "measure") == 0) return "Measure";
|
||||
if (name != nullptr && std::strcmp(name, "signals") == 0) return "Signals";
|
||||
return "";
|
||||
}
|
||||
|
||||
/// Names the switcher's pills and takes their empty labels out of the layout: an empty `GtkLabel`
|
||||
/// still asks for its own width, which leaves the icon off-centre in a pill meant to be one glyph.
|
||||
/// Called from the first tick, because the pills are `AdwViewSwitcher`'s own and exist only once the
|
||||
/// widget has been mapped.
|
||||
void name_side_tabs(App& app) {
|
||||
if (app.side_tabs_named || app.side_switcher == nullptr) return;
|
||||
GListModel* pages = app.side_stack != nullptr
|
||||
? G_LIST_MODEL(adw_view_stack_get_pages(ADW_VIEW_STACK(app.side_stack)))
|
||||
: nullptr;
|
||||
// The switcher's children *are* its pills, in page order, and `AdwViewSwitcherButton` is not a
|
||||
// public type (there is no `ADW_IS_VIEW_SWITCHER_BUTTON`), so the two lists are walked together by
|
||||
// index rather than checked by type.
|
||||
GtkWidget* pill = gtk_widget_get_first_child(app.side_switcher);
|
||||
for (guint i = 0; pages != nullptr && pill != nullptr && i < g_list_model_get_n_items(pages);
|
||||
pill = gtk_widget_get_next_sibling(pill), ++i) {
|
||||
AdwViewStackPage* page = ADW_VIEW_STACK_PAGE(g_list_model_get_item(pages, i));
|
||||
const char* title = side_tab_title(adw_view_stack_page_get_name(page));
|
||||
g_object_unref(page);
|
||||
if (title[0] == '\0') continue;
|
||||
gtk_widget_set_tooltip_text(pill, title);
|
||||
gtk_accessible_update_property(GTK_ACCESSIBLE(pill), GTK_ACCESSIBLE_PROPERTY_LABEL, title, -1);
|
||||
const std::function<void(GtkWidget*)> hide_labels = [&](GtkWidget* w) {
|
||||
if (GTK_IS_LABEL(w)) gtk_widget_set_visible(w, FALSE);
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(w); c != nullptr;
|
||||
c = gtk_widget_get_next_sibling(c)) {
|
||||
hide_labels(c);
|
||||
}
|
||||
};
|
||||
hide_labels(pill);
|
||||
// The tabs' glyphs are 32x16 documents, and the pill has the width for them: drawn at 32 they are
|
||||
// 1:1, so both motifs sit side by side at the size they were authored at and the 2px bars stay
|
||||
// crisp. A 16px box would scale the pair down to a smear, and a fractional scale would blur it.
|
||||
const std::function<void(GtkWidget*)> size_images = [&](GtkWidget* w) {
|
||||
if (GTK_IS_IMAGE(w)) gtk_image_set_pixel_size(GTK_IMAGE(w), 32);
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(w); c != nullptr;
|
||||
c = gtk_widget_get_next_sibling(c)) {
|
||||
size_images(c);
|
||||
}
|
||||
};
|
||||
size_images(pill);
|
||||
app.side_tabs_named = true;
|
||||
}
|
||||
}
|
||||
|
||||
void note_panel_controls(App& app) {
|
||||
if (!app.dbg_panel_controls.empty()) return;
|
||||
const auto in_bar = [](GtkWidget* w) {
|
||||
@@ -2932,15 +2930,15 @@ void note_panel_controls(App& app) {
|
||||
const auto in_cursors_pane = [&app](GtkWidget* w) {
|
||||
for (GtkWidget* p = w != nullptr ? gtk_widget_get_parent(w) : nullptr; p != nullptr;
|
||||
p = gtk_widget_get_parent(p)) {
|
||||
if (p == app.cursors_revealer) return 1;
|
||||
if (p == app.cursors_panel) return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
// `HeaderBar::pack_end()` parents into an internal box, so the bar is a level up, not the parent.
|
||||
char text[256];
|
||||
std::snprintf(text, sizeof(text),
|
||||
"cursors=%d measurements=%d math=%d in the toolbar, reset=%d in the cursors pane",
|
||||
in_bar(app.cursors_toggle), in_bar(app.measures_toggle), in_bar(app.math_toggle),
|
||||
"cursors=%d in the toolbar, switcher=%d, reset=%d in the cursors pane",
|
||||
in_bar(app.cursors_toggle), app.side_switcher != nullptr ? 1 : 0,
|
||||
in_cursors_pane(app.cursors_reset));
|
||||
// ... 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
|
||||
@@ -2948,8 +2946,7 @@ void note_panel_controls(App& app) {
|
||||
const std::pair<GtkWidget*, const char*> named[] = {
|
||||
{app.trigger_toggle, "trigger"}, {app.events_toggle, "events"},
|
||||
{app.pause_button, "pause"},
|
||||
{app.cursors_toggle, "cursors"}, {app.measures_toggle, "measures"},
|
||||
{app.math_toggle, "math"}, {app.sidebar_toggle, "sidebar"},
|
||||
{app.cursors_toggle, "cursors"}, {app.sidebar_toggle, "sidebar"},
|
||||
{app.menu_button, "menu"},
|
||||
};
|
||||
// The widget tree's child order, which *is* the visual order left to right: a box lays its children
|
||||
@@ -2997,6 +2994,62 @@ void note_panel_controls(App& app) {
|
||||
" " +
|
||||
icon_report(theme, "org.udpscope.app-symbolic",
|
||||
"/org/udpscope/icons/scalable/apps/org.udpscope.app-symbolic.svg");
|
||||
|
||||
// The two sidebar pages' own icons, named and resolved the same way, and what the switcher is
|
||||
// drawing for them. The pills are `AdwViewSwitcher`'s own buttons, built when the stack is mapped,
|
||||
// so this reads on the first tick: `pills` is the guard (0 pills would make a label count of 0
|
||||
// meaningless) and `labels` must be 0, because the switcher's policy is `narrow` -- the pages are
|
||||
// icons, not words, in a 300px column.
|
||||
// Walked through `get_pages()`, not the widget tree: an `AdwViewStackPage` is a plain `GObject`
|
||||
// (only its `child` is a widget), so `gtk_widget_get_first_child()` on the stack yields the pages'
|
||||
// scrolled windows and this line read empty the first time it was written.
|
||||
GListModel* pages = app.side_stack != nullptr
|
||||
? G_LIST_MODEL(adw_view_stack_get_pages(ADW_VIEW_STACK(app.side_stack)))
|
||||
: nullptr;
|
||||
for (guint i = 0; pages != nullptr && i < g_list_model_get_n_items(pages); ++i) {
|
||||
AdwViewStackPage* page = ADW_VIEW_STACK_PAGE(g_list_model_get_item(pages, i));
|
||||
const char* page_name = adw_view_stack_page_get_name(page);
|
||||
const char* icon = adw_view_stack_page_get_icon_name(page);
|
||||
app.dbg_page_icons += std::string(app.dbg_page_icons.empty() ? "" : " ") +
|
||||
(page_name != nullptr ? page_name : "?") + "=" +
|
||||
(icon != nullptr ? icon : "-") +
|
||||
(icon != nullptr && gtk_icon_theme_has_icon(theme, icon) ? "(ok)"
|
||||
: "(MISSING)");
|
||||
g_object_unref(page);
|
||||
}
|
||||
// `mapped`, not `visible`: visibility is per widget, so a label whose *parent* is hidden still
|
||||
// reads visible, and in the narrow policy that is exactly the case. `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;
|
||||
const std::function<void(GtkWidget*)> count_drawn = [&](GtkWidget* w) {
|
||||
if (GTK_IS_TOGGLE_BUTTON(w) && gtk_widget_get_mapped(w)) ++pills;
|
||||
// A *mapped* label with text: libadwaita's narrow pill keeps an empty label in the layout, so
|
||||
// "mapped" alone would count the empty ones and read 2 whether the pills were icons or words.
|
||||
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;
|
||||
int named_pills = 0;
|
||||
for (GtkWidget* w = app.side_switcher != nullptr ? gtk_widget_get_first_child(app.side_switcher)
|
||||
: nullptr;
|
||||
w != nullptr; w = gtk_widget_get_next_sibling(w)) {
|
||||
const char* tip = gtk_widget_get_tooltip_text(w);
|
||||
if (tip != nullptr && tip[0] != '\0') ++named_pills;
|
||||
}
|
||||
app.dbg_switcher_names = named_pills;
|
||||
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
|
||||
@@ -3184,6 +3237,11 @@ void update_capture_row(App& app) {
|
||||
gboolean tick(gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
if (app->window == nullptr) return G_SOURCE_REMOVE;
|
||||
// Before the reading, and not after it: the pills are `AdwViewSwitcher`'s own widgets and exist
|
||||
// only once the switcher is mapped, so this is the first frame that can name them -- and their
|
||||
// page titles are deliberately empty, so a frame that read before this ran would read pills with
|
||||
// no text and no name either.
|
||||
name_side_tabs(*app);
|
||||
note_panel_controls(*app);
|
||||
ScopeModel& model = *app->model;
|
||||
const gint64 tick_start_us = g_get_monotonic_time();
|
||||
@@ -4297,6 +4355,39 @@ void on_window_popover_press(GtkGestureClick*, int, double x, double y, gpointer
|
||||
/// gtk_toggle_button_set_active(), which emits ::toggled and so runs exactly the handler a
|
||||
/// click would; the non-toggles call their click handlers with a null button, which they
|
||||
/// already ignore. Split out of the key handler so the probe can fire a row directly.
|
||||
/// The pane's switches and the header bar's buttons are one control with two places to click, so a
|
||||
/// change in either re-reads the other. `panel_switch_sync` is the guard: setting a `GtkSwitch` emits
|
||||
/// its own `notify::active`, and without it the program's write would come back as a click.
|
||||
void sync_panel_switches(App& app) {
|
||||
if (app.panel_switch_sync) return;
|
||||
app.panel_switch_sync = true;
|
||||
const auto mirror = [](GtkWidget* panel_switch, GtkWidget* toolbar_toggle) {
|
||||
if (panel_switch == nullptr || toolbar_toggle == nullptr) return;
|
||||
gtk_switch_set_active(GTK_SWITCH(panel_switch),
|
||||
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toolbar_toggle)) == TRUE);
|
||||
};
|
||||
mirror(app.trigger_switch, app.trigger_toggle);
|
||||
mirror(app.cursors_switch, app.cursors_toggle);
|
||||
app.panel_switch_sync = false;
|
||||
}
|
||||
|
||||
/// A pane's own switch drives the *same action* as the header bar's button, so either place reaches
|
||||
/// the feature down one path, and the sync above is what keeps the two showing the same thing.
|
||||
void on_panel_switch_toggled(GObject* obj, GParamSpec*, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
if (app->panel_switch_sync) return;
|
||||
const bool on = gtk_switch_get_active(GTK_SWITCH(obj)) != FALSE;
|
||||
const bool is_trigger = app->trigger_switch != nullptr && obj == G_OBJECT(app->trigger_switch);
|
||||
GtkWidget* toggle = is_trigger ? app->trigger_toggle : app->cursors_toggle;
|
||||
if (toggle == nullptr) return;
|
||||
// The action toggles the toolbar button, so it is run only when the two actually disagree -- which
|
||||
// is what makes this a *set* rather than a second toggle of something already in that state.
|
||||
if ((gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(toggle)) != FALSE) != on) {
|
||||
run_app_action(*app, is_trigger ? AppAction::TriggerPanel : AppAction::CursorsPane);
|
||||
}
|
||||
sync_panel_switches(*app);
|
||||
}
|
||||
|
||||
void run_app_action(App& app, AppAction action) {
|
||||
const auto toggle = [](GtkWidget* button) {
|
||||
if (button == nullptr) return;
|
||||
@@ -4316,12 +4407,8 @@ void run_app_action(App& app, AppAction action) {
|
||||
case AppAction::CursorsPane:
|
||||
toggle(app.cursors_toggle);
|
||||
break;
|
||||
case AppAction::MeasuresPane:
|
||||
toggle(app.measures_toggle);
|
||||
break;
|
||||
case AppAction::MathPane:
|
||||
toggle(app.math_toggle);
|
||||
break;
|
||||
case AppAction::SideTabMeasure: select_side_tab(app, "measure"); break;
|
||||
case AppAction::SideTabSignals: select_side_tab(app, "signals"); break;
|
||||
case AppAction::LayoutEditor:
|
||||
toggle(app.layout_toggle);
|
||||
break;
|
||||
@@ -4449,11 +4536,6 @@ GtkWidget* build_toolbar(App& app) {
|
||||
g_signal_connect(app.layout_toggle, "toggled", G_CALLBACK(on_layout_editor_toggled), &app);
|
||||
gtk_box_append(GTK_BOX(menu_box), app.layout_toggle);
|
||||
|
||||
app.signal_toggle = menu_toggle("Signal list", nullptr, true);
|
||||
gtk_widget_set_tooltip_text(app.signal_toggle, "show or hide the signal list");
|
||||
g_signal_connect(app.signal_toggle, "toggled", G_CALLBACK(on_signals_toggled), &app);
|
||||
gtk_box_append(GTK_BOX(menu_box), app.signal_toggle);
|
||||
|
||||
gtk_box_append(GTK_BOX(menu_box), menu_separator());
|
||||
|
||||
GtkWidget* export_btn = menu_button("Export visible range...", "Ctrl+E");
|
||||
@@ -4511,12 +4593,10 @@ GtkWidget* build_toolbar(App& app) {
|
||||
app.trigger_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "trigger_toggle"));
|
||||
app.events_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "events_toggle"));
|
||||
app.pause_button = GTK_WIDGET(gtk_builder_get_object(ui, "pause_button"));
|
||||
app.measures_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "measures_toggle"));
|
||||
app.math_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "math_toggle"));
|
||||
app.cursors_toggle = GTK_WIDGET(gtk_builder_get_object(ui, "cursors_toggle"));
|
||||
if (app.menu_button == nullptr || app.sidebar_toggle == nullptr || app.trigger_toggle == nullptr ||
|
||||
app.events_toggle == nullptr || app.pause_button == nullptr || app.measures_toggle == nullptr ||
|
||||
app.math_toggle == nullptr || app.cursors_toggle == nullptr) {
|
||||
app.events_toggle == nullptr || app.pause_button == nullptr ||
|
||||
app.cursors_toggle == nullptr) {
|
||||
// A missing id is a mistake in the `.ui`: the bar comes up with the controls it got rather than taking
|
||||
// the window with it, and `[udpscope] header:` reads whatever geometry is left.
|
||||
return bar;
|
||||
@@ -4538,24 +4618,6 @@ GtkWidget* build_toolbar(App& app) {
|
||||
}),
|
||||
&app);
|
||||
g_signal_connect(app.pause_button, "toggled", G_CALLBACK(on_pause_toggled), &app);
|
||||
g_signal_connect(app.measures_toggle, "toggled",
|
||||
G_CALLBACK(+[](GtkToggleButton* b, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
a->measures_revealed = gtk_toggle_button_get_active(b);
|
||||
if (a->measures_revealer != nullptr) {
|
||||
gtk_revealer_set_reveal_child(GTK_REVEALER(a->measures_revealer),
|
||||
a->measures_revealed);
|
||||
}
|
||||
}),
|
||||
&app);
|
||||
g_signal_connect(app.math_toggle, "toggled", G_CALLBACK(+[](GtkToggleButton* b, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->math_revealer != nullptr) {
|
||||
gtk_revealer_set_reveal_child(GTK_REVEALER(a->math_revealer),
|
||||
gtk_toggle_button_get_active(b));
|
||||
}
|
||||
}),
|
||||
&app);
|
||||
g_signal_connect(app.cursors_toggle, "toggled", G_CALLBACK(on_cursors_toggled), &app);
|
||||
return bar;
|
||||
}
|
||||
@@ -4565,8 +4627,9 @@ void on_activate(GtkApplication* application, gpointer user) {
|
||||
|
||||
install_css();
|
||||
// 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 scroller. Everything that depends on the stream -- the events
|
||||
// pane, the two plot areas, the sidebar's panels -- is built below and put into the slots by id.
|
||||
// line under it, and the sidebar's tab switcher and two-page stack. Everything that depends on the
|
||||
// stream -- the events pane, the two plot areas, the sidebar's panels -- is built below and put
|
||||
// into the slots by id.
|
||||
g_clear_object(&app->window_ui);
|
||||
app->window_ui = gtk_builder_new_from_resource("/org/udpscope/ui/main-window.ui");
|
||||
if (app->window_ui == nullptr) return;
|
||||
@@ -4579,14 +4642,18 @@ void on_activate(GtkApplication* application, gpointer user) {
|
||||
app->status_badge = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_badge"));
|
||||
app->status_label = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_label"));
|
||||
app->status_info_button = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "status_info"));
|
||||
app->side_scroll = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_scroll"));
|
||||
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"));
|
||||
GtkWidget* side_switcher = GTK_WIDGET(gtk_builder_get_object(app->window_ui, "side_switcher"));
|
||||
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"));
|
||||
// A missing id is the one mistake neither the build nor `GtkBuilder` sees, and every use below would
|
||||
// be a null pointer.
|
||||
if (app->window == nullptr || side_view == nullptr || events_divider == nullptr ||
|
||||
app->plot_box == nullptr || app->plot_split == nullptr || app->status_badge == nullptr ||
|
||||
app->status_label == nullptr || app->status_info_button == nullptr ||
|
||||
app->side_scroll == nullptr || side == nullptr) {
|
||||
side == nullptr || side_stack == nullptr || side_switcher == nullptr ||
|
||||
measure_page == nullptr || signals_page == nullptr) {
|
||||
return;
|
||||
}
|
||||
// The window's actions live on the application, so it has to know which one it belongs to.
|
||||
@@ -4714,59 +4781,42 @@ void on_activate(GtkApplication* application, gpointer user) {
|
||||
gtk_popover_set_has_arrow(GTK_POPOVER(app->status_popover), TRUE);
|
||||
}
|
||||
|
||||
// The right-hand side: two independently collapsible panels, each revealed by
|
||||
// its toolbar toggle. No outer scrolled window -- the signal list scrolls
|
||||
// inside its own panel -- so neither panel can be squeezed to nothing by the
|
||||
// other's size request.
|
||||
// `side` is the sidebar column from the `.ui`, resolved above with the rest of the skeleton; the
|
||||
// app keeps it because the panel geometry, the divide and the revealers all ask about it.
|
||||
// The right-hand side is two tabs, not five stacked panels: one tab is on screen at a time, so
|
||||
// no section competes with four others for height. Each page scrolls on its own -- the two are
|
||||
// different heights, and one shared scroller would leave a switch scrolled where the new page
|
||||
// never asked to be.
|
||||
// `side` and `side_stack`/`side_switcher`/`measure_page`/`signals_page` are resolved above with
|
||||
// the rest of the skeleton and checked by the null guard; assign the members here.
|
||||
app->side_box = side;
|
||||
app->side_stack = side_stack;
|
||||
app->side_switcher = side_switcher;
|
||||
|
||||
// The two panels share a vertical box, so they collapse *vertically*: a
|
||||
// horizontal slide would animate the width and leave the panel's full height
|
||||
// reserved -- an invisible gap pushing the other panel down the column.
|
||||
app->trigger_revealer = gtk_revealer_new();
|
||||
gtk_revealer_set_transition_type(GTK_REVEALER(app->trigger_revealer),
|
||||
GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
|
||||
// Measure: what the cursors bracket, and the figures derived from it.
|
||||
app->cursors_panel = build_cursors_panel(*app);
|
||||
gtk_box_append(GTK_BOX(measure_page), app->cursors_panel);
|
||||
gtk_box_append(GTK_BOX(measure_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
gtk_box_append(GTK_BOX(measure_page), build_measures_panel(*app));
|
||||
|
||||
// Signals: the trigger picks a channel from the list beside it.
|
||||
app->trigger_panel = build_trigger_panel(*app);
|
||||
gtk_revealer_set_child(GTK_REVEALER(app->trigger_revealer), app->trigger_panel);
|
||||
gtk_box_append(GTK_BOX(side), app->trigger_revealer);
|
||||
gtk_box_append(GTK_BOX(side), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
|
||||
// The cursors readout sits with the other panels and is revealed by its toggle: the
|
||||
// instruments are on the plot, but what they read has to be somewhere a value can be
|
||||
// selected and copied from.
|
||||
app->cursors_revealer = gtk_revealer_new();
|
||||
gtk_revealer_set_transition_type(GTK_REVEALER(app->cursors_revealer),
|
||||
GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
|
||||
gtk_revealer_set_child(GTK_REVEALER(app->cursors_revealer), build_cursors_panel(*app));
|
||||
gtk_box_append(GTK_BOX(side), app->cursors_revealer);
|
||||
gtk_box_append(GTK_BOX(side), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
|
||||
app->measures_revealer = gtk_revealer_new();
|
||||
gtk_revealer_set_transition_type(GTK_REVEALER(app->measures_revealer),
|
||||
GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
|
||||
gtk_revealer_set_child(GTK_REVEALER(app->measures_revealer), build_measures_panel(*app));
|
||||
gtk_box_append(GTK_BOX(side), app->measures_revealer);
|
||||
gtk_box_append(GTK_BOX(side), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
|
||||
app->math_revealer = gtk_revealer_new();
|
||||
gtk_revealer_set_transition_type(GTK_REVEALER(app->math_revealer),
|
||||
GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
|
||||
gtk_revealer_set_child(GTK_REVEALER(app->math_revealer), build_math_panel(*app));
|
||||
gtk_box_append(GTK_BOX(side), app->math_revealer);
|
||||
gtk_box_append(GTK_BOX(side), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
rebuild_math_pane(*app);
|
||||
|
||||
gtk_box_append(GTK_BOX(signals_page), app->trigger_panel);
|
||||
gtk_box_append(GTK_BOX(signals_page), gtk_separator_new(GTK_ORIENTATION_HORIZONTAL));
|
||||
app->signal_panel = build_signal_panel(*app);
|
||||
app->signal_revealer = gtk_revealer_new();
|
||||
gtk_revealer_set_transition_type(GTK_REVEALER(app->signal_revealer),
|
||||
GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
|
||||
gtk_revealer_set_child(GTK_REVEALER(app->signal_revealer), app->signal_panel);
|
||||
gtk_revealer_set_reveal_child(GTK_REVEALER(app->signal_revealer), TRUE);
|
||||
gtk_box_append(GTK_BOX(side), app->signal_revealer);
|
||||
// Its size request and the scroller around it are the `.ui`'s: the column scrolls as a whole, so a
|
||||
// long signal list or a revealed trigger panel can never push either panel out of the window.
|
||||
gtk_box_append(GTK_BOX(signals_page), app->signal_panel);
|
||||
|
||||
// Both instruments start off, so both *bodies* start insensitive -- stated here by the one place
|
||||
// that knows the default, rather than left to whichever widget happened to be built first. The
|
||||
// bodies and not the panels: each panel's heading carries the switch that turns it on, and a
|
||||
// control inside an insensitive parent cannot be clicked.
|
||||
gtk_widget_set_sensitive(app->trigger_body, FALSE);
|
||||
gtk_widget_set_sensitive(app->cursors_body, FALSE);
|
||||
gtk_widget_set_sensitive(app->cursors_reset, FALSE);
|
||||
|
||||
if (app->side_stack != nullptr) {
|
||||
adw_view_stack_set_visible_child_name(ADW_VIEW_STACK(app->side_stack), "signals");
|
||||
g_signal_connect(app->side_stack, "notify::visible-child-name",
|
||||
G_CALLBACK(on_side_tab_changed), app);
|
||||
}
|
||||
|
||||
// The left side is the events pane *and* the plot, in one row: the pane is a column of its own
|
||||
// (the item's "collapsable pane on the left"), revealed by trigger mode.
|
||||
@@ -5401,6 +5451,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_STATS_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), stats_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_SIDE_TAB_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), side_tab_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_SEARCH_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), search_probe, &app);
|
||||
}
|
||||
@@ -5494,9 +5547,6 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_WINDOWPOP_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), windowpop_probe_step, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_KEYS_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), keys_probe, &app);
|
||||
}
|
||||
if (const char* env = g_getenv("UDPSCOPE_PROBE_COLOR_MS")) {
|
||||
g_timeout_add(static_cast<guint>(std::atoi(env)), color_probe, &app);
|
||||
}
|
||||
@@ -5604,7 +5654,7 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
std::printf("[udpscope] axes.w changes: %d (last at frame %d, side panel up=%d)\n",
|
||||
app.dbg_axes_w_changes, app.dbg_axes_w_change_frame,
|
||||
app.dbg_axes_w_change_side);
|
||||
std::printf("[udpscope] reset button visible: %d\n", app.dbg_reset_visible);
|
||||
std::printf("[udpscope] reset button sensitive: %d\n", app.dbg_reset_sensitive);
|
||||
std::printf("[udpscope] render blank frames: %d of %d painted\n",
|
||||
app.dbg_render_blank_frames, app.dbg_frames_painted);
|
||||
std::printf("[udpscope] traceless paints: %d (%d after the first bind, last at paint %d)\n",
|
||||
@@ -5634,25 +5684,16 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
if (GTK_IS_LABEL(c)) ++labels;
|
||||
if (GTK_IS_LIST_BOX(c)) ++lists;
|
||||
}
|
||||
std::printf("[udpscope] signals panel: labels=%d list=%d\n", labels, lists);
|
||||
}
|
||||
{
|
||||
// The math panel's frame, read from the window: the head row with the title and the add button, and the
|
||||
// list. `buttons=0` is a `.ui` that lost the `+` -- otherwise a panel that cannot add anything, with
|
||||
// nothing else complaining -- and the title is read out to show the file's own text arrived.
|
||||
GtkWidget* panel = app.math_list != nullptr ? gtk_widget_get_parent(app.math_list) : nullptr;
|
||||
GtkWidget* head = panel != nullptr ? gtk_widget_get_first_child(panel) : nullptr;
|
||||
GtkWidget* title = head != nullptr ? gtk_widget_get_first_child(head) : nullptr;
|
||||
int head_children = 0;
|
||||
int buttons = 0;
|
||||
for (GtkWidget* c = head != nullptr ? gtk_widget_get_first_child(head) : nullptr; c != nullptr;
|
||||
c = gtk_widget_get_next_sibling(c)) {
|
||||
++head_children;
|
||||
if (GTK_IS_BUTTON(c)) ++buttons;
|
||||
}
|
||||
std::printf("[udpscope] math panel: head=%d buttons=%d list=%d title='%s'\n", head_children,
|
||||
buttons, app.math_list != nullptr ? 1 : 0,
|
||||
GTK_IS_LABEL(title) ? gtk_label_get_text(GTK_LABEL(title)) : "-");
|
||||
// The list's shape, beside the frame: `rows` is what the list holds, `signals` the streamed
|
||||
// ones, and the `MATH` divider plus the math channels account for the rest -- so
|
||||
// `rows == signals + 1 + math` is the invariant, and `divider=0` is a list that lost its `+`.
|
||||
std::printf("[udpscope] signals panel: labels=%d list=%d rows=%d signals=%zu math=%zu "
|
||||
"divider=%d\n",
|
||||
labels, lists, app.dbg_signal_rows, app.dbg_signal_plain, app.math.size(),
|
||||
app.dbg_signal_rows ==
|
||||
static_cast<int>(app.dbg_signal_plain + 1 + app.math.size())
|
||||
? 1
|
||||
: 0);
|
||||
}
|
||||
{
|
||||
// The events pane's frame, read from the window: the four widgets the `.ui` gives it, and the list
|
||||
@@ -5671,14 +5712,26 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
inside(app.events_list, app.events_revealer));
|
||||
}
|
||||
std::printf("[udpscope] panels: side=%dx%d (visible=%d) signals=%dx%d row_h=%d "
|
||||
"(revealed=%d) trigger=%dx%d (revealed=%d) toggles=%d cursors=%d window=%.3fs "
|
||||
"window_cap=%.1fs\n",
|
||||
app.dbg_side_w, app.dbg_side_h, app.dbg_side_visible, app.dbg_signal_w,
|
||||
app.dbg_signal_h, app.dbg_signal_row_h, app.dbg_signal_revealed,
|
||||
"(page_current=%d) trigger=%dx%d (sensitive=%d) switcher=%d "
|
||||
"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_signal_h, app.dbg_signal_row_h, app.dbg_signal_page_current,
|
||||
app.dbg_trigger_w, app.dbg_trigger_h,
|
||||
app.dbg_trigger_revealed, app.dbg_toggles_visible,
|
||||
app.dbg_trigger_sensitive, app.dbg_switcher_visible,
|
||||
rows.cursors_visible() ? 1 : 0, rows.time_window(),
|
||||
app.window_limit_s);
|
||||
std::printf("[udpscope] panel switches: trigger=%d/%d cursors=%d/%d drove_toolbar=%d "
|
||||
"(pane switch / header bar button; `drove_toolbar` is the pane's own switch having "
|
||||
"moved the button)\n",
|
||||
app.dbg_panel_switch_trigger, app.dbg_toolbar_switch_trigger,
|
||||
app.dbg_panel_switch_cursors, app.dbg_toolbar_switch_cursors,
|
||||
app.dbg_panel_drove_toolbar);
|
||||
std::printf("[udpscope] switcher: policy=%s pills=%d labels=%d names=%d icon=%dx%d "
|
||||
"size=%dx%d\n",
|
||||
app.dbg_switcher_policy == 0 ? "narrow" : (app.dbg_switcher_policy == 1 ? "wide" : "?"),
|
||||
app.dbg_switcher_pills, app.dbg_switcher_labels, app.dbg_switcher_names,
|
||||
app.dbg_switcher_icon_w, app.dbg_switcher_icon_h, app.dbg_switcher_w,
|
||||
app.dbg_switcher_h);
|
||||
std::printf("[udpscope] mode with trigger on: %s\n", app.dbg_mode_triggered.c_str());
|
||||
std::printf("[udpscope] trigger marker x=%.3f, view x_max=%.3f; model trigger_time=%.3f "
|
||||
"valid=%d\n",
|
||||
@@ -5800,6 +5853,7 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
// critical (which is how the first version of this line earned four of them).
|
||||
std::printf("[udpscope] panel controls: %s\n", app.dbg_panel_controls.c_str());
|
||||
std::printf("[udpscope] toolbar icons: %s\n", app.dbg_toolbar_icons.c_str());
|
||||
std::printf("[udpscope] page icons: %s\n", app.dbg_page_icons.c_str());
|
||||
std::printf("[udpscope] app icons: %s\n", app.dbg_app_icons.c_str());
|
||||
// The measurements pane, with the numbers the engine computed: a probe adds measurements and
|
||||
// this is where their values are read, so a wrong threshold or a window that moved under a
|
||||
@@ -5841,11 +5895,12 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
std::printf("[udpscope] badge frames: lit=%d dim=%d idle=%d connecting=%d\n",
|
||||
app.dbg_badge_frames[0], app.dbg_badge_frames[1], app.dbg_badge_frames[2],
|
||||
app.dbg_badge_frames[3]);
|
||||
std::printf("[udpscope] trigger panel revealed before the probe toggled it: %d; "
|
||||
"signal list items=%d selected=%d; column visible after sidebar "
|
||||
"toggle off=%d\n",
|
||||
app.dbg_trigger_revealed_default, app.dbg_trigger_items,
|
||||
app.dbg_trigger_selected, app.dbg_side_scroll_after_hide);
|
||||
std::printf("[udpscope] trigger panel sensitive before the probe toggled it: %d; "
|
||||
"signal list items=%d selected=%d; column visible after sidebar toggle off=%d; "
|
||||
"pane switches live while their panes are off=%d\n",
|
||||
app.dbg_trigger_sensitive_default, app.dbg_trigger_items,
|
||||
app.dbg_trigger_selected, app.dbg_side_column_after_hide,
|
||||
app.dbg_panel_switch_live);
|
||||
{
|
||||
// The panel's *frame* read back: the row boxes the `.ui` holds, and that every control the code built
|
||||
// ended up inside the panel rather than in a row that is not there. A control is created in code and
|
||||
@@ -6078,9 +6133,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(model.mutex());
|
||||
std::printf("[udpscope] measures pane: n=%zu revealed=%d marks_shown=%d pane_rows=%zu "
|
||||
std::printf("[udpscope] measures pane: n=%zu marks_shown=%d pane_rows=%zu "
|
||||
"pane_w=%d pane_h=%d switch_on=%d reset_sensitive=%d\n",
|
||||
app.measures.size(), app.measures_revealed ? 1 : 0,
|
||||
app.measures.size(),
|
||||
rows.measure_marks_placed(), app.measure_labels.size(),
|
||||
app.dbg_measures_pane_w, app.dbg_measures_pane_h,
|
||||
// The two widgets the panel's *frame* is read by: the code drives `measure_stats_on`,
|
||||
|
||||
@@ -38,6 +38,15 @@
|
||||
<property name="css-classes">flat</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<!-- The pane's own switch, the same one the header bar carries: a feature whose pane is
|
||||
disabled while it is off needs its control *in* the pane, or turning it back on means
|
||||
finding the toolbar button that greyed it out. `body` is what goes insensitive, not the
|
||||
panel, so this stays live. -->
|
||||
<object class="GtkSwitch" id="cursors_switch">
|
||||
<property name="valign">center</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
||||
+3
-15
@@ -3,15 +3,15 @@
|
||||
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||||
<!--
|
||||
The header bar: the time-window control's slot, the trigger, pause and events group with the separator that
|
||||
marks where the *view's* controls end, and the five at the far end.
|
||||
marks where the *view's* controls end, and the three at the far end.
|
||||
|
||||
The bar carries only the controls you reach for *while watching a signal*; everything you set up once lives
|
||||
in the overflow menu, which is code — its rows act on the `RowSet` directly and two of them are toggles
|
||||
in the overflow menu, which is code — its rows act on the `RowSet` directly and one of them is a toggle
|
||||
whose state the rest of the interface reads, so a `GMenu` model would only add a second copy of that state.
|
||||
|
||||
The order is the file's, and it is the one thing here that is not self-evident: `pack_end()` lays its
|
||||
children out right to left, so a `<child type="end">` is added leftwards-from-the-right in document order
|
||||
and the screen reads the reverse of it — cursors, measurements, math, sidebar, menu.
|
||||
and the screen reads the reverse of it — cursors, sidebar, menu.
|
||||
|
||||
The first start child is a slot and not the control: the time-window control is code (its menu's rows come
|
||||
from the buffer's limits), and `gtk_header_bar_pack_start()` appends *after* whatever a file has already
|
||||
@@ -64,18 +64,6 @@
|
||||
<property name="active">1</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkToggleButton" id="math_toggle">
|
||||
<property name="icon-name">udpscope-math-symbolic</property>
|
||||
<property name="tooltip-text" translatable="yes">channels derived from the others by a graph of operations: gain, absolute value, filters, sums and the rest. An enabled one is acquired, plotted and measured exactly like a signal</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkToggleButton" id="measures_toggle">
|
||||
<property name="icon-name">udpscope-measure-symbolic</property>
|
||||
<property name="tooltip-text" translatable="yes">compute a value over the visible window of a signal, and optionally mark it on the plot</property>
|
||||
</object>
|
||||
</child>
|
||||
<child type="end">
|
||||
<object class="GtkToggleButton" id="cursors_toggle">
|
||||
<property name="icon-name">udpscope-cursors-symbolic</property>
|
||||
|
||||
+76
-13
@@ -3,7 +3,7 @@
|
||||
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||||
<!--
|
||||
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 with the status line under it, and the sidebar's scroller.
|
||||
events pane's divider, the plot column with the status line under it, and the sidebar's tab stack.
|
||||
|
||||
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
|
||||
@@ -20,7 +20,7 @@
|
||||
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
|
||||
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 `side_column`; and the
|
||||
areas, into the split inside `plot_box`; the sidebar's panels, appended to `measure_page` and `signals_page`; and the
|
||||
status popover, attached to `status_info`.
|
||||
|
||||
The header bar is not here either: it is the window's own titlebar, built by `build_toolbar()`
|
||||
@@ -48,18 +48,81 @@
|
||||
<property name="min-sidebar-width">320</property>
|
||||
<property name="max-sidebar-width">320</property>
|
||||
<property name="sidebar">
|
||||
<!-- The column scrolls as a whole, so a long signal list or a revealed trigger panel can
|
||||
never push either panel out of the window. -->
|
||||
<object class="GtkScrolledWindow" id="side_scroll">
|
||||
<property name="hscrollbar-policy">never</property>
|
||||
<property name="vscrollbar-policy">automatic</property>
|
||||
<property name="min-content-width">300</property>
|
||||
<property name="child">
|
||||
<object class="GtkBox" id="side_column">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="width-request">300</property>
|
||||
<!-- A switcher above a stack, and a scroller *per page*: the two pages are different
|
||||
heights, so one shared scroller around the stack would leave a tab switch scrolled
|
||||
to somewhere the new page never asked for. The switcher is the sibling above, not a
|
||||
child of either scroller: the one control that chooses what you are looking at must
|
||||
not be the first thing to scroll away.
|
||||
|
||||
The policy is `narrow` and the **titles are empty**: `narrow` still draws a label
|
||||
beside an icon (measured: a 48px "Measure" inside a 54px pill), so an empty title is
|
||||
what makes a pill an icon. The pages' *names* are what identify them to
|
||||
`select_side_tab()` and the session file, and the words are re-attached to the pills in
|
||||
code — tooltip and accessible name — by `name_side_tabs()` on the first tick, so an
|
||||
icon-only pill is still a named thing rather than an anonymous button.
|
||||
Each page's icon is a glyph of its own, and each one is a *pair*: the Measure tab is
|
||||
`udpscope-tab-measure-symbolic` (the cursors and the ruler they read against), and the
|
||||
Signals tab is `udpscope-tab-signals-symbolic` (the trigger's edge and the channel list
|
||||
below it). They are drawn for the tabs rather than reusing the icons the toolbar and the
|
||||
Preferences window already carry: a glyph the toolbar uses for one thing means two things
|
||||
the moment a tab shows it too. -->
|
||||
<object class="GtkBox" id="side_column">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="width-request">300</property>
|
||||
<child>
|
||||
<object class="AdwViewSwitcher" id="side_switcher">
|
||||
<property name="stack">side_stack</property>
|
||||
<property name="policy">narrow</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>
|
||||
</property>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwViewStack" id="side_stack">
|
||||
<property name="vexpand">1</property>
|
||||
<child>
|
||||
<object class="AdwViewStackPage">
|
||||
<property name="name">measure</property>
|
||||
<property name="title"></property>
|
||||
<!-- The cursors and the ruler: what the pane brackets, and what it
|
||||
reads the bracketed window with. -->
|
||||
<property name="icon-name">udpscope-tab-measure-symbolic</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="measure_page">
|
||||
<property name="orientation">vertical</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="AdwViewStackPage">
|
||||
<property name="name">signals</property>
|
||||
<property name="title"></property>
|
||||
<!-- The trigger and the channel list it acts on. -->
|
||||
<property name="icon-name">udpscope-tab-signals-symbolic</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>
|
||||
</child>
|
||||
</object>
|
||||
</property>
|
||||
<property name="content">
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- SPDX-License-Identifier: MIT -->
|
||||
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||||
<!--
|
||||
The math panel: a heading row with the button that adds a channel, and the list they go in.
|
||||
|
||||
The rows are not here. A math channel's row carries its name, its unit, its expression, its colour and its
|
||||
own edit and remove buttons, all of which follow the channel — so the file holds the frame and the code
|
||||
holds the rows, as in the measurements and signals panels beside it.
|
||||
|
||||
The heading takes the slack instead of the button, which is what keeps the `+` at the row's end however
|
||||
long the title or the panel is.
|
||||
-->
|
||||
<interface>
|
||||
<object class="GtkBox" id="math_panel">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">4</property>
|
||||
<property name="margin-top">6</property>
|
||||
<property name="margin-bottom">6</property>
|
||||
<property name="margin-start">6</property>
|
||||
<property name="margin-end">6</property>
|
||||
<child>
|
||||
<object class="GtkBox" id="math_head">
|
||||
<property name="orientation">horizontal</property>
|
||||
<property name="spacing">8</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="math_title">
|
||||
<property name="label" translatable="yes">Math channels</property>
|
||||
<property name="halign">start</property>
|
||||
<property name="hexpand">1</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton" id="math_add">
|
||||
<property name="icon-name">list-add-symbolic</property>
|
||||
<property name="css-classes">flat</property>
|
||||
<property name="tooltip-text" translatable="yes">add a math channel</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="math_list">
|
||||
<property name="orientation">vertical</property>
|
||||
<property name="spacing">2</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
||||
+4
-4
@@ -46,7 +46,7 @@
|
||||
<object class="GtkShortcutsShortcut">
|
||||
<property name="visible">1</property>
|
||||
<property name="accelerator">t</property>
|
||||
<property name="title" translatable="yes">Show or hide the trigger panel</property>
|
||||
<property name="title" translatable="yes">Enable or disable trigger mode</property>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
@@ -66,21 +66,21 @@
|
||||
<object class="GtkShortcutsShortcut">
|
||||
<property name="visible">1</property>
|
||||
<property name="accelerator">c</property>
|
||||
<property name="title" translatable="yes">Show or hide the cursors pane</property>
|
||||
<property name="title" translatable="yes">Enable or disable the cursors</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkShortcutsShortcut">
|
||||
<property name="visible">1</property>
|
||||
<property name="accelerator">m</property>
|
||||
<property name="title" translatable="yes">Show or hide the measurements pane</property>
|
||||
<property name="title" translatable="yes">Show the Measure tab</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkShortcutsShortcut">
|
||||
<property name="visible">1</property>
|
||||
<property name="accelerator">g</property>
|
||||
<property name="title" translatable="yes">Show or hide the math pane</property>
|
||||
<property name="title" translatable="yes">Show the Signals tab</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
<!-- SPDX-License-Identifier: MIT -->
|
||||
<!-- SPDX-FileCopyrightText: 2026 Martino Ferrari -->
|
||||
<!--
|
||||
The trigger panel's frame: the heading, one labelled row per setting, the two buttons that run it, and
|
||||
the search block.
|
||||
The trigger panel's frame: one labelled row per setting, the two buttons that run it, and the search
|
||||
block. Its *heading* is not here: the heading row carries the switch that turns trigger mode on and
|
||||
off, and this panel's body is what goes insensitive when it is off, so the heading has to be its
|
||||
sibling rather than a child of it (`build_trigger_panel()` returns the two together).
|
||||
|
||||
Every row here is a label and a place for a control, and the control itself is built in code. A
|
||||
dropdown's model and a spin button's range are the *application's* (the condition list, the level
|
||||
@@ -24,12 +26,6 @@
|
||||
<property name="margin-bottom">8</property>
|
||||
<property name="margin-start">8</property>
|
||||
<property name="margin-end">8</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="trigger_heading">
|
||||
<property name="label" translatable="yes">Trigger</property>
|
||||
<property name="halign">start</property>
|
||||
</object>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox" id="signal_row">
|
||||
<property name="orientation">horizontal</property>
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
<file preprocess="xml-stripblanks">main-window.ui</file>
|
||||
<file preprocess="xml-stripblanks">cursors-panel.ui</file>
|
||||
<file preprocess="xml-stripblanks">measures-panel.ui</file>
|
||||
<file preprocess="xml-stripblanks">math-panel.ui</file>
|
||||
<file preprocess="xml-stripblanks">math-editor.ui</file>
|
||||
<file preprocess="xml-stripblanks">events-pane.ui</file>
|
||||
<file preprocess="xml-stripblanks">header-bar.ui</file>
|
||||
|
||||
+85
-28
@@ -139,18 +139,21 @@ struct App {
|
||||
/// what we synced is ours -- GTK only notifies on a change, so the user cannot produce it.
|
||||
int window_sync_unit = -1;
|
||||
GtkWidget* trigger_toggle = nullptr;
|
||||
GtkWidget* trigger_revealer = nullptr;
|
||||
/// The trigger panel's own box, kept so the defaults probe can read its spin buttons without
|
||||
/// walking the window for them.
|
||||
GtkWidget* trigger_panel = nullptr;
|
||||
GtkWidget* signal_toggle = nullptr;
|
||||
GtkWidget* signal_revealer = nullptr;
|
||||
GtkWidget* signal_panel = nullptr;
|
||||
GtkWidget* side_box = nullptr;
|
||||
/// The sidebar's tab stack and its switcher. The stack is what chooses which panels are on
|
||||
/// screen; a feature's own on/off never does (it sets its panel's sensitivity instead).
|
||||
GtkWidget* side_stack = nullptr;
|
||||
GtkWidget* side_switcher = nullptr;
|
||||
/// The page name the stack is showing, and the one value the session file persists about the
|
||||
/// sidebar. "signals" on a first run.
|
||||
std::string side_tab = "signals";
|
||||
GtkWidget* arm_button = nullptr;
|
||||
GtkWidget* trigger_signal_drop = nullptr;
|
||||
GtkWidget* sidebar_toggle = nullptr;
|
||||
GtkWidget* side_scroll = nullptr;
|
||||
GtkWidget* trigger_edge_drop = nullptr;
|
||||
GtkWidget* trigger_type_drop = nullptr;
|
||||
GtkWidget* trigger_cmp_drop = nullptr;
|
||||
@@ -207,14 +210,56 @@ struct App {
|
||||
/// works, and every other line of the dump still passes.
|
||||
std::string dbg_toolbar_icons;
|
||||
std::string dbg_app_icons;
|
||||
/// The two sidebar pages' own icons and whether the theme resolved them, read on the first tick
|
||||
/// like the two lines above.
|
||||
std::string dbg_page_icons;
|
||||
/// What the switcher is: how many pills it holds, and how many labels it is *drawing*. The label
|
||||
/// count is the one that says the pages are icons rather than words: `narrow` hides them, and the
|
||||
/// titles stay set because they are the pages' names for a screen reader and for the tooltip.
|
||||
int dbg_switcher_pills = -1;
|
||||
int dbg_switcher_labels = -1;
|
||||
int dbg_switcher_names = -1;
|
||||
/// The pills' icon as it is *rendered*: a 32x16 glyph in a 32px box is drawn 1:1, which is what
|
||||
/// keeps its 2px bars crisp.
|
||||
int dbg_switcher_icon_w = -1;
|
||||
int dbg_switcher_icon_h = -1;
|
||||
/// The switcher's own size, beside it: the pills carry 32px icon boxes now, so the tab bar's height
|
||||
/// is what it costs.
|
||||
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
|
||||
/// must agree, and `dbg_panel_drove_toolbar` is the pane's switch having moved the button.
|
||||
int dbg_panel_switch_trigger = -1;
|
||||
int dbg_toolbar_switch_trigger = -1;
|
||||
int dbg_panel_switch_cursors = -1;
|
||||
int dbg_toolbar_switch_cursors = -1;
|
||||
int dbg_panel_drove_toolbar = -1;
|
||||
/// Both panes' switches being *sensitive* while their panes are off -- read before anything turns
|
||||
/// one on. It is the whole point of putting them in the heading: a control inside an insensitive
|
||||
/// parent cannot be clicked, so a switch that went insensitive with its own pane could never turn
|
||||
/// it back on.
|
||||
int dbg_panel_switch_live = -1;
|
||||
int dbg_switcher_policy = -1;
|
||||
/// Whether `name_side_tabs()` has named the pills: it can only run once the switcher is mapped.
|
||||
bool side_tabs_named = false;
|
||||
/// 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.
|
||||
GtkWidget* toolbar = nullptr;
|
||||
GtkWidget* cursors_reset = nullptr;
|
||||
/// The cursors pane: revealed with the toggle, and listing what the instruments
|
||||
/// read -- T0/T1/dT always, per row an Y1/Y2/dY where rulers are up, and in `mix` mode
|
||||
/// the value each cursor crosses each trace at.
|
||||
GtkWidget* cursors_revealer = nullptr;
|
||||
/// The cursors panel's root, kept because cursors-off disables it rather than hiding it.
|
||||
GtkWidget* cursors_panel = nullptr;
|
||||
/// The panes' *own* switches, in their header rows, and the panel bodies those switches disable.
|
||||
/// The switch and the header bar's button are one control with two places to click: each runs the
|
||||
/// same action, and `sync_panel_switches()` re-reads both from the toolbar's state after any
|
||||
/// change, so they cannot disagree.
|
||||
GtkWidget* trigger_switch = nullptr;
|
||||
GtkWidget* cursors_switch = nullptr;
|
||||
GtkWidget* trigger_body = nullptr;
|
||||
/// True while `sync_panel_switches()` is writing the switches: setting a `GtkSwitch` emits its own
|
||||
/// `notify::active`, and without this the program's own write would come back as a click.
|
||||
bool panel_switch_sync = false;
|
||||
/// The cursors pane: listing what the instruments read -- T0/T1/dT always, per row an Y1/Y2/dY
|
||||
/// where rulers are up, and in `mix` mode the value each cursor crosses each trace at.
|
||||
GtkWidget* cursors_mode_drop = nullptr;
|
||||
GtkWidget* cursors_body = nullptr;
|
||||
/// One line per row (rulers) and one per bound trace (`mix`), in the order the body
|
||||
@@ -237,17 +282,12 @@ struct App {
|
||||
/// price the frame tick should pay).
|
||||
std::vector<Measure> measures;
|
||||
std::vector<MeasureResult> measure_results;
|
||||
GtkWidget* measures_revealer = nullptr;
|
||||
GtkWidget* measures_list = nullptr;
|
||||
GtkWidget* measures_toggle = nullptr;
|
||||
// --- math channels --------------------------------------------------------
|
||||
/// The app's copy of the math channels, and the source of truth for the pane. Every change goes
|
||||
/// through `apply_math_channels()`, which writes this and hands it to the model -- the model's own
|
||||
/// copy is what the producer evaluates.
|
||||
/// The app's copy of the math channels: what the signal list's ticks mirror into and what the
|
||||
/// session file is written from. Every change goes through `apply_math_channels()`, which hands it
|
||||
/// to the model -- the model's own copy is what the producer evaluates.
|
||||
std::vector<MathChannel> math;
|
||||
GtkWidget* math_toggle = nullptr;
|
||||
GtkWidget* math_revealer = nullptr;
|
||||
GtkWidget* math_list = nullptr;
|
||||
/// The math editor's builder. The editor's window belongs to the builder's object list, so the builder is
|
||||
/// held for as long as the window is up and cleared on the *next* opening rather than with the window --
|
||||
/// freeing it under a live window frees the window with it. That is the source form's pattern.
|
||||
@@ -358,7 +398,6 @@ struct App {
|
||||
/// that memory reused and the read is a **SIGSEGV** in the middle of the dump.
|
||||
int dbg_measures_pane_w = -1;
|
||||
int dbg_measures_pane_h = -1;
|
||||
bool measures_revealed = false;
|
||||
/// The measurement dialog while one is open (null otherwise). A probe needs a handle on it:
|
||||
/// it is a window of its own, so the main window's widget tree does not reach its buttons.
|
||||
GtkWidget* measure_dialog = nullptr;
|
||||
@@ -508,7 +547,6 @@ struct App {
|
||||
/// True once the renderer's `Style` has been stated (see `tick()`): the renderer does not
|
||||
/// exist until the area is realized, so the one-shot has to be a tick's job.
|
||||
bool style_stated = false;
|
||||
bool signals_panel_initialised = false;
|
||||
/// Set once the user edits the trigger level, so the panel stops defaulting it
|
||||
/// into the middle of the declared range.
|
||||
bool trigger_level_touched = false;
|
||||
@@ -690,7 +728,7 @@ struct App {
|
||||
/// over a run that acquired anything -- that is what "it blinks" means, and a badge
|
||||
/// stuck lit looks identical to a working one in a screenshot.
|
||||
int dbg_badge_frames[4] = {0, 0, 0, 0};
|
||||
int dbg_reset_visible = -1;
|
||||
int dbg_reset_sensitive = -1;
|
||||
std::vector<std::uint64_t> last_stored;
|
||||
std::uint64_t dbg_latches_at_disarm = 0;
|
||||
std::uint64_t dbg_seq_at_disarm = 0;
|
||||
@@ -738,12 +776,16 @@ struct App {
|
||||
int dbg_signal_row_h = 0;
|
||||
int dbg_trigger_w = 0;
|
||||
int dbg_trigger_h = 0;
|
||||
int dbg_signal_revealed = 0;
|
||||
int dbg_trigger_revealed = 0;
|
||||
int dbg_side_visible = 0;
|
||||
int dbg_signal_page_current = 0;
|
||||
int dbg_trigger_sensitive = 0;
|
||||
int dbg_trigger_items = 0;
|
||||
int dbg_trigger_selected = -1;
|
||||
int dbg_side_scroll_after_hide = 1;
|
||||
/// The signal list's shape, captured where the list is built: how many rows it holds, and how many
|
||||
/// of them are streamed signals. The `MATH` divider and the math channels account for the rest, and
|
||||
/// `rows == signals + 1 + math` is the invariant the dump checks -- a list that lost its `+`.
|
||||
int dbg_signal_rows = 0;
|
||||
std::size_t dbg_signal_plain = 0;
|
||||
int dbg_side_column_after_hide = 1;
|
||||
std::string dbg_mode_triggered;
|
||||
std::string dbg_window_text;
|
||||
double dbg_marker_x = 0.0;
|
||||
@@ -751,8 +793,8 @@ struct App {
|
||||
double dbg_trigger_time = 0.0;
|
||||
int dbg_trigger_time_valid = -1;
|
||||
std::string dbg_mode_live;
|
||||
int dbg_toggles_visible = 0;
|
||||
int dbg_trigger_revealed_default = 0;
|
||||
int dbg_switcher_visible = 0;
|
||||
int dbg_trigger_sensitive_default = 0;
|
||||
|
||||
/// The detail string the popover and the exit dump read. The dump used to read the label,
|
||||
/// which no longer carries these fields.
|
||||
@@ -1151,6 +1193,9 @@ constexpr int kEventThumbH = 46;
|
||||
constexpr int kEventThumbMaxH = 104;
|
||||
inline int g_stats_phase = 0;
|
||||
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
|
||||
/// still current, so the report can say whether any of them advanced afterwards.
|
||||
inline std::vector<std::uint64_t> g_side_tab_n_before;
|
||||
inline double g_trigrenum_edge = 0.0;
|
||||
inline std::uint64_t g_trigrenum_latches = 0;
|
||||
inline ConditionProbe g_condition;
|
||||
@@ -1233,6 +1278,13 @@ std::string signal_chip_markup(App& app, const std::vector<SignalInfo>& signals,
|
||||
void on_signal_color_chosen(GObject* source, GAsyncResult* res, gpointer user);
|
||||
void open_signal_color(App& app, std::size_t signal);
|
||||
void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals);
|
||||
/// The `MATH` divider row in the one channel list, and the `+` it carries.
|
||||
GtkWidget* make_math_divider(App& app);
|
||||
void on_math_add_clicked(GtkButton* button, gpointer user);
|
||||
/// The index in `App::math` of the channel a synthetic signal is named after, or `-1`.
|
||||
int math_index_for_signal(App& app, const std::string& name);
|
||||
void on_math_edit_clicked(GtkButton* button, gpointer user);
|
||||
void on_math_delete_clicked(GtkButton* button, gpointer user);
|
||||
void rebuild_trigger_signals(App& app);
|
||||
double window_limit(const App& app);
|
||||
void sync_window_display(App& app, double seconds);
|
||||
@@ -1331,12 +1383,9 @@ void math_dialog_sync(MathDialog* d);
|
||||
void on_math_apply(GtkButton*, gpointer user);
|
||||
GtkWidget* math_row(const char* label, GtkWidget* widget, GtkWidget** row_out);
|
||||
void open_math_dialog(App& app, int edit);
|
||||
GtkWidget* build_math_panel(App& app);
|
||||
void rebuild_math_pane(App& app);
|
||||
GtkWidget* build_measures_panel(App& app);
|
||||
GtkWidget* build_cursors_panel(App& app);
|
||||
void on_cursors_toggled(GtkToggleButton* button, gpointer user);
|
||||
void on_signals_toggled(GtkToggleButton* button, gpointer user);
|
||||
void on_fit_clicked(GtkButton*, gpointer user);
|
||||
void on_fit_x_clicked(GtkButton*, gpointer user);
|
||||
void on_cursors_reset(GtkButton*, gpointer user);
|
||||
@@ -1359,6 +1408,12 @@ void row_menu_fill(RowMenuState& st);
|
||||
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);
|
||||
gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y, gpointer user);
|
||||
void on_side_tab_changed(GObject*, GParamSpec*, gpointer user);
|
||||
void select_side_tab(App& app, const char* name);
|
||||
/// 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.
|
||||
void sync_panel_switches(App& app);
|
||||
void on_panel_switch_toggled(GObject* obj, GParamSpec*, gpointer user);
|
||||
void update_side_visibility(App& app);
|
||||
void on_sidebar_toggled(GtkToggleButton* button, gpointer user);
|
||||
void rebuild_signal_panels(App& app);
|
||||
@@ -1418,6 +1473,8 @@ gboolean measure_probe(gpointer user);
|
||||
gboolean measure_trace(gpointer user);
|
||||
gboolean stats_probe_report(gpointer user);
|
||||
gboolean stats_probe(gpointer user);
|
||||
gboolean side_tab_probe_report(gpointer user);
|
||||
gboolean side_tab_probe(gpointer user);
|
||||
gboolean pan_probe(gpointer user);
|
||||
gboolean arm_probe(gpointer user);
|
||||
gboolean trigrenum_probe_step3(gpointer user);
|
||||
|
||||
+27
-8
@@ -14,7 +14,18 @@ namespace ui_impl {
|
||||
|
||||
/// `%.6g s` is exact enough for a cursor readout and short enough for the panel.
|
||||
void update_cursors_pane(App& app) {
|
||||
if (app.cursors_body == nullptr || !app.rows->cursors_visible()) return;
|
||||
if (app.cursors_body == nullptr) return;
|
||||
if (!app.rows->cursors_visible()) {
|
||||
// A readout quotes a stored sample; with the instruments off there is no sample it quotes, so
|
||||
// the cells blank rather than keeping the last ones. `"--"` is the same spelling an
|
||||
// unavailable cell already uses -- two spellings of one state in one grid would be the defect.
|
||||
for (const App::CursorLine& line : app.cursors_lines) {
|
||||
if (line.a != nullptr) gtk_label_set_text(GTK_LABEL(line.a), "--");
|
||||
if (line.b != nullptr) gtk_label_set_text(GTK_LABEL(line.b), "--");
|
||||
if (line.d != nullptr) gtk_label_set_text(GTK_LABEL(line.d), "--");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (app.cursors_pane_mode != static_cast<int>(app.rows->cursor_mode()) ||
|
||||
app.cursors_pane_rows != app.rows->row_count() ||
|
||||
app.cursors_pane_generation != app.model->generation() ||
|
||||
@@ -85,6 +96,12 @@ GtkWidget* build_cursors_panel(App& app) {
|
||||
app.cursors_mode_drop = GTK_WIDGET(gtk_builder_get_object(ui, "mode_drop"));
|
||||
app.cursors_reset = GTK_WIDGET(gtk_builder_get_object(ui, "reset"));
|
||||
app.cursors_body = GTK_WIDGET(gtk_builder_get_object(ui, "body"));
|
||||
app.cursors_switch = GTK_WIDGET(gtk_builder_get_object(ui, "cursors_switch"));
|
||||
if (app.cursors_switch != nullptr) {
|
||||
gtk_widget_set_tooltip_text(app.cursors_switch,
|
||||
"the cursors, the same switch the header bar carries");
|
||||
g_signal_connect(app.cursors_switch, "notify::active", G_CALLBACK(on_panel_switch_toggled), &app);
|
||||
}
|
||||
if (box == nullptr || app.cursors_mode_drop == nullptr || app.cursors_reset == nullptr ||
|
||||
app.cursors_body == nullptr) {
|
||||
// A missing id is a mistake in the `.ui`. The panel comes up empty rather than taking the window
|
||||
@@ -117,18 +134,20 @@ void on_cursors_toggled(GtkToggleButton* button, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
const bool on = gtk_toggle_button_get_active(button);
|
||||
app->rows->set_cursors_visible(on);
|
||||
// The reset button exists for the cursors: with them hidden there is nothing to
|
||||
// reset, and a button that does nothing reads as broken.
|
||||
gtk_widget_set_visible(app->cursors_reset, on);
|
||||
// The pane is the cursors' readout: with them off there is nothing to read.
|
||||
if (app->cursors_revealer != nullptr) {
|
||||
gtk_revealer_set_reveal_child(GTK_REVEALER(app->cursors_revealer), on);
|
||||
}
|
||||
// Disabled, not hidden, and the same for the reset beside it: with the cursors off there is
|
||||
// nothing to reset, but the button must stay where the user will look for it. The *body* goes
|
||||
// insensitive and not the panel, because the panel's header carries the switch that turns the
|
||||
// cursors back on -- and a control inside an insensitive parent cannot be clicked.
|
||||
gtk_widget_set_sensitive(app->cursors_reset, on);
|
||||
if (app->cursors_body != nullptr) gtk_widget_set_sensitive(app->cursors_body, on);
|
||||
if (!on) update_cursors_pane(*app);
|
||||
if (on) {
|
||||
app->rows->reset_cursors();
|
||||
rebuild_cursors_pane(*app);
|
||||
update_cursors_pane(*app);
|
||||
}
|
||||
// The pane's own switch shows the same state, whichever of the two was clicked.
|
||||
sync_panel_switches(*app);
|
||||
}
|
||||
|
||||
void on_cursors_reset(GtkButton*, gpointer user) {
|
||||
|
||||
@@ -58,6 +58,7 @@ LayoutState collect_layout(App& app) {
|
||||
st.math = app.math;
|
||||
st.measures = app.measures;
|
||||
st.measure_stats = app.measure_stats_on;
|
||||
st.side_tab = app.side_tab;
|
||||
// The connection is the session's too: the file's name carries only host and port.
|
||||
st.multicast = app.source_multicast;
|
||||
st.interface_addr = app.source_interface;
|
||||
@@ -327,6 +328,7 @@ void apply_layout(App& app, const LayoutState& st) {
|
||||
app.measures = st.measures;
|
||||
app.measure_stats_on = st.measure_stats;
|
||||
app.updating_panels = true;
|
||||
select_side_tab(app, st.side_tab.c_str());
|
||||
if (app.measures_stats_check != nullptr) {
|
||||
gtk_check_button_set_active(GTK_CHECK_BUTTON(app.measures_stats_check),
|
||||
app.measure_stats_on);
|
||||
|
||||
+3
-21
@@ -22,7 +22,9 @@ void apply_math_channels(App& app) {
|
||||
// refills that; a file's nothing does, so the view is re-read -- and with the math set now in place
|
||||
// the reload computes the derived channels over the file as it goes.
|
||||
reload_file_view(app);
|
||||
rebuild_math_pane(app);
|
||||
// The math channels are rows of the signal list too -- `append_math_signals_locked()` puts them
|
||||
// in `signals()` -- so the list is stale until it is rebuilt.
|
||||
rebuild_signal_panels(app);
|
||||
schedule_layout_save(app);
|
||||
}
|
||||
|
||||
@@ -1041,25 +1043,5 @@ GtkWidget* math_row(const char* label, GtkWidget* widget, GtkWidget** row_out) {
|
||||
return row;
|
||||
}
|
||||
|
||||
GtkWidget* build_math_panel(App& app) {
|
||||
// The panel's frame -- the heading row with its add button, and the list -- is a UI file; the rows are
|
||||
// not, because each one is a channel and the whole list is rebuilt whenever the set changes. `make test-ui`
|
||||
// checks the file's types against the GIR; the ids below are what keeps the code and the file agreeing.
|
||||
GtkBuilder* ui = ui_load(app, "/org/udpscope/ui/math-panel.ui");
|
||||
GtkWidget* box = GTK_WIDGET(gtk_builder_get_object(ui, "math_panel"));
|
||||
GtkWidget* add = GTK_WIDGET(gtk_builder_get_object(ui, "math_add"));
|
||||
app.math_list = GTK_WIDGET(gtk_builder_get_object(ui, "math_list"));
|
||||
if (box == nullptr || add == nullptr || app.math_list == nullptr) {
|
||||
// A missing id is a mistake in the `.ui`: the panel comes up empty rather than taking the window with
|
||||
// it, and `[udpscope] math panel:` reads the difference.
|
||||
return box != nullptr ? box : gtk_box_new(GTK_ORIENTATION_VERTICAL, 4);
|
||||
}
|
||||
g_signal_connect(add, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
add_math_channel(*static_cast<App*>(user));
|
||||
}),
|
||||
&app);
|
||||
return box;
|
||||
}
|
||||
|
||||
} // namespace ui_impl
|
||||
} // namespace udpscope
|
||||
+178
-31
@@ -74,24 +74,79 @@ gboolean exit_probe_step3(gpointer user) {
|
||||
a->window = nullptr; // stop the tick before the widgets go
|
||||
a->dbg_side_w = gtk_widget_get_width(a->side_box);
|
||||
a->dbg_side_h = gtk_widget_get_height(a->side_box);
|
||||
a->dbg_signal_w = gtk_widget_get_width(a->signal_revealer);
|
||||
a->dbg_signal_h = gtk_widget_get_height(a->signal_revealer);
|
||||
a->dbg_trigger_w = gtk_widget_get_width(a->trigger_revealer);
|
||||
a->dbg_trigger_h = gtk_widget_get_height(a->trigger_revealer);
|
||||
a->dbg_signal_w = gtk_widget_get_width(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_h = gtk_widget_get_height(a->trigger_panel);
|
||||
// The switcher's icon as it is *rendered*: a 32x16 glyph in a 32px box draws 1:1, which is what
|
||||
// keeps its 2px bars crisp rather than blurred by a fractional scale. 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);
|
||||
}
|
||||
// The panes' own switches, and *both* directions of the sync: the header bar's trigger button is
|
||||
// flipped here and the pane's switch has to follow, then the cursors' switch is driven from the
|
||||
// pane and the button is what has to follow. Both are read back after the fact, so the two pairs in
|
||||
// the dump are the claim: they agree, having been moved from opposite ends.
|
||||
if (a->trigger_toggle != nullptr) {
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE);
|
||||
}
|
||||
a->dbg_panel_switch_trigger =
|
||||
a->trigger_switch != nullptr && gtk_switch_get_active(GTK_SWITCH(a->trigger_switch)) ? 1 : 0;
|
||||
a->dbg_toolbar_switch_trigger =
|
||||
a->trigger_toggle != nullptr &&
|
||||
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(a->trigger_toggle))
|
||||
? 1 : 0;
|
||||
if (a->cursors_switch != nullptr) {
|
||||
gtk_switch_set_active(GTK_SWITCH(a->cursors_switch), TRUE);
|
||||
a->dbg_panel_switch_cursors = gtk_switch_get_active(GTK_SWITCH(a->cursors_switch)) ? 1 : 0;
|
||||
a->dbg_toolbar_switch_cursors =
|
||||
a->cursors_toggle != nullptr &&
|
||||
gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(a->cursors_toggle))
|
||||
? 1 : 0;
|
||||
a->dbg_panel_drove_toolbar = a->dbg_toolbar_switch_cursors;
|
||||
}
|
||||
for (GtkWidget* pill = a->side_switcher != nullptr ? gtk_widget_get_first_child(a->side_switcher)
|
||||
: nullptr;
|
||||
pill != nullptr; pill = gtk_widget_get_next_sibling(pill)) {
|
||||
const std::function<void(GtkWidget*)> measure_icon = [&](GtkWidget* node) {
|
||||
if (GTK_IS_IMAGE(node) && gtk_widget_get_mapped(node)) {
|
||||
a->dbg_switcher_icon_w = std::max(a->dbg_switcher_icon_w, gtk_widget_get_width(node));
|
||||
a->dbg_switcher_icon_h = std::max(a->dbg_switcher_icon_h, gtk_widget_get_height(node));
|
||||
}
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(node); c != nullptr;
|
||||
c = gtk_widget_get_next_sibling(c)) {
|
||||
measure_icon(c);
|
||||
}
|
||||
};
|
||||
measure_icon(pill);
|
||||
}
|
||||
// The height of one signal row, which is the thing "the channel toggle is too tall"
|
||||
// was about. The revealer's own height cannot stand in for it: it also covers the
|
||||
// was about. The panel's own height cannot stand in for it: it also covers the
|
||||
// heading, the hint and the box margins.
|
||||
if (GtkWidget* first = gtk_widget_get_first_child(a->signal_list)) {
|
||||
a->dbg_signal_row_h = gtk_widget_get_height(first);
|
||||
}
|
||||
if (a->area != nullptr) a->dbg_frames_painted = static_cast<int>(a->area->frames_painted());
|
||||
a->dbg_reset_visible = a->cursors_reset != nullptr && gtk_widget_get_visible(a->cursors_reset) ? 1 : 0;
|
||||
a->dbg_signal_revealed =
|
||||
gtk_revealer_get_child_revealed(GTK_REVEALER(a->signal_revealer)) ? 1 : 0;
|
||||
a->dbg_trigger_revealed =
|
||||
gtk_revealer_get_child_revealed(GTK_REVEALER(a->trigger_revealer)) ? 1 : 0;
|
||||
a->dbg_side_visible = gtk_widget_get_visible(a->side_box) ? 1 : 0;
|
||||
a->dbg_side_scroll_after_hide = gtk_widget_get_visible(a->side_scroll) ? 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
|
||||
// did, so the reading asks about *state* -- is this page the current one -- and never about size.
|
||||
const char* page = a->side_stack != nullptr
|
||||
? 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;
|
||||
a->dbg_trigger_sensitive = on_signals && gtk_widget_get_sensitive(a->trigger_body) ? 1 : 0;
|
||||
// `gtk_widget_get_visible` on the sidebar's GtkBox is always 1: AdwOverlaySplitView hides its
|
||||
// sidebar child via `gtk_widget_set_child_visible()`, not `gtk_widget_set_visible()`. The useful
|
||||
// reading is whether the split view is currently showing the sidebar panel.
|
||||
const int sidebar_shown = a->side_view != nullptr &&
|
||||
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_switcher_visible =
|
||||
a->side_switcher != nullptr && gtk_widget_get_visible(a->side_switcher) ? 1 : 0;
|
||||
if (a->trigger_signal_drop != nullptr) {
|
||||
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;
|
||||
@@ -100,9 +155,6 @@ gboolean exit_probe_step3(gpointer user) {
|
||||
}
|
||||
// `dbg_window_text` is kept by `sync_window_display()`, the only place that knows the span the
|
||||
// window control is showing: there is no label to read it back from any more.
|
||||
a->dbg_toggles_visible =
|
||||
(gtk_widget_get_visible(a->signal_toggle) && gtk_widget_get_visible(a->trigger_toggle)) ? 1
|
||||
: 0;
|
||||
if (window != nullptr) gtk_window_destroy(GTK_WINDOW(window));
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
@@ -1116,6 +1168,89 @@ gboolean stats_probe(gpointer user) {
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Phase two of the tab-switch probe: the hidden page's accumulator must have advanced. A statistics
|
||||
/// readout is *history*, so min/max/avg would gain a gap for exactly as long as the trigger was on
|
||||
/// 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 tab is current.
|
||||
gboolean side_tab_probe_report(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
std::uint64_t advanced = 0;
|
||||
std::uint64_t min_n = 0;
|
||||
bool first = true;
|
||||
for (std::size_t i = 0; i < a->measure_stats.size(); ++i) {
|
||||
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 (first || n < min_n) {
|
||||
min_n = n;
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
std::fprintf(stderr,
|
||||
"[side-tab-probe] page=%s rows=%zu accums=%zu advanced=%" G_GUINT64_FORMAT
|
||||
" min_n=%" G_GUINT64_FORMAT "\n",
|
||||
a->side_tab.c_str(), a->measures.size(), a->measure_stats.size(), advanced, min_n);
|
||||
std::fflush(stderr);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
/// Phase one: four measurements on the first channel, statistics on, then switch *away* from the
|
||||
/// Measure tab and come back in two seconds to read what ran while it was not on screen.
|
||||
gboolean side_tab_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
// 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
|
||||
// channel with no trace leaves the live window *still* -- and a still window is exactly the state
|
||||
// the tick is supposed to skip, so a probe that left it that way would be testing nothing. This
|
||||
// probe is about the case where the window does move while the page is somewhere else.
|
||||
std::size_t pick = a->model->signals().size();
|
||||
{
|
||||
const std::vector<SignalInfo>& signals = a->model->signals();
|
||||
for (std::size_t i = 0; i < signals.size(); ++i) {
|
||||
if (ScopeModel::is_time_signal(signals, i)) continue; // the time base is not acquired
|
||||
pick = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pick >= a->model->signals().size()) return G_SOURCE_REMOVE;
|
||||
a->model->set_signals_enabled({pick}, true);
|
||||
std::string name;
|
||||
{
|
||||
const std::vector<ChannelInfo>& channels = a->model->channels();
|
||||
if (channels.empty()) return G_SOURCE_REMOVE;
|
||||
name = channels.front().name;
|
||||
a->rows->assign(0, 0, channels.front().name, channels.front().name,
|
||||
signal_color_index(a->model->signals(), channels.front().signal));
|
||||
}
|
||||
a->relayout_pending = true;
|
||||
if (a->measures.empty()) {
|
||||
for (const MeasureType t : {MeasureType::Average, MeasureType::Top, MeasureType::PeakToPeak,
|
||||
MeasureType::Amplitude}) {
|
||||
MeasureOptions options;
|
||||
options.show_on_plot = measure_can_show_on_plot(t);
|
||||
a->measures.push_back({name, t, options});
|
||||
}
|
||||
rebuild_measures_pane(*a);
|
||||
}
|
||||
if (a->measures_stats_check != nullptr) {
|
||||
gtk_check_button_set_active(GTK_CHECK_BUTTON(a->measures_stats_check), TRUE);
|
||||
}
|
||||
select_side_tab(*a, "measure");
|
||||
update_measures(*a, /*force=*/true, /*accumulate=*/true);
|
||||
g_side_tab_n_before.clear();
|
||||
for (const MeasureStatsAccum& s : a->measure_stats) g_side_tab_n_before.push_back(s.n);
|
||||
// Away from the page the measurements are on: its widgets are now unrealized and measure 0x0, and
|
||||
// the tick keeps calling `update_measures()` regardless.
|
||||
select_side_tab(*a, "signals");
|
||||
std::fprintf(stderr, "[side-tab-probe] page=%s accums=%zu baseline recorded\n",
|
||||
a->side_tab.c_str(), g_side_tab_n_before.size());
|
||||
std::fflush(stderr);
|
||||
// Long enough for several throttled recomputes (one per 100 ms while the window moves).
|
||||
g_timeout_add(2000, side_tab_probe_report, a);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
gboolean pan_probe(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
if (a->window == nullptr || a->rows->row_count() <= 0) return G_SOURCE_REMOVE;
|
||||
@@ -2186,8 +2321,11 @@ gboolean export_probe(gpointer user) {
|
||||
gboolean exit_probe_step1(gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
// Record the panel states before the toggles are exercised.
|
||||
a->dbg_trigger_revealed_default =
|
||||
gtk_revealer_get_child_revealed(GTK_REVEALER(a->trigger_revealer)) ? 1 : 0;
|
||||
a->dbg_trigger_sensitive_default = gtk_widget_get_sensitive(a->trigger_body) ? 1 : 0;
|
||||
a->dbg_panel_switch_live =
|
||||
a->trigger_switch != nullptr && gtk_widget_get_sensitive(a->trigger_switch) &&
|
||||
a->cursors_switch != nullptr && gtk_widget_get_sensitive(a->cursors_switch)
|
||||
? 1 : 0;
|
||||
if (a->trigger_toggle != nullptr) {
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->trigger_toggle), TRUE);
|
||||
}
|
||||
@@ -2227,13 +2365,10 @@ void cursors_probe_report(App& a, const char* what) {
|
||||
double my = 0.0;
|
||||
const bool have_mark = a.rows->crossing_at(0, 0, false, mx, my);
|
||||
std::fprintf(stderr,
|
||||
"[cursors-probe] %-14s mode=%-10s on=%d pane=%d t0=%.6f t1=%.6f dt=%.6f "
|
||||
"[cursors-probe] %-14s mode=%-10s on=%d pane_sensitive=%d t0=%.6f t1=%.6f dt=%.6f "
|
||||
"T=[%s] lines=%d marks=%d [%s]\n",
|
||||
what, kModes[mode], a.rows->cursors_visible() ? 1 : 0,
|
||||
a.cursors_revealer != nullptr &&
|
||||
gtk_revealer_get_reveal_child(GTK_REVEALER(a.cursors_revealer))
|
||||
? 1
|
||||
: 0,
|
||||
a.cursors_body != nullptr && gtk_widget_get_sensitive(a.cursors_body) ? 1 : 0,
|
||||
a.rows->cursor_a_x(), a.rows->cursor_b_x(),
|
||||
a.rows->cursor_b_x() - a.rows->cursor_a_x(), t_text.c_str(), n,
|
||||
a.rows->crossings_visible(), lines.c_str());
|
||||
@@ -2498,7 +2633,7 @@ gboolean measures_probe_step(gpointer user) {
|
||||
m.options.show_on_plot = measure_can_show_on_plot(m.type);
|
||||
a->measures.push_back(std::move(m));
|
||||
}
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->measures_toggle), true);
|
||||
select_side_tab(*a, "measure");
|
||||
rebuild_measures_pane(*a);
|
||||
std::fprintf(stderr, "[measures-probe] added %zu measurements on '%s'\n", a->measures.size(),
|
||||
signal.c_str());
|
||||
@@ -4027,24 +4162,36 @@ gboolean keys_probe(gpointer user) {
|
||||
|
||||
before_after("t: trigger panel", a->trigger_toggle, AppAction::TriggerPanel);
|
||||
before_after("c: cursors", a->cursors_toggle, AppAction::CursorsPane);
|
||||
before_after("m: measures", a->measures_toggle, AppAction::MeasuresPane);
|
||||
before_after("g: math", a->math_toggle, AppAction::MathPane);
|
||||
{
|
||||
const std::string before_tab = a->side_tab;
|
||||
run_app_action(*a, AppAction::SideTabMeasure);
|
||||
const std::string after_tab = a->side_tab;
|
||||
char buf[96];
|
||||
std::snprintf(buf, sizeof(buf), " m: tab %s->%s |", before_tab.c_str(), after_tab.c_str());
|
||||
line += buf;
|
||||
}
|
||||
{
|
||||
const std::string before_tab = a->side_tab;
|
||||
run_app_action(*a, AppAction::SideTabSignals);
|
||||
const std::string after_tab = a->side_tab;
|
||||
char buf[96];
|
||||
std::snprintf(buf, sizeof(buf), " g: tab %s->%s |", before_tab.c_str(), after_tab.c_str());
|
||||
line += buf;
|
||||
}
|
||||
before_after("l: layout", a->layout_toggle, AppAction::LayoutEditor);
|
||||
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'
|
||||
// through the real handler, and require the measurements toggle not to move.
|
||||
// through the real handler, and require the side tab not to change.
|
||||
int refused = -1;
|
||||
if (a->window_entry != nullptr) {
|
||||
gtk_menu_button_popup(GTK_MENU_BUTTON(a->window_button));
|
||||
gtk_widget_grab_focus(a->window_entry);
|
||||
const int before = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(a->measures_toggle))
|
||||
? 1 : 0;
|
||||
const std::string before_tab = a->side_tab;
|
||||
const gboolean handled = on_app_key_pressed(nullptr, GDK_KEY_m, 0,
|
||||
static_cast<GdkModifierType>(0), a);
|
||||
const int after = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(a->measures_toggle))
|
||||
? 1 : 0;
|
||||
refused = (handled == FALSE && before == after) ? 1 : 0;
|
||||
const std::string after_tab = a->side_tab;
|
||||
refused = (handled == FALSE && before_tab == after_tab) ? 1 : 0;
|
||||
gtk_menu_button_popdown(GTK_MENU_BUTTON(a->window_button));
|
||||
}
|
||||
char tail[64];
|
||||
|
||||
+182
-35
@@ -24,7 +24,23 @@ void on_signal_switch(GObject* obj, GParamSpec*, gpointer user) {
|
||||
const std::vector<SignalInfo>& signals = app->model->signals();
|
||||
if (index < signals.size()) name = signals[index].name;
|
||||
}
|
||||
// Hold a reference on the emitting widget across the rebuild. `rebuild_signal_panels` destroys
|
||||
// the list that owns `obj` (this check button's row), dropping the row's reference and leaving
|
||||
// only GLib's own per-emission ref. `gtk_check_button_set_active` emits both `notify::active`
|
||||
// and `toggled` in sequence: GLib's ref covers only one emission at a time, so after
|
||||
// `notify::active` finishes GLib unrefs the instance and, without our extra hold, that is the
|
||||
// last reference -- and the subsequent `toggled` emission is then into freed memory, which is
|
||||
// the two `GLib-GObject-CRITICAL` diagnostics. Taking our own ref here and releasing it from
|
||||
// an idle keeps the instance alive until after `set_active` has emitted everything and returned.
|
||||
g_object_ref(obj);
|
||||
app->model->set_signal_enabled(index, on);
|
||||
// `App::math` is the copy the session file is written from, so a math row's tick has to reach it.
|
||||
// Here and not after the `if (!on)` block below: `apply_math_channels()` down there pushes
|
||||
// `App::math` into the model, and a stale `enabled = true` would switch the channel straight back on.
|
||||
{
|
||||
const int k = math_index_for_signal(*app, name);
|
||||
if (k >= 0) app->math[static_cast<std::size_t>(k)].enabled = on;
|
||||
}
|
||||
if (!on) {
|
||||
// A trace of a signal nobody acquires draws nothing, and holds the slot a live one could take.
|
||||
int gone = remove_signal_from_plots(*app, name);
|
||||
@@ -48,6 +64,11 @@ void on_signal_switch(GObject* obj, GParamSpec*, gpointer user) {
|
||||
}
|
||||
// The enable state is part of the session (a signal can be on only as a math channel's source).
|
||||
schedule_layout_save(*app);
|
||||
// Release the extra reference from an idle so that `set_active`'s full emission chain --
|
||||
// `notify::active` and `toggled` -- has completed before the instance can be finalized.
|
||||
g_idle_add_full(G_PRIORITY_DEFAULT_IDLE,
|
||||
+[](gpointer) -> gboolean { return G_SOURCE_REMOVE; },
|
||||
obj, g_object_unref);
|
||||
}
|
||||
|
||||
/// The palette entry a signal owns: its position among the *plottable* signals, the
|
||||
@@ -132,11 +153,110 @@ void on_signal_color_chosen(GObject* source, GAsyncResult* res, gpointer user) {
|
||||
schedule_layout_save(app);
|
||||
}
|
||||
|
||||
/// `App::math` is indexed by position and `signals()` by name, and the mapping between them is
|
||||
/// the name: `append_math_signals_locked()` names each synthetic signal after its channel and
|
||||
/// `enable_locked()` matches it back the same way.
|
||||
int math_index_for_signal(App& app, const std::string& name) {
|
||||
for (std::size_t i = 0; i < app.math.size(); ++i) {
|
||||
if (app.math[i].name == name) return static_cast<int>(i);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// The `MATH` boundary in the one channel list, carrying the `+` that creates a channel. Not
|
||||
/// selectable and not activatable: it is a heading, and a heading that highlights under the
|
||||
/// pointer reads as a row you can plot.
|
||||
GtkWidget* make_math_divider(App& app) {
|
||||
GtkWidget* row = gtk_list_box_row_new();
|
||||
gtk_list_box_row_set_selectable(GTK_LIST_BOX_ROW(row), FALSE);
|
||||
gtk_list_box_row_set_activatable(GTK_LIST_BOX_ROW(row), FALSE);
|
||||
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
gtk_widget_set_margin_top(box, 6);
|
||||
gtk_widget_set_margin_start(box, 6);
|
||||
gtk_widget_set_margin_end(box, 6);
|
||||
GtkWidget* label = gtk_label_new("MATH");
|
||||
gtk_widget_add_css_class(label, "dim-label");
|
||||
gtk_widget_add_css_class(label, "caption-heading");
|
||||
gtk_widget_set_halign(label, GTK_ALIGN_START);
|
||||
gtk_widget_set_hexpand(label, TRUE);
|
||||
gtk_box_append(GTK_BOX(box), label);
|
||||
GtkWidget* add = gtk_button_new_from_icon_name("list-add-symbolic");
|
||||
gtk_widget_add_css_class(add, "flat");
|
||||
gtk_widget_set_tooltip_text(add, "add a math channel");
|
||||
g_signal_connect(add, "clicked", G_CALLBACK(on_math_add_clicked), &app);
|
||||
gtk_box_append(GTK_BOX(box), add);
|
||||
gtk_list_box_row_set_child(GTK_LIST_BOX_ROW(row), box);
|
||||
return row;
|
||||
}
|
||||
|
||||
/// A handler that rebuilds the signal list destroys the widget it was emitted from -- the row it
|
||||
/// lives in, and that row's menu popover with it. GLib holds a reference for the duration of *one*
|
||||
/// emission, and a button's own code keeps running after the handler returns, so the last reference
|
||||
/// has to outlive the emission: hold one here and drop it from an idle. This is the same repair
|
||||
/// `on_signal_switch()` makes, for the same reason.
|
||||
static void outlive_the_rebuild(GtkWidget* widget) {
|
||||
g_object_ref(widget);
|
||||
g_idle_add_full(G_PRIORITY_DEFAULT_IDLE,
|
||||
+[](gpointer) -> gboolean { return G_SOURCE_REMOVE; }, widget, g_object_unref);
|
||||
}
|
||||
|
||||
/// Create a channel, then open the editor on it: a new channel is a single `Source` node with a
|
||||
/// default name, which is of no use until it is given a graph.
|
||||
void on_math_add_clicked(GtkButton* button, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
outlive_the_rebuild(GTK_WIDGET(button));
|
||||
add_math_channel(*a);
|
||||
if (a->math.empty()) return;
|
||||
// `add_math_channel()` appended, so the new channel is the last one; `apply_math_channels()`
|
||||
// inside it has already rebuilt this list, which is why the index is taken after the call.
|
||||
open_math_dialog(*a, static_cast<int>(a->math.size()) - 1);
|
||||
}
|
||||
|
||||
/// The row's menu items resolve their identity the one way the list already does: the widget
|
||||
/// carries its *signal* index, and the name is read off the model under the lock.
|
||||
static std::string signal_name_at(App& app, const std::size_t index) {
|
||||
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||
const std::vector<SignalInfo>& signals = app.model->signals();
|
||||
return index < signals.size() ? signals[index].name : std::string();
|
||||
}
|
||||
|
||||
/// Close the row's popover before anything is opened or destroyed from it: a dialog that takes
|
||||
/// focus over an open popover leaves it hanging over the new window, and `Delete` removes the row
|
||||
/// the popover belongs to, which cannot be left pointing at it.
|
||||
static void dismiss_row_menu(GtkButton* button) {
|
||||
GtkWidget* popover = gtk_widget_get_ancestor(GTK_WIDGET(button), GTK_TYPE_POPOVER);
|
||||
if (popover != nullptr) gtk_widget_set_visible(popover, FALSE);
|
||||
}
|
||||
|
||||
void on_math_edit_clicked(GtkButton* button, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
const std::size_t index = static_cast<std::size_t>(
|
||||
GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(button), "udpscope-signal")));
|
||||
const int k = math_index_for_signal(*a, signal_name_at(*a, index));
|
||||
if (k < 0) return;
|
||||
dismiss_row_menu(button);
|
||||
open_math_dialog(*a, k);
|
||||
}
|
||||
|
||||
void on_math_delete_clicked(GtkButton* button, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
const std::size_t index = static_cast<std::size_t>(
|
||||
GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(button), "udpscope-signal")));
|
||||
const int k = math_index_for_signal(*a, signal_name_at(*a, index));
|
||||
if (k < 0) return;
|
||||
dismiss_row_menu(button);
|
||||
outlive_the_rebuild(GTK_WIDGET(button));
|
||||
a->math.erase(a->math.begin() + k);
|
||||
// Rebuilds the model's signal list, the plots and this list, and schedules the save.
|
||||
apply_math_channels(*a);
|
||||
}
|
||||
|
||||
/// One line per signal: colour chip, checkbox, name, unit, and a menu for the things
|
||||
/// you touch once (information, calibration). A `GtkSwitch` and two always-visible
|
||||
/// buttons made every row about twice this tall, which is what turned a four-signal
|
||||
/// stream into a panel you have to scroll.
|
||||
void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
|
||||
if (app.signal_list == nullptr) return;
|
||||
app.updating_panels = true;
|
||||
GtkWidget* list = app.signal_list;
|
||||
GtkWidget* child = gtk_widget_get_first_child(list);
|
||||
@@ -150,6 +270,12 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
|
||||
// that showed both would name one "sine (A)" and the other "sine (B)" as if they were different
|
||||
// things, and give them different colours. What the row toggles, colours and drags is the signal.
|
||||
std::vector<std::size_t> rows;
|
||||
// The `MATH` divider goes in before the first math row, and once only: `append_math_signals_locked()`
|
||||
// has already put every math channel in `signals()`, so the boundary is the one thing the deleted
|
||||
// math pane contributed that the list does not already have.
|
||||
bool math_divider_done = false;
|
||||
// Streamed signals, counted as they are drawn: the dump reads `rows == signals + 1 + math`.
|
||||
std::size_t plain = 0;
|
||||
for (std::size_t i = 0; i < signals.size(); ++i) {
|
||||
if (ScopeModel::is_time_signal(signals, i)) continue; // the time base is not plottable
|
||||
const std::string base = layer_base_name(signals[i].name);
|
||||
@@ -163,6 +289,13 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
|
||||
|
||||
for (std::size_t i : rows) {
|
||||
const SignalInfo& sig = signals[i];
|
||||
// One list, with a boundary in it: a derived channel *is* a channel, and the divider carries the
|
||||
// `+` that creates the first one.
|
||||
if (sig.math && !math_divider_done) {
|
||||
math_divider_done = true;
|
||||
gtk_list_box_append(GTK_LIST_BOX(list), make_math_divider(app));
|
||||
}
|
||||
if (!sig.math) ++plain;
|
||||
GtkWidget* row = gtk_list_box_row_new();
|
||||
gtk_widget_add_css_class(row, "udpscope-compact");
|
||||
GtkWidget* box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||
@@ -201,11 +334,13 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
|
||||
|
||||
GtkWidget* enable = gtk_check_button_new();
|
||||
gtk_check_button_set_active(GTK_CHECK_BUTTON(enable), app.model->signal_enabled(i));
|
||||
gtk_widget_set_tooltip_text(enable, "acquire this signal");
|
||||
// A derived channel is computed here, not streamed: its tick is its own switch, and it has one
|
||||
// in every view -- a file view has no acquisition to start, which is the other tick's question.
|
||||
gtk_widget_set_tooltip_text(enable, sig.math ? "compute this channel" : "acquire this signal");
|
||||
// A file view has nothing to acquire: an event's or a file's samples are already stored, and
|
||||
// "acquire this signal" is a stream's question. Hidden rather than disabled -- a greyed tick invites
|
||||
// the user to look for the reason, and the reason is that this mode has no acquisition at all.
|
||||
gtk_widget_set_visible(enable, app.waveform_name.empty());
|
||||
gtk_widget_set_visible(enable, sig.math || app.waveform_name.empty());
|
||||
g_object_set_data(G_OBJECT(enable), "udpscope-signal", GSIZE_TO_POINTER(i));
|
||||
g_signal_connect(enable, "notify::active", G_CALLBACK(on_signal_switch), &app);
|
||||
gtk_box_append(GTK_BOX(box), enable);
|
||||
@@ -230,11 +365,6 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
|
||||
gtk_widget_set_margin_start(menu_box, 4);
|
||||
gtk_widget_set_margin_end(menu_box, 4);
|
||||
|
||||
GtkWidget* info = menu_button("Information", nullptr);
|
||||
g_object_set_data(G_OBJECT(info), "udpscope-signal", GSIZE_TO_POINTER(i));
|
||||
g_signal_connect(info, "clicked", G_CALLBACK(on_info_clicked), &app);
|
||||
gtk_box_append(GTK_BOX(menu_box), info);
|
||||
|
||||
// The way back from a chosen colour. The chooser itself cannot carry a "default" button, and a
|
||||
// setting with no undo is a trap; it lives here rather than on the chip because a second
|
||||
// gesture on a one-glyph target is not something a user would find.
|
||||
@@ -260,20 +390,42 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
|
||||
schedule_layout_save(*a);
|
||||
}),
|
||||
&app);
|
||||
gtk_box_append(GTK_BOX(menu_box), recolor);
|
||||
if (sig.math) {
|
||||
// A math row carries its own menu. Information and Calibrate... are a *streamed*
|
||||
// signal's: calibration is applied at ingest, and a derived channel has no stream to
|
||||
// describe. No rename either -- the node editor owns a math channel's name and its unit,
|
||||
// and a second place to change them would be a second answer to the same question.
|
||||
GtkWidget* edit = menu_button("Edit graph...", nullptr);
|
||||
g_object_set_data(G_OBJECT(edit), "udpscope-signal", GSIZE_TO_POINTER(i));
|
||||
g_signal_connect(edit, "clicked", G_CALLBACK(on_math_edit_clicked), &app);
|
||||
gtk_box_append(GTK_BOX(menu_box), edit);
|
||||
gtk_box_append(GTK_BOX(menu_box), recolor);
|
||||
GtkWidget* del = menu_button("Delete", nullptr);
|
||||
gtk_widget_set_tooltip_text(del, "delete this math channel");
|
||||
g_object_set_data(G_OBJECT(del), "udpscope-signal", GSIZE_TO_POINTER(i));
|
||||
g_signal_connect(del, "clicked", G_CALLBACK(on_math_delete_clicked), &app);
|
||||
gtk_box_append(GTK_BOX(menu_box), del);
|
||||
} else {
|
||||
GtkWidget* info = menu_button("Information", nullptr);
|
||||
g_object_set_data(G_OBJECT(info), "udpscope-signal", GSIZE_TO_POINTER(i));
|
||||
g_signal_connect(info, "clicked", G_CALLBACK(on_info_clicked), &app);
|
||||
gtk_box_append(GTK_BOX(menu_box), info);
|
||||
|
||||
GtkWidget* cal = menu_button("Calibrate...", nullptr);
|
||||
gtk_widget_set_tooltip_text(cal, "gain and offset, applied at ingest");
|
||||
g_object_set_data(G_OBJECT(cal), "udpscope-signal", GSIZE_TO_POINTER(i));
|
||||
g_signal_connect(cal, "clicked",
|
||||
G_CALLBACK(+[](GtkButton* b, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
const std::size_t idx = static_cast<std::size_t>(
|
||||
GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(b), "udpscope-signal")));
|
||||
open_calibration(*a, GTK_WINDOW(a->window), idx);
|
||||
}),
|
||||
&app);
|
||||
gtk_box_append(GTK_BOX(menu_box), cal);
|
||||
gtk_box_append(GTK_BOX(menu_box), recolor);
|
||||
|
||||
GtkWidget* cal = menu_button("Calibrate...", nullptr);
|
||||
gtk_widget_set_tooltip_text(cal, "gain and offset, applied at ingest");
|
||||
g_object_set_data(G_OBJECT(cal), "udpscope-signal", GSIZE_TO_POINTER(i));
|
||||
g_signal_connect(cal, "clicked",
|
||||
G_CALLBACK(+[](GtkButton* b, gpointer user) {
|
||||
App* a = static_cast<App*>(user);
|
||||
const std::size_t idx = static_cast<std::size_t>(
|
||||
GPOINTER_TO_SIZE(g_object_get_data(G_OBJECT(b), "udpscope-signal")));
|
||||
open_calibration(*a, GTK_WINDOW(a->window), idx);
|
||||
}),
|
||||
&app);
|
||||
gtk_box_append(GTK_BOX(menu_box), cal);
|
||||
}
|
||||
|
||||
GtkWidget* popover = gtk_popover_new();
|
||||
gtk_widget_add_css_class(popover, "udpscope-menu");
|
||||
@@ -297,15 +449,18 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
|
||||
|
||||
gtk_list_box_append(GTK_LIST_BOX(list), row);
|
||||
}
|
||||
// Appended when there are no math channels at all: the `+` is the only way to create the first
|
||||
// one, so it cannot be conditional on one already existing.
|
||||
if (!math_divider_done) gtk_list_box_append(GTK_LIST_BOX(list), make_math_divider(app));
|
||||
app.dbg_signal_plain = plain;
|
||||
app.dbg_signal_rows = 0;
|
||||
for (GtkWidget* c = gtk_widget_get_first_child(list); c != nullptr;
|
||||
c = gtk_widget_get_next_sibling(c)) {
|
||||
++app.dbg_signal_rows;
|
||||
}
|
||||
app.updating_panels = false;
|
||||
}
|
||||
|
||||
void on_signals_toggled(GtkToggleButton* button, gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
gtk_revealer_set_reveal_child(GTK_REVEALER(app->signal_revealer),
|
||||
gtk_toggle_button_get_active(button));
|
||||
}
|
||||
|
||||
gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y,
|
||||
gpointer user) {
|
||||
App* app = static_cast<App*>(user);
|
||||
@@ -396,17 +551,9 @@ void rebuild_signal_panels(App& app) {
|
||||
rebuild_trigger_signals(app);
|
||||
refresh_window_limits(app);
|
||||
|
||||
// Nothing to configure before a CONFIG arrives: hide the whole column and its
|
||||
// toolbar buttons rather than showing two empty panels. The first CONFIG
|
||||
// reveals the signal panel (the trigger panel stays behind its own button).
|
||||
// Nothing to configure before a CONFIG arrives: hide the whole column.
|
||||
update_side_visibility(app);
|
||||
if (have_signals && !app.signals_panel_initialised) {
|
||||
app.signals_panel_initialised = true;
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.signal_toggle), TRUE);
|
||||
}
|
||||
if (!have_signals) {
|
||||
app.signals_panel_initialised = false;
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.signal_toggle), FALSE);
|
||||
gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(app.trigger_toggle), FALSE);
|
||||
}
|
||||
app.signal_generation = app.model->generation();
|
||||
|
||||
+32
-2
@@ -195,13 +195,19 @@ void on_trigger_toggled(GtkToggleButton* button, gpointer user) {
|
||||
if (on && signal >= 0) {
|
||||
app->model->set_signal_enabled(static_cast<std::size_t>(signal), true);
|
||||
}
|
||||
gtk_revealer_set_reveal_child(GTK_REVEALER(app->trigger_revealer), on);
|
||||
// Disabled, not hidden: a panel you can still see is a panel you can read, and the settings you
|
||||
// are about to switch back on must not vanish while you look for them. The *body* is what goes
|
||||
// insensitive and not the panel, because the panel's heading row carries the switch that turns
|
||||
// this back on -- and a control inside an insensitive parent cannot be clicked.
|
||||
gtk_widget_set_sensitive(app->trigger_body, on);
|
||||
// The event-history button is part of trigger mode and the pane it opens is *on demand*; leaving
|
||||
// trigger mode takes the button away and collapses the pane, so going back to streaming is not a
|
||||
// column of stale events beside the plot. Recording is the other half of that answer, so this is
|
||||
// `sync_events_availability()`'s call and not the trigger's alone.
|
||||
sync_events_availability(*app);
|
||||
gtk_button_set_label(GTK_BUTTON(app->arm_button), on ? "Disarm" : "Arm");
|
||||
// The pane's own switch shows the same state, whichever of the two was clicked.
|
||||
sync_panel_switches(*app);
|
||||
}
|
||||
|
||||
/// The way out of trigger mode, from inside the panel that froze the view: it is
|
||||
@@ -234,6 +240,8 @@ GtkWidget* build_trigger_panel(App& app) {
|
||||
GtkBuilder* ui = ui_load(app, "/org/udpscope/ui/trigger-panel.ui");
|
||||
GtkWidget* box = GTK_WIDGET(gtk_builder_get_object(ui, "trigger_panel"));
|
||||
if (box == nullptr) return gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
|
||||
// The *body* is what trigger-off disables; this panel's index into it is `trigger_body`.
|
||||
app.trigger_body = box;
|
||||
|
||||
app.trigger_signal_drop = gtk_drop_down_new(G_LIST_MODEL(gtk_string_list_new(nullptr)), nullptr);
|
||||
g_signal_connect(app.trigger_signal_drop, "notify::selected",
|
||||
@@ -420,7 +428,29 @@ GtkWidget* build_trigger_panel(App& app) {
|
||||
&app);
|
||||
}
|
||||
sync_trigger_rows(app);
|
||||
return box;
|
||||
|
||||
// The heading row, and the switch that is the *same* control as the header bar's button. Built here
|
||||
// rather than in the file because it must not go insensitive with the body: turning trigger mode
|
||||
// back on would otherwise mean finding the toolbar button that greyed the panel out.
|
||||
GtkWidget* head = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 8);
|
||||
gtk_widget_set_margin_top(head, 8);
|
||||
gtk_widget_set_margin_start(head, 8);
|
||||
gtk_widget_set_margin_end(head, 8);
|
||||
GtkWidget* title = gtk_label_new("Trigger");
|
||||
gtk_widget_set_halign(title, GTK_ALIGN_START);
|
||||
gtk_widget_set_hexpand(title, TRUE);
|
||||
gtk_box_append(GTK_BOX(head), title);
|
||||
app.trigger_switch = gtk_switch_new();
|
||||
gtk_widget_set_valign(app.trigger_switch, GTK_ALIGN_CENTER);
|
||||
gtk_widget_set_tooltip_text(app.trigger_switch,
|
||||
"trigger mode, the same switch the header bar carries");
|
||||
g_signal_connect(app.trigger_switch, "notify::active", G_CALLBACK(on_panel_switch_toggled), &app);
|
||||
gtk_box_append(GTK_BOX(head), app.trigger_switch);
|
||||
|
||||
GtkWidget* outer = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
|
||||
gtk_box_append(GTK_BOX(outer), head);
|
||||
gtk_box_append(GTK_BOX(outer), box);
|
||||
return outer;
|
||||
}
|
||||
|
||||
} // namespace ui_impl
|
||||
|
||||
Reference in new issue
Block a user