10 Commits
Author SHA1 Message Date
Martino Ferrari 91ca29d181 scope windows: the same instruments as the main window
A scope or detached window drew its traces and nothing else: no cursors, no gate band, no trigger
marker, no measurement marks. They are not per-window state to be recreated -- the cursors are one
pair of times, the gate one band, the trigger one instant -- so the main window's row set stays the
**owner** and every window is written from it.

`sync_scope_instruments()` runs in the main tick after the main's own `sync_cursors()`/
`sync_gate()`: a gesture inside a scope window is read back from *that* window's controllers first
(those two calls only adopt while their own plot holds the grab, which is exactly "the drag
happened here"), adopted by the owner, and then every window -- the dragged one included -- is
written from the owner with the same cursor pair, mode and visibility, gate bounds and visibility,
and trigger marker. `RowSet::set_cursors()` is the new pair setter; `trigger_marker_x()`/`_visible()`
are the getters the mirror reads. The measurement marks travel by signal name
(`RowSet::MeasureMark::signal`, filled by the measures pass), so each window finds the row that
plots the signal and draws its ruler or mark there -- or none, when it does not plot it. The search
marks are times on the window and are placed on every window by the same per-frame path, and the
crosshair-axis-labels preference is restated to open windows when it changes.

Probe: `[scope-probe] instruments windows=N cursors=N gate=N trigger=N marks=N (main trigger
on=..)`, after adding a real gated `Top` measurement on `sine` -- the gate is restated visible each
frame by `update_measures()` and marks come only from `show_on_plot` measurements, so the leg uses
the app's own path. Docs: README, the cursors invariant, and the probe readings.
2026-11-06 12:21:38 +01:00
Martino Ferrari a29f38eaa9 session: remember extra windows, and reopen them from the menu
A scope or detached window was never written to the session, and a detached figure vanished from
the layout at the next start -- the figure had been removed from the main rows, and nothing kept
the window that held it.

Each window is now a `[[window]]` block in the session -- `name`, `detached`, and its own
`[[window.plot]]` rows with the same keys the main `[[plot]]` rows carry (signals, row weight,
cell width). `collect_layout()` writes the open windows' present arrangement plus the ones already
closed; loading puts **all** of them in `App::closed_windows` and never opens one: a window is a
view the user closed, and a launch that tiled windows onto the desktop would take that decision
for them (a tiling WM would tile them straight away). The overflow menu gains **Reopen closed
windows (N)**, which opens every remembered window through `open_scope_window_from_state()` -- the
one window constructor, then its rows, weights and signals dealt into its row set.

Closing a window records it, by name (a close/reopen cycle replaces its entry rather than
multiplying them), and schedules the save; **attaching** records nothing, because that window's
figure is on the main window again. A detached figure therefore survives a quit through its
window's entry.

Probe: `[scope-probe] loaded closed=N scopes=0` (remembered, none opened) and `reopen
scopes=..->.. remembered=.. left=0` (close, remember, the menu item's own click reopens).
Docs: README, the Mallard layout page, the probe readings and the session invariant.
2026-11-06 12:06:21 +01:00
Martino Ferrari 0c9333858c scope windows: their own layout editor, and the same shortcuts
A detached window is a view with an arrangement of its own -- and there was no way to change it:
the layout editor was the main window's, and a second window's rows could not be added or removed.

Every scope window now carries its own editor: `L` (or the grid toggle in its corner, revealed by
the same proximity rule as the attach button) shows the main editor's `+` / `×` / Done controls,
and they act on **that window's** row set (`set_scope_layout_editing()`, every button tagged with
its window), so a window holds as many figures as the user gives it and the two editors cannot
move each other's rows. Nothing a scope window arranges is written to the main window's session:
the windows themselves are not persisted.

The keyboard: each scope window gets a `GtkEventControllerKey` running the application's one
accelerator table -- `L` acts on the window the key arrived in, every other action reaches the
application through `run_app_action()`. The plot's own keys ('f', the arrows, Escape) already
worked there, through its plot controller.

The window struct now always builds its overlay (the corner controls' host); the attach button
stays detached-only, and both corner toggles step aside while the editor is up (Done is the way
out).

Probe: `[scope-probe] layout buttons=3 rows=1->2` -- the toggle builds 2*rows+1 buttons and a
boundary `+` adds a row. `L` in a real scope window verified by OCR ("Done" appears, the row's
`×` at its left). Docs: README, the Mallard layout page, the probe readings, and the layout
invariant (a view's arrangement is the view's).
2026-11-06 10:54:50 +01:00
Martino Ferrari ecdbf4f0e1 scope windows: mirror a pin as a range, and restore the tick rebuild
**The blank scope window in trigger mode.** A held event pins the main window to
`[-pre, +post]`, and the tick mirrors that pin onto every scope window. It passed
`pinned_range().min/max` to `pin_event_window()`, which takes **spans** (`pre, post`): `min` is
signed, so `pre` came out negative and the group range became `[+pre, +post]` — an inverted x
window. The plotter then had nothing to draw: no trace, no x-axis labels, an empty plot that
looked like a broken window. `RowSet::pin_display_range(x0, x1)` states the pin as the range a
mirroring view actually has, and the tick uses it; live mode was never affected, which is why the
defect only showed under the trigger.

Also restores the scope-window `rebuild_if_dirty()` in the tick, which a reproduction experiment
had left commented out (`REPRO-TEMP`) and a later commit carried: a window opened empty and filled
by a drop kept the figure it was built with.

Probe: `[scope-probe] pin_mirror=1 main=[..] scope=[..] x=[..]` pins the main window, reads the
scope's pin and plot x range a frame later, and reads `0` for an inverted or mismatched pin.
Docs: probe readings and the trigger/pinning invariant (range vs spans).
2026-11-06 10:39:10 +01:00
Martino Ferrari 2279e2140a scope windows: their own frame pump, and a diagnosis that names the GSK renderer
A second toplevel's frame clock is not the main window's: the tick binds, queues a render and
mirrors the span, but with nothing asking GTK for frames on *that* window the area can sit with
its queue full and present nothing -- a blank plot. Every scope window now carries a tick
callback on its own area (`gtk_gl_area_queue_render` per frame), which is the request that keeps
its frame clock running.

The "cannot paint" diagnosis now names the window's GSK renderer (`[GSK renderer: GskVulkanRenderer]`)
and says to restart with `GSK_RENDERER=gl` when it is the Vulkan one -- the interaction between
the Vulkan GSK renderer and an embedded `GtkGLArea` in a second toplevel is a known trouble spot,
and a tiling WM has no title bar to carry a title, so the sentence goes to stderr and the status
bar instead.
2026-11-06 10:09:28 +01:00
Martino Ferrari fa2d0f12fc scope windows: Fit X axis / Fit all in the menu, and a window that cannot paint says so
The scope menu carried a y fit only: with one shared time span, the x fit *is* the main window's
fit, and the next frame mirrors the result back -- so `Fit X axis` and `Fit all` now sit in the
menu beside the y entries and reset the shared range.

And a blank plot becomes a sentence: a scope window whose GL context failed, or whose area has
painted no frame two seconds after opening, writes the reason into its own title and to stderr
(`[scope] window 'plot 1' is not drawing: GL context failed: ...`). A blank rectangle is
indistinguishable from a failed context or an area that never got a frame, and the report "the
plot is not there, no x axis" is exactly what this answers: if the title stays unchanged, the
area is painting and the question moves to the compositor presenting the second window.
2026-11-06 09:03:54 +01:00
Martino Ferrari 2924f7627d scope windows: a right-click menu of their own
A scope window is a view with its own arrangement, but its plot had no menu: every right-click
went nowhere, because the main window's row menu acts on the main window's rows.

`on_scope_right_click()` wires each scope window's GL area (beside its drop target) to a menu
that acts on **that window's** row set: y axis auto scale, y axis fixed range (the main menu's
dialog, aimed at the window's rows through `FixedRangeContext::rows`), fit, and one
*Remove '<signal>'* per trace on the plot. A detached window's menu also carries **Attach back
to the main window**. What it deliberately does not carry is an x fit: the window follows the
main window's time span, so an x fit here would be undone by the next frame's mirror.

`remove_trace()` and `FixedRangeContext` are generalized to take the row set they act on (the
main window passes its own). Probe: `[scope-probe] rclick=1` asserts the click gesture is wired
per window; the menu build is exercised by the real right-clicks the smoke makes.
2026-11-06 08:55:29 +01:00
Martino Ferrari b90a99ec39 scope probe: read the windows' own GL account, and drop into the empty window
`[scope-probe]` grows the two readings a "blank scope window" report needs before it can be
blamed: `empty_drop=`/`empty_slot0=` (the same drop onto the empty window, the user's own
path) and `gl copy=/gl empty=/gl detached ok= frames=` -- each window's adapter context
status and staged-frame count. A scope whose context failed draws nothing at all: no axes,
no traces, exactly the "blank plot widget, no x axis" report -- and that is a driver/context
finding, not a layout one. The detached window is left open by the probe, so a run's
screenshots can answer "does the detached figure draw" with pixels.
2026-11-06 08:19:50 +01:00
Martino Ferrari 5ed75eb470 layout: detach a figure to its own window, and attach it back
Beside each figure row's full-screen toggle -- same corner, same show-on-approach reveal, same
one-figure/never-maximized rules -- a **detach** button moves the figure into a window of its
own: copied into a new scope window (titled "detached") and removed from the main window's rows,
which then save the change. The detached window is the one scope window with an **attach**
control: an overlay wrapping its GL area, a button in the top-right corner revealed by the same
pointer-proximity rule from the overlay's own motion controller, and one click appends the
figure back to the main window's rows (cells, weights, every slot's signal in its colour) and
closes the window.

`detach_figure()` / `attach_scope_window()`; the reveal and placement share the maximize
buttons' geometry and pointer state, per row. A detached window's rows participate everywhere a
scope window's do (drops, the signal page's *Plot on* groups), which the previous commit gave
them.

Probe: `[maximize-probe]` now counts the detach buttons (`detach_single=0` for one figure,
`detach=2` for two, absent while maximized) and reads their corner reveal
(`detach_near=1 detach_other=0`, the pointer parked on the detach button's own centre -- the
strictest zone of the pair). Real-pointer smoke: hover reveals the pair, clicking detach opens
the window, clicking attach closes it with the figure back. Docs: README, the Mallard layout
page, the probe readings, the layout invariant, and the tour's plot stop.
2026-11-05 23:35:26 +01:00
Martino Ferrari 51b4fad386 scope windows take drops, and the signal page lists them
The second windows were read-only by accident: nothing accepted a drop on them, and the
signal page's *Plot on* buttons knew only the main window's rows.

- **Drop target per scope window**: the same string payload a badge drags (the signal index),
  handled by `on_scope_drop_channel()` -- the target carries its own window, the row is that
  window's cell under the pointer, and `assign_signal_to_scope()` lands the signal in the
  window's own row set (enabling an unacquired signal first, exactly as the main drop does).
  The rebuild and the area's re-point are the window's next frame in the tick: a drop arrives
  from inside the very widget it rearranges, and a synchronous rebuild would reset the drag
  controller under it.
- **Plot on, per window**: the signal page carries one named group per scope window ("In
  \"plot 2\""), one button per plot of that window, sensitive on that window's own rows. A
  window closing refreshes the pages in its destroy handler, before the deferred delete could
  dangle a button's scope pointer.
- Probe: `[scope-probe]` drops through the wired handler (`drop=1 slot1='square'`) and counts
  the page's groups (`page_groups=2`); the section documents that it must not run with
  `UDPSCOPE_ENABLE_ALL_MS` -- the enable-all probe's second leg clears the front plot, which
  is the slot this probe copies. README, help and the layout invariant follow.
2026-11-05 19:37:42 +01:00
20 changed files with 1760 additions and 57 deletions

No files matched your search

+31 -1
View File
@@ -129,7 +129,11 @@ rect plus a `kMaximizeRevealPx` of slack, per row) and is invisible otherwise, s
decorated by controls nobody is reaching for; one click makes that row fill decorated by controls nobody is reaching for; one click makes that row fill
the plot area and the same button restores the whole arrangement; the plot's right-click menu the plot area and the same button restores the whole arrangement; the plot's right-click menu
offers the same as **Full screen this figure** / **Restore the layout**, and a layout of a single offers the same as **Full screen this figure** / **Restore the layout**, and a layout of a single
figure carries no button (there is nothing to enlarge *to*). It is a *view* — the row set figure carries no button (there is nothing to enlarge *to*). Beside the toggle, and with the
same show-on-approach, a **detach** button moves the figure into a window of its own (removed
from the main window's rows, which save the change); the detached window carries an **attach**
button in the same corner — its figure returns to the main window's rows and the window closes.
It is a *view* — the row set
saves every row's state on the way in and re-applies it on the way out (`RowState` in saves every row's state on the way in and re-applies it on the way out (`RowState` in
`plot_host.*`), so nothing plotted moves and nothing is saved. A plot's right-click menu has `plot_host.*`), so nothing plotted moves and nothing is saved. A plot's right-click menu has
**Open this plot in a new window**, which copies that figure — its cells, their width shares and **Open this plot in a new window**, which copies that figure — its cells, their width shares and
@@ -138,6 +142,32 @@ and `PlotArea`; the overflow menu's **New scope window** opens one with an empty
window is another *view* of the same acquisition: the main window's tick binds and acquires its window is another *view* of the same acquisition: the main window's tick binds and acquires its
rowset under the same model mutex, mirrors the main window's time window (or its pin) onto it rowset under the same model mutex, mirrors the main window's time window (or its pin) onto it
every frame, and pumps its render — so the two windows never disagree about the data, the display every frame, and pumps its render — so the two windows never disagree about the data, the display
mode or the span. **A signal badge dragged onto a scope window's plot lands there** — the same
drop the main plot takes, into that window's own row set, on the cell the pointer is over — and
the signal page's *Plot on* buttons list **every window**: one group per window (named, `In
"plot 2"`), one button per plot of it. **The session remembers every extra window**, but a launch never opens one: a window is a view
you closed on purpose, and a start that tiled them onto the desktop would take that decision for
you. The overflow menu's **Reopen closed windows (N)** brings them back, arrangement and all; a
window that was *attached* is not remembered (its figure is on the main window again), and a
detached figure is preserved by its window's entry, so quitting with one open no longer loses it.
A scope window draws the **same instruments as the main window** — the two cursors (draggable
there, and shared: moving one in any window moves it everywhere), the gate band, the trigger
marker and the measurements' marks on the traces each window actually plots. They have one owner
(the main window's plot set) and are written into every window each frame, so no window can drift.
A scope window also **carries its own layout editor**: `L` (or the grid button in its corner,
revealed like the others) shows the same `+`/`×`/Done controls the main window's editor has, and
they change *that* window's rows — so a second window can hold several figures as easily as the
main one. A right-click on a scope window's plot opens **its own menu** — y axis (auto or a fixed
range), *Fit X axis*, *Fit all*, remove a trace — acting on that window's rows (the x fit resets the **shared** time span, so every view follows); a detached
window's menu carries **Attach back to the main window** beside them. A window that cannot paint
says so on stderr and in the status bar after two seconds, naming its **GSK renderer**: a blank
plot is indistinguishable from a context that failed, and the GTK **Vulkan** renderer is known to
interact badly with an embedded `GtkGLArea` in a second toplevel — the sentence then says to
restart with `GSK_RENDERER=gl`. Each scope window also carries a **tick callback of its own**,
which keeps its toplevel's frame clock advancing: the main window's tick binds and queues the
render, but a second window whose clock never ticks presents nothing.
mode or the span. mode or the span.
**The window.** The plot is the window. Under it sits a fixed-height **badge bar** (36 px). Its left **The window.** The plot is the window. Under it sits a fixed-height **badge bar** (36 px). Its left
+17 -1
View File
@@ -357,7 +357,14 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
(same display mode, same epoch), and its span follows the main window's window or pin every frame. It is (same display mode, same epoch), and its span follows the main window's window or pin every frame. It is
therefore a second *view* -- the settle rule is that a scope must never acquire, plan or epoch anything of therefore a second *view* -- the settle rule is that a scope must never acquire, plan or epoch anything of
its own. Closing the main window destroys its scopes first: a leftover toplevel keeps the GApplication's its own. Closing the main window destroys its scopes first: a leftover toplevel keeps the GApplication's
loop alive around a window that is gone (the exit run's hang). loop alive around a window that is gone (the exit run's hang). What a user *arranges* in a scope is the
scope's own arrangement, and it takes the same two doors the main window's rows take: a badge **dropped on
a scope window's plot** lands in that window's row set on the cell under the pointer (its own drop target,
its own `assign_signal_to_scope()`), and the signal page's *Plot on* section carries **one named group per
window** -- rebuilt out of `App::scopes` on every fill, and again when a window closes (the destroy handler
refreshes the pages before the deferred delete can dangle a button's scope pointer). The change itself is
never applied synchronously: a drop arrives from inside the very widget it rearranges, so the rebuild and
the area's re-point are the window's next frame in the tick.
- **A figure row's full-screen is one click from two places, and never from one figure.** The - **A figure row's full-screen is one click from two places, and never from one figure.** The
toggle sits on the row's top-right corner, **out of hiding only while the pointer is near that toggle sits on the row's top-right corner, **out of hiding only while the pointer is near that
@@ -373,3 +380,12 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
figure row carries **no** toggle (and no menu entry): there is nothing to enlarge *to*, and a figure row carries **no** toggle (and no menu entry): there is nothing to enlarge *to*, and a
visible no-op is worse than none. The button reappears while a row *is* maximized, because visible no-op is worse than none. The button reappears while a row *is* maximized, because
there it is the way back. there it is the way back.
- **A view's arrangement is the view's, editor included.** A scope window holds its own figure
rows and edits them with its own `+`/`×`/Done set (`set_scope_layout_editing()`,
`rebuild_scope_layout_editor()`, `sync_scope_layout_editor()`): the buttons are tagged with the
window and the handlers act on *that* window's row set, so the two editors cannot move each
other's figures. `L` acts on the window the key arrived in (a `GtkEventControllerKey` per
window; every other accelerator reaches the application through `run_app_action()`), and a
pinned layout change is **not** written to the main window's session — the windows themselves
are not persisted.
@@ -130,3 +130,14 @@ Part of [CLAUDE.md](../../CLAUDE.md). These are rules that cost a defect to lear
stages the badge texts, which leave with the rest of the over batch over everything. An stages the badge texts, which leave with the rest of the over batch over everything. An
assertion on badge pixels inside a label's rows is the regression test: with the fill under assertion on badge pixels inside a label's rows is the regression test: with the fill under
the text, the label's glyph pixels are still lit inside the badge's rect. the text, the label's glyph pixels are still lit inside the badge's rect.
- **The instruments have one owner: the main window's row set.** The two cursors, the gate band,
the trigger marker and the measurement marks are *shared*, not copied per window. The main tick's
`sync_scope_instruments()` writes the owner's state into every scope window each frame (the
setters early-out, so a steady frame is a handful of comparisons), and a gesture inside a scope
window is read back from **that** window's controllers first -- `sync_cursors()`/`sync_gate()`
only adopt while their own plot holds the grab, which is exactly "the drag happened here" -- and
adopted by the owner before anything is written out. The measurement marks travel by **signal
name** (`RowSet::MeasureMark::signal`), so a window finds the row that plots it; a window that
does not plot the signal draws none. The search marks are times on the window and are placed on
every window by the same per-frame path.
@@ -204,3 +204,14 @@ one, which is the only ordering in which a switch cannot lose what it switches f
configuration's folder): an upgrade keeps the events it had. The key is a *sanitized* path configuration's folder): an upgrade keeps the events it had. The key is a *sanitized* path
component, the same rule the configuration files use, so a name typed with a slash cannot component, the same rule the configuration files use, so a name typed with a slash cannot
leave the directory. leave the directory.
- **The session remembers extra windows; a launch never opens them.** Each scope or detached
window is a `[[window]]` block (`name`, `detached`, and its own `[[window.plot]]` rows -- the
same keys the main `[[plot]]` rows carry), written from the open windows' present arrangement
plus the ones already closed (`collect_layout()`). Loading puts **all** of them in
`App::closed_windows`, never into `app.scopes`: the overflow menu's *Reopen closed windows (N)*
is the only path that opens them (`open_scope_window_from_state()`, the one constructor every
window uses, then its rows). Closing a window records it (by name, replaced, so close/reopen
cycles do not multiply entries); **attaching** records nothing -- its figure is on the main
window again -- and a detached figure survives a quit through its window's entry instead of
being lost from the main layout.
@@ -629,3 +629,11 @@ samples. Three rules keep that honest:
its ink is not in the widget's paint tree, so `gtk_snapshot_free_to_node()` on a widget paintable of its ink is not in the widget's paint tree, so `gtk_snapshot_free_to_node()` on a widget paintable of
it returns NULL and the "screenshot" would be an empty file. Drawing it in the store also keeps that it returns NULL and the "screenshot" would be an empty file. Drawing it in the store also keeps that
class GTK-free, which is what makes the cache testable without a window. class GTK-free, which is what makes the cache testable without a window.
- **A pin is mirrored as a *range*, never as a pair of numbers that might be spans.** `Range`
(`pinned_range()`: `{-pre, +post}`) and the pin's *spans* (`pin_event_window(pre, post)`) are two
different things that both look like a pair, and mixing them inverts the window: a second view
that handed `pinned_range().min` to `pin_event_window()` pinned to `[+pre, +post]`, and the plot
drew nothing — no trace, no axis labels — because the x range was degenerate. Views that mirror a
pin use `pin_display_range(x0, x1)`, the range's own form. `[scope-probe] pin_mirror=1` reads the
two windows' pins and the scope's x range, and an inverted pin reads `0`.
+70 -4
View File
@@ -1422,19 +1422,28 @@ Measured output (viewport pixels; the absolute numbers are the headless window's
## `[maximize-probe]` ## `[maximize-probe]`
`UDPSCOPE_PROBE_MAXIMIZE_MS`. The one-click **full-screen row** toggle, exercised through the `UDPSCOPE_PROBE_MAXIMIZE_MS`. The one-click **full-screen row** toggle, exercised through the
`RowSet` call the button's handler makes. **Do not run it with `UDPSCOPE_ENABLE_ALL_MS`**: the
enable-all probe's second leg clears the front plot two seconds in, and this probe's `before`
read then names an empty row (`row0=`) -- a probe race, not a defect. The full-screen recipe:
`RowSet` call the button's handler makes (a probe cannot click an overlay button the frame `RowSet` call the button's handler makes (a probe cannot click an overlay button the frame
places), arranged as two single-cell rows with one signal each. The two reads after it are the places), arranged as two single-cell rows with one signal each. The two reads after it are the
**corner reveal**: no pointer over the area, then a pointer position in the first row's corner **corner reveal**: no pointer over the area, then a pointer position in the first row's corner
(set directly -- the probe owns the state the motion controller fills). Measured output: (set directly -- the probe owns the state the motion controller fills). Measured output:
``` ```
[maximize-probe] single_buttons=0 before: rows=2 frows=2 row0=sine row1=square buttons=2; max: rows=1 frows=1 flag=1 row0=sine; back: rows=2 row0=sine row1=square flag=0 [maximize-probe] single_buttons=0 detach_single=0 before: rows=2 frows=2 row0=sine row1=square buttons=2 detach=2; max: rows=1 frows=1 flag=1 row0=sine; back: rows=2 row0=sine row1=square flag=0
[maximize-probe] corner: hidden_when_off=1 shown_when_near=1 other_row=0 [maximize-probe] corner: hidden_when_off=1 shown_when_near=1 other_row=0 detach_near=1 detach_other=0
``` ```
- `single_buttons=` is the toggle count *before* the second row is added, while the layout is - `single_buttons=` is the toggle count *before* the second row is added, while the layout is
the default one figure: **0** — one figure has nothing to enlarge *to*, so it carries no the default one figure: **0** — one figure has nothing to enlarge *to*, so it carries no
button (and `buttons=` is what the same count reads once the second row exists). button (and `buttons=` is what the same count reads once the second row exists).
`detach_single=` is the same for the **detach** buttons: **0** — detaching the only figure
would leave the main window with nothing to draw — and `detach=2` once the second row exists
(and **0** again on the `max:` line, where a maximized layout has hidden its rows).
- the corner leg's `detach_near=1 detach_other=0` read the detach buttons with the same pointer
the maximize toggle reads: both come out together at the corner, the other row's stay hidden.
The pointer sits on the detach button's own centre — its zone is the strictest of the pair.
- `before:` is the arrangement as set: two plots, two figure rows, the signals on their rows, - `before:` is the arrangement as set: two plots, two figure rows, the signals on their rows,
and `buttons=` one overlay toggle per figure row (the toggles are on screen whether or not and `buttons=` one overlay toggle per figure row (the toggles are on screen whether or not
the layout editor is up). the layout editor is up).
@@ -1453,19 +1462,76 @@ places), arranged as two single-cell rows with one signal each. The two reads af
`UDPSCOPE_PROBE_SCOPE_MS`. The **second window**: `open_scope_window()` twice — once copying `UDPSCOPE_PROBE_SCOPE_MS`. The **second window**: `open_scope_window()` twice — once copying
the main window's figure 0, once empty — and read back a few frames later (the main tick is the the main window's figure 0, once empty — and read back a few frames later (the main tick is the
scope's bind/acquire/repaint pump). Measured output: scope's bind/acquire/repaint pump). It can run beside `UDPSCOPE_ENABLE_ALL_MS`, but that probe's
second leg clears the front plot through the signal page, so the main's slot 0 is empty by then
and `copied` names the **first non-empty** copied slot instead (`'square'` in that order, `'sine'`
alone). `copied=''` with every slot empty is a copy that moved nothing. Measured output:
``` ```
[scope-probe] loaded closed=2 scopes=0
[scope-probe] count=2 copied='sine' bound=1 empty_ok=1 windows=1 [scope-probe] count=2 copied='sine' bound=1 empty_ok=1 windows=1
[scope-probe] drop=1 slot1='square' page_groups=2 empty_drop=1 empty_slot0='square'
[scope-probe] detach=1 scopes=3 frows=1 attach=1 scopes_back=2 frows_back=2 slot='ramp'
[scope-probe] gl copy=ok(67f) empty=ok(39f) empty_bound=1
[scope-probe] gl detached ok=1 frames=42 rows=1 slot0='sine'
[scope-probe] rclick=1
[scope-probe] pin_mirror=1 main=[-1.000..1.000] scope=[-1.000..1.000] x=[-1.000..1.000]
``` ```
- `count=2` is both windows having been created and retained; `windows=1` each being a visible - `count=2` is both windows having been created and retained; `windows=1` each being a visible
top-level window. top-level window.
- `copied='sine'` is the copy's first slot holding the main window's figure-0 signal, and - `copied='sine'` is the copy's first non-empty slot holding the main window's figure-0 signal
(`'square'` when the enable-all probe ran first and cleared slot 0 first), and
`bound=1` the tick having resolved it to a live channel — the scope is bound to the same `bound=1` the tick having resolved it to a live channel — the scope is bound to the same
model, not a snapshot taken at creation. model, not a snapshot taken at creation.
- `empty_ok=1` is the `-1` call producing an empty row, which is the other entry point the - `empty_ok=1` is the `-1` call producing an empty row, which is the other entry point the
overflow menu uses. overflow menu uses.
- `loaded closed=N scopes=0` is the **session remembering windows without opening any**: the
entries a previous run saved are in `App::closed_windows`, and no scope window exists at start.
`scopes` above zero is a launch that opened windows on its own.
- `reopen scopes=… remembered=… left=0` is **close → remember → the menu entry**: destroying a
window puts its arrangement into `closed_windows` (by name -- `name=1` checks it), clicking the
*Reopen closed windows* item ("clicked", the item's own handler) opens every remembered one, and
the list is empty after (`left=0`). A `remembered` that did not grow is a destroy handler that
forgot to record; `left` non-zero is an entry the menu did not take.
- `instruments windows=N cursors=N gate=N trigger=N marks=N` is the **one owner, every window**:
given a cursor pair, a gate band and the trigger marker on the main window, every scope window
reports the same (each count is how many windows matched). `marks` is the per-window measurement
mark count agreeing with what that window plots -- a window that plots the measured signal has
its ruler, one that does not has none. All four counts equal to `windows` is the reading;
`trigger` is compared against the **main** (the marker is up only while the trigger is), and the
parenthesis reports the main's state so a zero is distinguishable from an unequal one. The leg
adds a real gated `Top` measurement on `sine`, because the gate is restated visible every frame
by `update_measures()` and `marks` only come from `show_on_plot` measurements.
- The second line is the **window as a drop target and a *Plot on* group**: `drop=1` is
`on_scope_drop_channel()` — the handler the window's own target wires — putting `square` in
the copied window's second slot (`slot1=`), the rebuild and the area's re-point then left to
the window's next frame; `page_groups=2` is the signal page of an unplotted signal carrying
**one *Plot on* group per window** (`udpscope-scope-plot-on`), so a signal can be plotted
into a scope window without dragging it there. `empty_drop=1 empty_slot0='square'` is the
same drop onto the **empty** window — the user's own path, "open a scope window and drop a
signal into it" — landing in its row set. `drop=0` names a refused drop (a full plot or an
unacquirable signal); `page_groups` less than the window count is a page rebuilt before the
windows were read or a group that lost its window.
- `pin_mirror=1` is a scope window **mirroring a pin as a range**: the probe pins the main window
and reads the scope's pin and its plot's x range a frame later. Both must agree and the x range
must be ordered (`x=[min..max]`, `min < max`). A `0` with an inverted scope pin (`[+pre, +post]`)
is the defect this exists for: `pin_event_window()` takes **spans**, and passing it a range's
signed `min` turned the window inside out, so a scope in Held or Paused mode drew no trace and no
x labels at all.
- `layout buttons=3 rows=1->2` is the window's **own layout editor**: the toggle builds
`2 × rows + 1` buttons (a `×` per row, a `+` per boundary, Done is not counted) for one figure,
and emitting the first boundary `+` adds a row. A wrong count is a button set built for the main
window's rows; `rows` unchanged is a handler writing the wrong row set.
- `rclick=1` is the scope window's **own right-click menu** being wired (one click gesture with
button 3 on its GL area, beside the drop target): the menu's actions act on that window's row
set.
- The fourth line is the windows' **GL account** (`gl copy=`/`gl empty=`): `ok(Nf)` is a context
that staged N frames, `err:` carries the adapter's failure message. A scope whose context
failed draws *nothing* — no axes, no traces, exactly the "blank plot widget" report — so
`err:` there is a driver/context finding, not a layout one, and `empty_bound=1` (the dropped
signal's channel, read a frame after the drop) with `ok(...)` together mean the window is
drawing; a blank window under those two readings is a compositor problem, not the app's.
## `[search-probe]` ## `[search-probe]`
+9 -3
View File
@@ -77,12 +77,18 @@
nothing is saved, nothing is dropped, and the rows' traces, colours and cursors come back nothing is saved, nothing is dropped, and the rows' traces, colours and cursors come back
where they were. The plot's right-click menu offers the same as <gui>Full screen this where they were. The plot's right-click menu offers the same as <gui>Full screen this
figure</gui> (and <gui>Restore the layout</gui> while one is enlarged). A layout of a single figure</gui> (and <gui>Restore the layout</gui> while one is enlarged). A layout of a single
figure has nothing to enlarge *to*, so it carries no button.</p></item> figure has nothing to enlarge *to*, so it carries no button.</p></item> Beside the toggle, shown the same way, a <gui>detach</gui> button moves the figure into a
window of its own — it leaves the main window's rows; the detached window carries an
<gui>attach</gui> button in the same corner, which returns the figure to the main window and
closes the window.
<item><p>A plot's right-click menu has <gui>Open this plot in a new window</gui>: a second <item><p>A plot's right-click menu has <gui>Open this plot in a new window</gui>: a second
window showing the same stream through a copy of that figure (its cells, their widths and window showing the same stream through a copy of that figure (its cells, their widths and
every trace), with its own axes following the main window's time window. The overflow menu's every trace), with its own axes following the main window's time window. The overflow menu's
<gui>New scope window</gui> opens the same kind of window with an empty figure. Closing one <gui>New scope window</gui> opens the same kind of window with an empty figure. A signal
closes that view only; the main window keeps acquiring.</p></item> badge dragged onto a window's plot lands in that window (on the plot under the pointer), and
the signal page's <gui>Plot on</gui> section lists every window — one named group per
window, one button per plot of it. Closing one closes that view only; the main window keeps
acquiring.</p></item>
</list> </list>
</section> </section>
+53 -3
View File
@@ -448,7 +448,8 @@ bool load_layout(const std::string& path, LayoutState& out) {
std::string line; std::string line;
// What the last `[[...]]` header opened, so keys land in the right record. // What the last `[[...]]` header opened, so keys land in the right record.
enum class Section { Top, Plot, Calibration, Unit, Color, Math, Measure } section = Section::Top; enum class Section { Top, Plot, Window, WindowPlot, Calibration, Unit, Color, Math, Measure } section =
Section::Top;
int plot_count = 0; int plot_count = 0;
bool plot_section_seen = false; bool plot_section_seen = false;
while (std::getline(in, line)) { while (std::getline(in, line)) {
@@ -459,6 +460,14 @@ bool load_layout(const std::string& path, LayoutState& out) {
section = Section::Plot; section = Section::Plot;
plot_section_seen = true; plot_section_seen = true;
st.rows.emplace_back(); st.rows.emplace_back();
} else if (t == "[[window]]") {
section = Section::Window;
st.windows.emplace_back();
} else if (t == "[[window.plot]]") {
// A window's own plot rows: the same shape as the main layout's `[[plot]]`, one level in.
if (st.windows.empty()) st.windows.emplace_back();
section = Section::WindowPlot;
st.windows.back().rows.emplace_back();
} else if (t == "[[calibration]]") { } else if (t == "[[calibration]]") {
section = Section::Calibration; section = Section::Calibration;
st.calibration.emplace_back(); st.calibration.emplace_back();
@@ -608,6 +617,35 @@ bool load_layout(const std::string& path, LayoutState& out) {
} catch (...) { } catch (...) {
} }
} }
} else if (section == Section::Window && !st.windows.empty()) {
if (key == "name") {
st.windows.back().name = strip_quote(value);
} else if (key == "detached") {
st.windows.back().detached = value == "true";
}
} else if ((section == Section::WindowPlot && !st.windows.empty() &&
!st.windows.back().rows.empty())) {
// The window's rows take exactly the main layout's keys; `rows` is reached through the
// window record so the parser stays one shape.
LayoutState::Row& row = st.windows.back().rows.back();
if (key == "signals") {
std::vector<std::string> names;
if (value.size() >= 2 && value.front() == '[' && value.back() == ']') {
parse_string_array(value.substr(1, value.size() - 2), names);
}
row.signals = names;
} else if (key == "weight") {
row.weight = std::strtod(value.c_str(), nullptr);
} else if (key == "row") {
try {
const int r = std::stoi(value);
if (r >= 0) row.row = r;
} catch (...) {
}
} else if (key == "width") {
const double w = std::strtod(value.c_str(), nullptr);
if (std::isfinite(w) && w > 0.0) row.width = w;
}
} else if (section == Section::Plot && !st.rows.empty()) { } else if (section == Section::Plot && !st.rows.empty()) {
if (key == "signals") { if (key == "signals") {
std::vector<std::string> names; std::vector<std::string> names;
@@ -802,8 +840,7 @@ bool save_layout(const std::string& path, const LayoutState& state) {
if (state.buffer_samples > 0) o << "buffer_samples = " << state.buffer_samples << "\n"; if (state.buffer_samples > 0) o << "buffer_samples = " << state.buffer_samples << "\n";
o << "relative_time = " << (state.relative_time ? "true" : "false") << "\n"; o << "relative_time = " << (state.relative_time ? "true" : "false") << "\n";
if (state.capture_packets > 0) o << "capture_packets = " << state.capture_packets << "\n"; if (state.capture_packets > 0) o << "capture_packets = " << state.capture_packets << "\n";
for (const auto& row : state.rows) { const auto write_row = [&](const LayoutState::Row& row) {
o << "\n[[plot]]\n";
// A row of -1 is a state built without the split (nothing in the app does that now); leaving the // A row of -1 is a state built without the split (nothing in the app does that now); leaving the
// key out keeps it readable as the one-cell-per-row file it is. // key out keeps it readable as the one-cell-per-row file it is.
if (row.row >= 0) o << "row = " << row.row << "\nwidth = " << row.width << "\n"; if (row.row >= 0) o << "row = " << row.row << "\nwidth = " << row.width << "\n";
@@ -813,6 +850,19 @@ bool save_layout(const std::string& path, const LayoutState& state) {
o << toml_escape(row.signals[i]); o << toml_escape(row.signals[i]);
} }
o << "]\n"; o << "]\n";
};
for (const auto& row : state.rows) {
o << "\n[[plot]]\n";
write_row(row);
}
// The extra windows: written so a closed one can be reopened, never opened by a launch.
for (const auto& window : state.windows) {
o << "\n[[window]]\nname = " << toml_escape(window.name)
<< "\ndetached = " << (window.detached ? "true" : "false") << "\n";
for (const auto& row : window.rows) {
o << "\n[[window.plot]]\n";
write_row(row);
}
} }
for (const auto& cal : state.calibration) { for (const auto& cal : state.calibration) {
o << "\n[[calibration]]\nname = " << toml_escape(cal.name) << "\ngain = " << cal.gain o << "\n[[calibration]]\nname = " << toml_escape(cal.name) << "\ngain = " << cal.gain
+13
View File
@@ -36,6 +36,19 @@ struct LayoutState {
double window_s = 0.0; ///< 0 keeps the default window double window_s = 0.0; ///< 0 keeps the default window
std::vector<Row> rows; std::vector<Row> rows;
/// An **extra window**: a scope or detached window, remembered with the session but never opened
/// by a launch. A window is a view a user closed, and bringing one back is a decision -- the
/// *Reopen closed windows* entry asks for it, so a start never conjures windows onto a desktop
/// that does not want them (a tiling WM would tile them straight away). `name` is the window's own
/// short name (`plot 2`, `scope 3`), `detached` whether its figure came from a detach, and `rows`
/// the same plot rows the main layout carries: signals, row weights and cell widths.
struct Window {
std::string name;
bool detached = false;
std::vector<Row> rows;
};
std::vector<Window> windows;
struct Calibration { struct Calibration {
std::string name; std::string name;
double gain = 1.0; double gain = 1.0;
+23
View File
@@ -945,6 +945,16 @@ void RowSet::place_cursors(Row& r, plot::CursorPlacement placement) {
} }
void RowSet::set_cursors(double a, double b) {
if (a == cursor_a_x_ && b == cursor_b_x_) return;
cursor_a_x_ = a;
cursor_b_x_ = b;
// `Free`: the pair is the owner's already-committed position (a gesture in another window, or
// this window's own last sync); snapping it here would fight the hand still moving it.
for (int i = 0; i < rows_; ++i) place_cursors(row(i), plot::CursorPlacement::Free);
place_crossings();
}
void RowSet::set_cursor_mode(CursorMode mode) { void RowSet::set_cursor_mode(CursorMode mode) {
if (mode == cursor_mode_) return; if (mode == cursor_mode_) return;
cursor_mode_ = mode; cursor_mode_ = mode;
@@ -1509,6 +1519,19 @@ void RowSet::pin_event_window(double pre_s, double post_s) {
figure_->set_x_link_range(x_group_, {-pre_s, post_s}); figure_->set_x_link_range(x_group_, {-pre_s, post_s});
} }
void RowSet::pin_display_range(double x0, double x1) {
if (!(x1 > x0)) return;
window_s_ = std::max(1.0e-3, x1 - x0);
pinned_ = true;
pinned_pre_ = -x0;
pinned_post_ = x1;
if (!figure_) return;
// The same two statements `pin_event_window()` makes, from the range instead of the spans: the
// group stops following the live edge and is placed on the event's own window.
figure_->set_x_link_follow(x_group_, plot::XFollow::Off);
figure_->set_x_link_range(x_group_, {x0, x1});
}
void RowSet::fit_all() { void RowSet::fit_all() {
for (int i = 0; i < rows_; ++i) { for (int i = 0; i < rows_; ++i) {
Row& r = row(i); Row& r = row(i);
+21
View File
@@ -355,6 +355,16 @@ class RowSet {
void place_crossings(); void place_crossings();
CursorMode cursor_mode() const noexcept { return cursor_mode_; } CursorMode cursor_mode() const noexcept { return cursor_mode_; }
/// Writes both vertical cursors at once: the two-time pair is one instrument with one owner
/// (the main window's row set), and a scope window is written from it each frame -- reading it
/// back from that window's own plots is what lets a drag *there* move the same pair.
void set_cursors(double a, double b);
/// The trigger marker's position and whether it is up: what a scope window is written from, the
/// same way it is written from the cursor pair.
double trigger_marker_x() const noexcept { return trigger_marker_x_; }
bool trigger_marker_visible() const noexcept { return trigger_marker_on_; }
/// The two vertical cursors' positions, in the **display** x the axis reads -- not stream /// The two vertical cursors' positions, in the **display** x the axis reads -- not stream
/// seconds, which they coincided with only while `display_epoch()` was always 0. A live view /// seconds, which they coincided with only while `display_epoch()` was always 0. A live view
/// is newest-relative now, so a cursor left at -1.5 s keeps meaning "1.5 s ago" and the data /// is newest-relative now, so a cursor left at -1.5 s keeps meaning "1.5 s ago" and the data
@@ -399,6 +409,10 @@ class RowSet {
/// its label stays empty. /// its label stays empty.
struct MeasureMark { struct MeasureMark {
int row = 0; int row = 0;
/// The signal the mark belongs to, so a *scope window* can find the row that plots it (its
/// row numbering is its own): a mark that never carried the name could only be placed in a
/// copy by index, which a window's own layout editor invalidates.
std::string signal;
bool level = false; bool level = false;
double value = 0.0; double value = 0.0;
plot::u32 color = 0; plot::u32 color = 0;
@@ -570,6 +584,13 @@ class RowSet {
/// 0, so nothing outside the pre+post window is visible. /// 0, so nothing outside the pre+post window is visible.
void pin_event_window(double pre_s, double post_s); void pin_event_window(double pre_s, double post_s);
/// The same pin, stated as the **range** a second view mirrors: the caller has the display
/// range (`x0 < x1`) and not the spans -- a scope window takes `pinned_range()` from the main
/// window and hands it on. `pin_event_window()` takes *spans*, and passing it a range's signed
/// `min` turned the pin inside out (a scope in Held mode pinned to `[+pre, +post]`: no trace
/// and no x labels). A degenerate or inverted range pins nothing.
void pin_display_range(double x0, double x1);
/// Fits every row's y axis to the data it holds, and puts x back on the whole time window -- /// Fits every row's y axis to the data it holds, and puts x back on the whole time window --
/// `fit_x()`'s x half exactly, so the two cannot drift apart. Like it, **this does not change /// `fit_x()`'s x half exactly, so the two cannot drift apart. Like it, **this does not change
/// the time window**: the y axes are fitted to their data, x is merely un-zoomed. /// the time window**: the y axes are fitted to their data, x is merely un-zoomed.
+898 -12
View File
File diff suppressed because it is too large. Load diff
+95 -1
View File
@@ -215,6 +215,40 @@ struct App {
GtkWindow* window = nullptr; GtkWindow* window = nullptr;
plot::gtk4::PlotArea* area = nullptr; plot::gtk4::PlotArea* area = nullptr;
GtkWidget* widget = nullptr; GtkWidget* widget = nullptr;
/// The window's short name, the one the signal page's *Plot on* groups carry ("plot 2" for a
/// copy, "scope 3" for an empty one): a button must say which window it plots into.
std::string name;
/// The drop target wired onto `widget`: the handler reads the *window* off it (each window's
/// target names its own scope), and the probe calls the handler with it directly.
GtkDropTarget* drop = nullptr;
/// This window came from a **detach** -- the figure left the main window to live here -- so it
/// carries the attach control and can be put back. A copied or empty window is a plain second
/// view, and has no attach control.
bool detached = false;
/// False while attaching: the window is closing *because* its figure went back to the main
/// window, so there is nothing to remember (remembering it would offer to reopen a window
/// whose figure is on screen again).
bool remember_on_close = true;
/// The overlay wrapping the GL widget: the corner controls (attach, the layout toggle, the
/// layout editor's own buttons) all live on it. Its motion controller keeps the pointer state
/// they are revealed by.
GtkWidget* overlay = nullptr;
/// Layout editing in this window: its own editor and toggle. A scope is a view with its own
/// arrangement, so it holds as many figure rows as it is given and edits them with the same
/// `+` / `×` / Done controls the main window's editor has -- its own, never the main window's.
bool layout_editing = false;
GtkWidget* layout_toggle = nullptr;
std::vector<GtkWidget*> layout_buttons;
GtkWidget* layout_done = nullptr;
int layout_buttons_rows = -1;
std::vector<GtkWidget*> attach_buttons;
double pointer_x = 0.0;
double pointer_y = 0.0;
bool pointer_valid = false;
/// Frames this window's area was watched for its first paint: a window whose GL context failed
/// or that never paints says so in its own title (and on stderr) instead of staying a blank
/// rectangle nobody can diagnose.
int paint_checks = 0;
/// The area's initial plot, owned here: `PlotArea::create()` wants a `Plot&` that /// The area's initial plot, owned here: `PlotArea::create()` wants a `Plot&` that
/// outlives the widget, and sharing the main window's placeholder would hand two areas /// outlives the widget, and sharing the main window's placeholder would hand two areas
/// one plot the moment the figure mode is ever left. /// one plot the moment the figure mode is ever left.
@@ -222,6 +256,14 @@ struct App {
std::unique_ptr<RowSet> rows; std::unique_ptr<RowSet> rows;
}; };
std::vector<std::unique_ptr<ScopeWindow>> scopes; std::vector<std::unique_ptr<ScopeWindow>> scopes;
/// The **closed** extra windows, as the session remembers them: a window's arrangement is kept
/// when it is closed (and across a restart), and the *Reopen closed windows* entry opens them --
/// a launch never does, because a window is a view the user closed on purpose.
std::vector<LayoutState::Window> closed_windows;
/// The menu entry that reopens them, so its label can carry the count.
GtkWidget* reopen_scopes_item = nullptr;
/// Every empty scope window is numbered from this, so two of them never share a name.
int scope_seq = 0;
/// On while the layout editor is up. Nothing else about the plot changes: the editor is /// On while the layout editor is up. Nothing else about the plot changes: the editor is
/// a set of overlay buttons, not a mode the figure knows about. /// a set of overlay buttons, not a mode the figure knows about.
@@ -235,6 +277,10 @@ struct App {
/// count changes (`maximize_buttons_rows` is the count they were built for). /// count changes (`maximize_buttons_rows` is the count they were built for).
std::vector<GtkWidget*> maximize_buttons; std::vector<GtkWidget*> maximize_buttons;
int maximize_buttons_rows = -1; int maximize_buttons_rows = -1;
/// The **detach** buttons: one per figure row beside its full-screen toggle, same reveal,
/// same rebuild cadence -- but only while the layout holds two or more figures (detaching the
/// only figure would leave the main window with nothing to draw) and never while maximized.
std::vector<GtkWidget*> detach_buttons;
/// The boundary a resize drag is on, and the weights it started from: a drag is *relative* to where it /// The boundary a resize drag is on, and the weights it started from: a drag is *relative* to where it
/// began, so the beginning has to be kept -- reading the weights while dragging would compound. /// began, so the beginning has to be kept -- reading the weights while dragging would compound.
int layout_drag_row = -1; int layout_drag_row = -1;
@@ -870,6 +916,7 @@ struct App {
int dbg_maximize_step = 0; int dbg_maximize_step = 0;
std::size_t dbg_maximize_single = 0; std::size_t dbg_maximize_single = 0;
int dbg_maximize_hidden = -1; int dbg_maximize_hidden = -1;
std::size_t dbg_detach_single = 0;
/// Which step of the scope-window probe runs next (`UDPSCOPE_PROBE_SCOPE_MS`). /// Which step of the scope-window probe runs next (`UDPSCOPE_PROBE_SCOPE_MS`).
int dbg_scope_step = 0; int dbg_scope_step = 0;
int cursors_pane_mode = -1; int cursors_pane_mode = -1;
@@ -1627,6 +1674,9 @@ struct ExportContext {
}; };
struct FixedRangeContext { struct FixedRangeContext {
App* app; App* app;
/// The row set the range applies to: `nullptr` is the main window's. A scope window's own
/// dialog writes its window's rows.
RowSet* rows = nullptr;
int row; int row;
GtkWidget* lo; GtkWidget* lo;
GtkWidget* hi; GtkWidget* hi;
@@ -2038,6 +2088,41 @@ void row_menu_fill(RowMenuState& st);
GtkWidget* open_row_menu(App& app, int row, double x, double y, GtkWidget* parent, bool autohide = true); GtkWidget* open_row_menu(App& app, int row, double x, double y, GtkWidget* parent, bool autohide = true);
void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer user); void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer user);
gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y, gpointer user); gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y, gpointer user);
/// The same drop, onto a **scope window's** plot: the target carries the window, the row is the
/// window's own cell under the pointer, and the assignment lands in that window's row set.
gboolean on_scope_drop_channel(GtkDropTarget*, const GValue* value, double x, double y, gpointer user);
/// Plots `signal` on `row` of a scope window's row set (the signal page's *Plot on* buttons and the
/// drop target both end here). False, with a notice, when the signal cannot be acquired or the plot
/// holds four signals already.
bool assign_signal_to_scope(App& app, App::ScopeWindow& scope, std::size_t signal, int row);
/// Runs one keyboard accelerator. Scope windows carry the same shortcuts as the main window: the
/// view-level ones (`L` -- the layout editor) act on the window the key arrived in; the rest reach
/// the application through `run_app_action()`.
gboolean on_scope_key(GtkEventControllerKey* controller, guint keyval, guint keycode,
GdkModifierType state, gpointer user);
/// The session record of a window: its name, whether it was detached, and its plot rows.
LayoutState::Window scope_window_state(const App::ScopeWindow& scope);
/// Remembers a window as closed (`closed_windows`, replaced by name) -- what its destroy handler
/// does, and what keeps a closed window reopenable.
void remember_closed_window(App& app, const App::ScopeWindow& scope);
/// Opens a window from a remembered state: an empty scope window, then its rows, weights and
/// signals. Returns the opened window (nullptr when it could not be made).
App::ScopeWindow* open_scope_window_from_state(App& app, const LayoutState::Window& state);
/// Writes the main window's instruments -- the two cursors, the gate band, the trigger marker --
/// into every scope window, and adopts one dragged *there* back into the main window first. Called
/// once per frame from the main tick, after the main's own `sync_cursors()`/`sync_gate()`.
void sync_scope_instruments(App& app);
/// The main window's measurement marks, translated to each scope window's own rows (by signal
/// name) and placed there: a window that plots a measured signal draws its mark.
void mirror_measure_marks(App& app);
/// Refreshes the *Reopen closed windows* entry: its count and whether it is sensitive.
void sync_reopen_scopes_item(App& app);
/// Turns a scope window's own layout editor on or off, building or taking down its buttons.
void set_scope_layout_editing(App& app, App::ScopeWindow& scope, bool on);
/// The scope window's own right-click menu: the same row menu's actions, aimed at **that
/// window's** row set.
void on_scope_right_click(GtkGestureClick* gesture, int n_press, double x, double y,
gpointer user);
/// Reads each pane's own switch back off the header bar's button, and the other way round: the two are /// Reads each pane's own switch back off the header bar's button, and the other way round: the two are
/// one control in two places, and `panel_switch_sync` keeps the write from looking like a click. /// one control in two places, and `panel_switch_sync` keeps the write from looking like a click.
void sync_panel_switches(App& app); void sync_panel_switches(App& app);
@@ -2146,7 +2231,16 @@ gboolean scope_probe_step(gpointer user);
/// A second top-level window showing the same stream through its own figure: `figure_row` >= 0 /// A second top-level window showing the same stream through its own figure: `figure_row` >= 0
/// copies that figure of the main window (cells, widths, every slot's signal), -1 opens an /// copies that figure of the main window (cells, widths, every slot's signal), -1 opens an
/// empty one-row scope. The main window's tick binds, acquires and repaints it. /// empty one-row scope. The main window's tick binds, acquires and repaints it.
void open_scope_window(App& app, int figure_row); void open_scope_window(App& app, int figure_row, bool detached = false);
/// Detaches figure row `fr` into a window of its own: the figure is copied into a new scope
/// window -- which is what makes the window *detached*, the one kind with an attach control --
/// and removed from the main window's rows. Refused, changing nothing, for a one-figure layout
/// or a maximized one: detaching then would leave the main window with nothing to draw.
void detach_figure(App& app, int fr);
/// Puts a detached window's figure back: a figure row is appended to the main window carrying
/// the window's arrangement (cells, weights, every slot's signal in its colour) and the window
/// closes. The window must be a detached one; anything else returns without changing.
void attach_scope_window(App& app, App::ScopeWindow& scope);
gboolean measurepin_probe(gpointer user); gboolean measurepin_probe(gpointer user);
gboolean measurepin_held_wait(gpointer user); gboolean measurepin_held_wait(gpointer user);
gboolean measurepin_pause_wait(gpointer user); gboolean measurepin_pause_wait(gpointer user);
+50
View File
@@ -14,6 +14,46 @@ namespace ui_impl {
/// Reads the session layout back out of the model and rows: the display window, /// Reads the session layout back out of the model and rows: the display window,
/// each row's slot signal names, and each signal's calibration. /// each row's slot signal names, and each signal's calibration.
LayoutState::Window scope_window_state(const App::ScopeWindow& scope) {
LayoutState::Window w;
w.name = scope.name;
w.detached = scope.detached;
if (scope.rows == nullptr) return w;
for (int i = 0; i < scope.rows->row_count(); ++i) {
LayoutState::Row row;
const RowSet::Row& r = scope.rows->row(i);
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
const std::size_t si = static_cast<std::size_t>(s);
row.signals.push_back(r.layer[si] > 0 ? std::string() : r.signal_name[si]);
}
const int fr = scope.rows->figure_row_of(i);
const std::vector<float>& weights = scope.rows->row_weights();
row.row = fr;
if (fr >= 0 && static_cast<std::size_t>(fr) < weights.size()) {
row.weight = weights[static_cast<std::size_t>(fr)];
}
const std::vector<float>& cw = scope.rows->cell_weights(fr);
const int cell = scope.rows->cell_of(i);
if (cell >= 0 && static_cast<std::size_t>(cell) < cw.size()) row.width = cw[static_cast<std::size_t>(cell)];
w.rows.push_back(std::move(row));
}
return w;
}
void remember_closed_window(App& app, const App::ScopeWindow& scope) {
LayoutState::Window w = scope_window_state(scope);
for (LayoutState::Window& known : app.closed_windows) {
if (known.name != w.name) continue;
known = std::move(w);
sync_reopen_scopes_item(app);
schedule_layout_save(app);
return;
}
app.closed_windows.push_back(std::move(w));
sync_reopen_scopes_item(app);
schedule_layout_save(app);
}
LayoutState collect_layout(App& app) { LayoutState collect_layout(App& app) {
LayoutState st; LayoutState st;
st.window_s = app.rows->time_window(); st.window_s = app.rows->time_window();
@@ -64,6 +104,12 @@ LayoutState collect_layout(App& app) {
if (model.signals()[si].math || !on[si]) continue; if (model.signals()[si].math || !on[si]) continue;
st.enabled.push_back(model.signals()[si].name); st.enabled.push_back(model.signals()[si].name);
} }
// The extra windows: the **open** ones by their present arrangement, then the closed ones as
// they were remembered. A launch never opens them; the *Reopen closed windows* entry does.
for (const std::unique_ptr<App::ScopeWindow>& scope : app.scopes) {
st.windows.push_back(scope_window_state(*scope));
}
for (const LayoutState::Window& w : app.closed_windows) st.windows.push_back(w);
// Only the signals the user actually re-coloured: the rest take the positional palette, and // Only the signals the user actually re-coloured: the rest take the positional palette, and
// writing those out would freeze a colour that is meant to follow the signal list. // writing those out would freeze a colour that is meant to follow the signal list.
for (const auto& [name, rgb] : app.rows->signal_colors()) { for (const auto& [name, rgb] : app.rows->signal_colors()) {
@@ -437,6 +483,10 @@ void apply_layout(App& app, const LayoutState& st) {
break; break;
} }
} }
// The extra windows are **remembered, not opened**: a session's windows are views the user
// closed, and a launch that tiled them onto the desktop would take that decision for them.
app.closed_windows = st.windows;
sync_reopen_scopes_item(app);
app.measures = st.measures; app.measures = st.measures;
app.measure_stats_on = st.measure_stats; app.measure_stats_on = st.measure_stats;
app.updating_panels = true; app.updating_panels = true;
+2
View File
@@ -296,6 +296,7 @@ void place_measure_marks(App& app) {
} }
} }
app.rows->set_measure_marks(app.measure_marks_display); app.rows->set_measure_marks(app.measure_marks_display);
mirror_measure_marks(app);
} }
/// Recomputes every measurement over the window it describes and writes the results into the /// Recomputes every measurement over the window it describes and writes the results into the
@@ -734,6 +735,7 @@ void update_measures(App& app, bool force, bool accumulate) {
// `MeasureMarkSpec` -- the badge text is built where the display conversion happens). // `MeasureMarkSpec` -- the badge text is built where the display conversion happens).
RowSet::MeasureMark mark; RowSet::MeasureMark mark;
mark.row = row; mark.row = row;
mark.signal = m.signal;
mark.level = spec.level; mark.level = spec.level;
mark.value = spec.level ? spec.value : spec.value + s0; mark.value = spec.level ? spec.value : spec.value + s0;
mark.color = color; mark.color = color;
+3
View File
@@ -287,6 +287,9 @@ void open_preferences(App& app) {
// The crosshair's axis badges, at once like the theme: it is a drawing flag on the // The crosshair's axis badges, at once like the theme: it is a drawing flag on the
// plots, and a change is the next frame's drawing. // plots, and a change is the next frame's drawing.
app.rows->set_crosshair_axis_labels(s.crosshair_axis_labels); app.rows->set_crosshair_axis_labels(s.crosshair_axis_labels);
for (const std::unique_ptr<App::ScopeWindow>& scope : app.scopes) {
if (scope->rows != nullptr) scope->rows->set_crosshair_axis_labels(s.crosshair_axis_labels);
}
// Only a failure is announced. A change arrives once per keystroke in the directory row, and a // Only a failure is announced. A change arrives once per keystroke in the directory row, and a
// status line that flashed on each one would be the app shouting about its own bookkeeping. // status line that flashed on each one would be the app shouting about its own bookkeeping.
if (!written) set_notice(app, "could not write the preferences file", 4); if (!written) set_notice(app, "could not write the preferences file", 4);
+319 -17
View File
@@ -1651,6 +1651,7 @@ gboolean maximize_probe_step(gpointer user) {
// A layout of one figure carries no full-screen toggle (there is nothing to enlarge // A layout of one figure carries no full-screen toggle (there is nothing to enlarge
// *to*); the button appears together with the second figure row, below. // *to*); the button appears together with the second figure row, below.
a->dbg_maximize_single = a->maximize_buttons.size(); a->dbg_maximize_single = a->maximize_buttons.size();
a->dbg_detach_single = a->detach_buttons.size();
a->rows->set_cells_per_row({1, 1}); a->rows->set_cells_per_row({1, 1});
a->rows->assign(0, 0, sine, sine, sine_color); a->rows->assign(0, 0, sine, sine, sine_color);
a->rows->assign(1, 0, square, square, square_color); a->rows->assign(1, 0, square, square, square_color);
@@ -1684,11 +1685,13 @@ gboolean maximize_probe_step(gpointer user) {
const std::string row1_back = a->rows->row(1).signal_name[0]; const std::string row1_back = a->rows->row(1).signal_name[0];
const bool flag_back = a->rows->figures_maximized(); const bool flag_back = a->rows->figures_maximized();
std::fprintf(stderr, std::fprintf(stderr,
"[maximize-probe] single_buttons=%zu before: rows=%d frows=%d row0=%s row1=%s " "[maximize-probe] single_buttons=%zu detach_single=%zu before: rows=%d frows=%d "
"buttons=%zu; max: rows=%d frows=%d flag=%d row0=%s; " "row0=%s row1=%s buttons=%zu detach=%zu; max: rows=%d frows=%d flag=%d row0=%s; "
"back: rows=%d row0=%s row1=%s flag=%d\n", "back: rows=%d row0=%s row1=%s flag=%d\n",
a->dbg_maximize_single, rows_before, frows_before, row0_before.c_str(), a->dbg_maximize_single, a->dbg_detach_single, rows_before, frows_before,
row1_before.c_str(), buttons, rows_max, frows_max, flag_max ? 1 : 0, row0_before.c_str(),
row1_before.c_str(), buttons, a->detach_buttons.size(), rows_max, frows_max,
flag_max ? 1 : 0,
row0_max.c_str(), rows_back, row0_back.c_str(), row1_back.c_str(), row0_max.c_str(), rows_back, row0_back.c_str(), row1_back.c_str(),
flag_back ? 1 : 0); flag_back ? 1 : 0);
std::fflush(stderr); std::fflush(stderr);
@@ -1703,25 +1706,37 @@ gboolean maximize_probe_step(gpointer user) {
case 2: { case 2: {
a->dbg_maximize_hidden = a->dbg_maximize_hidden =
a->maximize_buttons.empty() || !gtk_widget_get_visible(a->maximize_buttons[0]) ? 1 : 0; a->maximize_buttons.empty() || !gtk_widget_get_visible(a->maximize_buttons[0]) ? 1 : 0;
// On the **detach** button's own centre -- its zone is the strictest of the corner pair, so a
// pointer there must reveal both it and the full-screen toggle beside it.
if (const plot::Plot* last = a->rows->row(0).plot) {
const double scale =
a->area != nullptr && a->area->device_scale() > 0.0f
? static_cast<double>(a->area->device_scale())
: 1.0;
const double right = static_cast<double>(last->layout().axes.right()) / scale;
a->area_pointer_x = right - 2 * kLayoutButtonPx - 2 * kLayoutButtonPad +
0.5 * kLayoutButtonPx;
a->area_pointer_y = kLayoutButtonPad + 0.5 * kLayoutButtonPx;
} else {
a->area_pointer_x = static_cast<double>(gtk_widget_get_width(a->area_widget)) - 8.0; a->area_pointer_x = static_cast<double>(gtk_widget_get_width(a->area_widget)) - 8.0;
a->area_pointer_y = 8.0; a->area_pointer_y = 8.0;
}
a->area_pointer_valid = true; a->area_pointer_valid = true;
a->dbg_maximize_step = 3; a->dbg_maximize_step = 3;
g_timeout_add(250, maximize_probe_step, a); g_timeout_add(250, maximize_probe_step, a);
break; break;
} }
case 3: { case 3: {
const int shown = !a->maximize_buttons.empty() && const auto visible = [](const std::vector<GtkWidget*>& buttons, std::size_t i) {
gtk_widget_get_visible(a->maximize_buttons[0]) return i < buttons.size() && gtk_widget_get_visible(buttons[i]) ? 1 : 0;
? 1 };
: 0; const int shown = visible(a->maximize_buttons, 0);
const int other = a->maximize_buttons.size() > 1 && const int other = visible(a->maximize_buttons, 1);
gtk_widget_get_visible(a->maximize_buttons[1]) const int dshown = visible(a->detach_buttons, 0);
? 1 const int dother = visible(a->detach_buttons, 1);
: 0;
std::fprintf(stderr, "[maximize-probe] corner: hidden_when_off=%d shown_when_near=%d " std::fprintf(stderr, "[maximize-probe] corner: hidden_when_off=%d shown_when_near=%d "
"other_row=%d\n", "other_row=%d detach_near=%d detach_other=%d\n",
a->dbg_maximize_hidden, shown, other); a->dbg_maximize_hidden, shown, other, dshown, dother);
std::fflush(stderr); std::fflush(stderr);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
@@ -1736,11 +1751,29 @@ gboolean maximize_probe_step(gpointer user) {
/// bound to the same stream a few frames later, while the empty one stays empty. The window /// bound to the same stream a few frames later, while the empty one stays empty. The window
/// creation itself (a real `GtkWindow`, an own `PlotArea`, its own `RowSet`) is exercised by /// creation itself (a real `GtkWindow`, an own `PlotArea`, its own `RowSet`) is exercised by
/// the same call the two menu entries make. /// the same call the two menu entries make.
/// How many descendants carry `name` -- the scope window's *Plot on* groups are counted, one per
/// window, where a single `find_named` would stop at the first.
int count_named(GtkWidget* root, const char* name) {
int n = 0;
if (root == nullptr) return 0;
if (strcmp(gtk_widget_get_name(root), name) == 0) ++n;
for (GtkWidget* c = gtk_widget_get_first_child(root); c != nullptr;
c = gtk_widget_get_next_sibling(c)) {
n += count_named(c, name);
}
return n;
}
gboolean scope_probe_step(gpointer user) { gboolean scope_probe_step(gpointer user) {
App* a = static_cast<App*>(user); App* a = static_cast<App*>(user);
if (a->window == nullptr) return G_SOURCE_REMOVE; if (a->window == nullptr) return G_SOURCE_REMOVE;
switch (a->dbg_scope_step) { switch (a->dbg_scope_step) {
case 0: { case 0: {
// What the session remembered, and what a launch opened: the windows are never opened at
// start, so `scopes` is one (the main window) and `closed` is the count read from the file.
std::fprintf(stderr, "[scope-probe] loaded closed=%zu scopes=%zu\n",
a->closed_windows.size(), a->scopes.size());
std::fflush(stderr);
const std::vector<SignalInfo>& signals = a->model->signals(); const std::vector<SignalInfo>& signals = a->model->signals();
for (std::size_t i = 0; i < signals.size(); ++i) { for (std::size_t i = 0; i < signals.size(); ++i) {
if (signals[i].name == "sine") a->model->set_signal_enabled(i, true); if (signals[i].name == "sine") a->model->set_signal_enabled(i, true);
@@ -1777,15 +1810,284 @@ gboolean scope_probe_step(gpointer user) {
} }
const App::ScopeWindow& copy = *a->scopes[0]; const App::ScopeWindow& copy = *a->scopes[0];
const App::ScopeWindow& empty = *a->scopes[1]; const App::ScopeWindow& empty = *a->scopes[1];
const std::string copied = copy.rows->row(0).signal_name[0]; // The first non-empty slot of the copied row: the copy is slot-for-slot faithful, and slot 0
const bool copied_bound = copy.rows->row(0).channel[0] >= 0; // is empty whenever an earlier probe (the enable-all one disables a signal at its end) left
const bool empty_ok = empty.rows->row(0).signal_name[0].empty(); // the main's slot 0 empty -- so this reads *a* copied trace without assuming which slot.
std::string copied;
bool copied_bound = false;
for (int sl = 0; sl < RowSet::kSlotsPerRow; ++sl) {
const std::size_t si = static_cast<std::size_t>(sl);
if (copy.rows->row(0).signal_name[si].empty()) continue;
copied = copy.rows->row(0).signal_name[si];
copied_bound = copy.rows->row(0).channel[si] >= 0;
break;
}
bool empty_ok = true;
for (int sl = 0; sl < RowSet::kSlotsPerRow; ++sl) {
if (!empty.rows->row(0).signal_name[static_cast<std::size_t>(sl)].empty()) empty_ok = false;
}
const bool windows = copy.window != nullptr && empty.window != nullptr && const bool windows = copy.window != nullptr && empty.window != nullptr &&
gtk_widget_get_visible(GTK_WIDGET(copy.window)) && gtk_widget_get_visible(GTK_WIDGET(copy.window)) &&
gtk_widget_get_visible(GTK_WIDGET(empty.window)); gtk_widget_get_visible(GTK_WIDGET(empty.window));
std::fprintf(stderr, std::fprintf(stderr,
"[scope-probe] count=2 copied='%s' bound=%d empty_ok=%d windows=%d\n", "[scope-probe] count=2 copied='%s' bound=%d empty_ok=%d windows=%d\n",
copied.c_str(), copied_bound ? 1 : 0, empty_ok ? 1 : 0, windows ? 1 : 0); copied.c_str(), copied_bound ? 1 : 0, empty_ok ? 1 : 0, windows ? 1 : 0);
// **A drop onto a window**: the handler the drop target wires, called directly with a
// cell-0 coordinate. The copied window holds 'sine' in slot 0, so the dropped signal must
// land in its second slot (the rebuild and the area's re-point are the window's next frame).
std::size_t square = std::size_t(-1);
for (std::size_t i = 0; i < a->model->signals().size(); ++i) {
if (a->model->signals()[i].name == "square") square = i;
}
GValue payload = G_VALUE_INIT;
g_value_init(&payload, G_TYPE_STRING);
g_value_set_string(&payload, std::to_string(square).c_str());
const gboolean dropped = square != std::size_t(-1) && copy.drop != nullptr
? on_scope_drop_channel(copy.drop, &payload, 40.0, 20.0, a)
: FALSE;
g_value_unset(&payload);
const std::string slot1 = copy.rows->row(0).signal_name[1];
// The same drop onto the **empty** window: its figure must gain the trace and, after the
// window's next frame, draw it (the screenshots a run of this probe takes read it back).
GValue payload2 = G_VALUE_INIT;
g_value_init(&payload2, G_TYPE_STRING);
g_value_set_string(&payload2, std::to_string(square).c_str());
const gboolean dropped2 = square != std::size_t(-1) && empty.drop != nullptr
? on_scope_drop_channel(empty.drop, &payload2, 40.0, 20.0, a)
: FALSE;
g_value_unset(&payload2);
const std::string empty_slot0 = empty.rows->row(0).signal_name[0];
// **The signal page lists the windows**: one *Plot on* group per window, in the page of a
// signal that is plotted nowhere (so every button is live).
signal_page_ensure(*a, "ramp");
signal_pages_sync(*a);
int groups = 0;
const auto page_it = a->signal_pages.find("ramp");
if (page_it != a->signal_pages.end()) groups = count_named(page_it->second, "udpscope-scope-plot-on");
std::fprintf(stderr,
"[scope-probe] drop=%d slot1='%s' page_groups=%d empty_drop=%d "
"empty_slot0='%s'\n",
dropped ? 1 : 0, slot1.c_str(), groups, dropped2 ? 1 : 0,
empty_slot0.c_str());
// **Detach and attach**: a second figure row detaches -- the window is marked detached and
// carries the attach control -- and its figure comes back on the main window.
a->rows->set_cells_per_row({1, 1});
{
std::size_t ramp = std::size_t(-1);
int color = 0;
for (std::size_t i = 0; i < a->model->signals().size(); ++i) {
if (a->model->signals()[i].name == "ramp") {
ramp = i;
color = signal_color_index(a->model->signals(), i);
}
}
if (ramp != std::size_t(-1)) a->rows->assign(1, 0, "ramp", "ramp", color);
}
a->relayout_pending = true;
detach_figure(*a, 1);
const bool detached_ok = a->scopes.size() == 3 && a->scopes[2]->detached &&
a->rows->figure_row_count() == 1 &&
a->scopes[2]->rows->row(0).signal_name[0] == "ramp" &&
a->scopes[2]->attach_buttons.size() == 1;
attach_scope_window(*a, *a->scopes[2]);
const int frows_back = a->rows->figure_row_count();
const bool attach_ok = a->scopes.size() == 2 && frows_back == 2 &&
a->rows->row(a->rows->first_plot_of_row(1)).signal_name[0] == "ramp";
std::fprintf(stderr,
"[scope-probe] detach=%d scopes=3 frows=1 attach=%d scopes_back=2 "
"frows_back=%d slot='ramp'\n",
detached_ok ? 1 : 0, attach_ok ? 1 : 0, frows_back);
std::fflush(stderr);
// And a detached window **left open**, so the screenshots a run of this probe takes can
// answer "does the detached figure draw": figure 0 (sine) moves out and stays out.
detach_figure(*a, 0);
a->dbg_scope_step = 3;
g_timeout_add(800, scope_probe_step, a);
return G_SOURCE_REMOVE;
}
case 3: {
// The windows' **own GL account**: a scope whose context failed -- which a second GL area
// on a real driver can hit where the software renderer does not -- draws *nothing*: no
// axes, no traces, exactly the "blank plot widget" report. `frames=` counts staged frames;
// `ok=0` carries the adapter's message.
const auto gl_state = [](const App::ScopeWindow& s) {
if (s.area == nullptr) return std::string("no-area");
const plot::Status st = s.area->init_status();
return st ? "ok(" + std::to_string(s.area->frames_painted()) + "f)"
: std::string("err: ") + st.message;
};
const bool empty_bound = a->scopes[1]->rows->row(0).channel[0] >= 0;
std::fprintf(stderr, "[scope-probe] gl copy=%s empty=%s empty_bound=%d\n",
gl_state(*a->scopes[0]).c_str(), gl_state(*a->scopes[1]).c_str(),
empty_bound ? 1 : 0);
if (!a->scopes.empty()) {
const App::ScopeWindow& det = *a->scopes.back();
const plot::Status st = det.area != nullptr ? det.area->init_status() : plot::Status{};
std::fprintf(stderr,
"[scope-probe] gl detached ok=%d frames=%llu rows=%d slot0='%s'\n",
(bool)st ? 1 : 0,
static_cast<unsigned long long>(det.area != nullptr
? det.area->frames_painted()
: 0),
det.rows != nullptr ? det.rows->row_count() : 0,
det.rows != nullptr && det.rows->row_count() > 0
? det.rows->row(0).signal_name[0].c_str()
: "");
}
// The scope window carries its own right-click menu (its gesture is on the GL area, wired
// beside the drop target): one click gesture whose button is 3, per window.
int right_clicks = 0;
GtkWidget* det_widget = a->scopes.empty() ? nullptr : a->scopes.back()->widget;
if (det_widget != nullptr) {
GListModel* controllers = gtk_widget_observe_controllers(det_widget);
const guint n = g_list_model_get_n_items(G_LIST_MODEL(controllers));
for (guint i = 0; i < n; ++i) {
GObject* c = G_OBJECT(g_list_model_get_item(controllers, i));
if (GTK_IS_GESTURE_SINGLE(c) &&
gtk_gesture_single_get_button(GTK_GESTURE_SINGLE(c)) == 3) {
++right_clicks;
}
g_object_unref(c);
}
g_object_unref(controllers);
}
std::fprintf(stderr, "[scope-probe] rclick=%d\n", right_clicks);
std::fflush(stderr);
// **The pin mirror**: a held event pins the main window, and every scope must be pinned to
// the *same range*. `pin_event_window()` takes spans (pre, post) and passing it a range's
// signed min turned the pin inside out (a scope in Held drew no trace and no x labels).
a->rows->pin_event_window(1.6, 0.4);
a->dbg_scope_step = 4;
g_timeout_add(350, scope_probe_step, a);
return G_SOURCE_REMOVE;
}
case 4: {
const plot::Range main_pin = a->rows->pinned_range();
const plot::Range scope_pin = a->scopes[0]->rows->pinned_range();
const plot::Range scope_x = a->scopes[0]->rows->row(0).plot != nullptr
? a->scopes[0]->rows->row(0).plot->x_range(0)
: plot::Range{};
const bool pinned = a->scopes[0]->rows->window_pinned() == a->rows->window_pinned();
const bool same = std::fabs(scope_pin.min - main_pin.min) < 1e-9 &&
std::fabs(scope_pin.max - main_pin.max) < 1e-9 &&
scope_x.max > scope_x.min;
std::fprintf(stderr,
"[scope-probe] pin_mirror=%d main=[%.3f..%.3f] scope=[%.3f..%.3f] "
"x=[%.3f..%.3f]\n",
pinned && same ? 1 : 0, main_pin.min, main_pin.max, scope_pin.min,
scope_pin.max, scope_x.min, scope_x.max);
std::fflush(stderr);
a->rows->set_time_window(2.0); // unpin: leave the window as the run found it
// **The scope window's own layout editor**: the toggle builds its buttons and a `+` adds a
// figure row -- the arrangement of a view is the view's.
set_scope_layout_editing(*a, *a->scopes[1], true);
const std::size_t layout_buttons = a->scopes[1]->layout_buttons.size();
const int rows_before = a->scopes[1]->rows->row_count();
if (layout_buttons > static_cast<std::size_t>(rows_before)) {
g_signal_emit_by_name(a->scopes[1]->layout_buttons[static_cast<std::size_t>(rows_before)],
"clicked");
}
const int rows_after = a->scopes[1]->rows->row_count();
set_scope_layout_editing(*a, *a->scopes[1], false);
std::fprintf(stderr, "[scope-probe] layout buttons=%zu rows=%d->%d\n", layout_buttons,
rows_before, rows_after);
// **Closing remembers, the menu reopens.** A closed window's arrangement stays in the
// session (`closed_windows`), a launch never opens it, and the *Reopen closed windows* entry
// brings it back -- so this leg closes the empty window, reads what was remembered, and
// clicks the entry.
const std::string closed_name = a->scopes[1]->name;
const std::size_t scopes_before = a->scopes.size();
gtk_window_destroy(GTK_WINDOW(a->scopes[1]->window));
const std::size_t remembered = a->closed_windows.size();
const bool remembered_name = remembered > 0 && a->closed_windows.back().name == closed_name &&
!a->closed_windows.back().rows.empty();
const std::size_t scopes_after_close = a->scopes.size();
if (a->reopen_scopes_item != nullptr) {
g_signal_emit_by_name(a->reopen_scopes_item, "clicked");
}
const std::size_t scopes_after = a->scopes.size();
const int back_rows = scopes_after > scopes_after_close ? a->scopes.back()->rows->row_count() : 0;
const std::string back_slot =
scopes_after > scopes_after_close && a->scopes.back()->rows->row_count() > 0
? a->scopes.back()->rows->row(0).signal_name[0]
: std::string();
std::fprintf(stderr,
"[scope-probe] reopen scopes=%zu->%zu(closed)->%zu remembered=%zu name=%d "
"rows=%d slot0='%s' left=%zu\n",
scopes_before, scopes_after_close, scopes_after, remembered,
remembered_name ? 1 : 0, back_rows, back_slot.c_str(),
a->closed_windows.size());
std::fflush(stderr);
// **The instruments are on every window**: the cursors, the gate band and the trigger
// marker are written from the main window's one owner (the next tick's
// `sync_scope_instruments()`), so the states below are asked for on the main and read on
// the scope.
a->rows->set_cursors_visible(true);
a->rows->set_cursor_mode(RowSet::CursorMode::Both);
a->rows->set_cursors(-1.0, 0.0);
a->rows->set_gate(-1.2, -0.8);
a->rows->set_gate_visible(true);
a->rows->set_trigger_marker(0.0, true);
// A real measurement on 'sine' with `show_on_plot`: the marks path is the one a user's
// measurement takes (`update_measures()` -> `place_measure_marks()` -> the windows), so a
// window that plots 'sine' must draw its ruler and one that does not must draw none.
{
Measure m;
m.signal = "sine";
m.type = MeasureType::Top;
m.options.show_on_plot = true;
// Gated: `update_measures()` restates the gate's visibility every frame (it is up while
// *any* measurement is gated), so the band is genuinely on and its bounds are the ones
// set above rather than a value the mirror would be right to hide.
m.options.gate = true;
a->measures.push_back(std::move(m));
rebuild_measures_pane(*a);
}
a->dbg_scope_step = 5;
g_timeout_add(700, scope_probe_step, a);
return G_SOURCE_REMOVE;
}
case 5: {
// A frame has passed: the tick wrote the main's instruments into every window.
int cursor_ok = 0;
int gate_ok = 0;
int trigger_ok = 0;
int marks_ok = 0;
for (const std::unique_ptr<App::ScopeWindow>& scope : a->scopes) {
if (scope->rows == nullptr) continue;
const bool cursors = scope->rows->cursors_visible() &&
scope->rows->cursor_mode() == RowSet::CursorMode::Both &&
std::fabs(scope->rows->cursor_a_x() + 1.0) < 1e-9 &&
std::fabs(scope->rows->cursor_b_x()) < 1e-9;
const bool gate = scope->rows->gate_visible() &&
std::fabs(scope->rows->gate_x0() + 1.2) < 1e-9 &&
std::fabs(scope->rows->gate_x1() + 0.8) < 1e-9;
const bool trigger =
scope->rows->trigger_marker_visible() == a->rows->trigger_marker_visible() &&
std::fabs(scope->rows->trigger_marker_x() - a->rows->trigger_marker_x()) < 1e-9;
// The mark: the window that plots 'sine' has it, one that does not has none. Which is
// which is read off the same names.
bool plots_sine = false;
for (int r = 0; r < scope->rows->row_count(); ++r) {
for (int sl = 0; sl < RowSet::kSlotsPerRow; ++sl) {
if (scope->rows->row(r).signal_name[static_cast<std::size_t>(sl)] == "sine") {
plots_sine = true;
}
}
}
const int placed = scope->rows->measure_marks_placed();
const bool mark = plots_sine ? placed == 1 : placed == 0;
cursor_ok += cursors ? 1 : 0;
gate_ok += gate ? 1 : 0;
trigger_ok += trigger ? 1 : 0;
marks_ok += mark ? 1 : 0;
}
const int n = static_cast<int>(a->scopes.size());
std::fprintf(stderr,
"[scope-probe] instruments windows=%d cursors=%d gate=%d trigger=%d "
"marks=%d (main trigger on=%d x=%.3f)\n",
n, cursor_ok, gate_ok, trigger_ok, marks_ok,
a->rows->trigger_marker_visible() ? 1 : 0, a->rows->trigger_marker_x());
std::fflush(stderr); std::fflush(stderr);
return G_SOURCE_REMOVE; return G_SOURCE_REMOVE;
} }
+42
View File
@@ -288,6 +288,48 @@ void fill(App& app, const std::string& name, GtkWidget* page) {
&app); &app);
} }
gtk_box_append(GTK_BOX(page), rows_box); gtk_box_append(GTK_BOX(page), rows_box);
// The scope windows: each is a view with its own arrangement, and a signal is plotted there
// exactly as it is here -- one group per window (named, so two windows never blur into one),
// one button per plot of that window's figure. The buttons read *that* window's rows for
// "already on this plot", and a click lands in the window's own row set.
for (const std::unique_ptr<App::ScopeWindow>& scope : app.scopes) {
if (scope->rows == nullptr || scope->rows->row_count() <= 0) continue;
GtkWidget* scope_head = gtk_label_new(("In \"" + scope->name + "\"").c_str());
gtk_widget_add_css_class(scope_head, "heading");
gtk_widget_set_halign(scope_head, GTK_ALIGN_START);
gtk_box_append(GTK_BOX(page), scope_head);
GtkWidget* scope_box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 4);
gtk_widget_set_name(scope_box, "udpscope-scope-plot-on");
for (int r = 0; r < scope->rows->row_count(); ++r) {
GtkWidget* b = text_button(scope->rows->plot_label(r).c_str(), scope_box);
tag(b, index);
g_object_set_data(G_OBJECT(b), "udpscope-row", GINT_TO_POINTER(r));
g_object_set_data(G_OBJECT(b), "udpscope-scope", scope.get());
bool on_this_row = false;
for (int slot = 0; slot < RowSet::kSlotsPerRow; ++slot) {
if (scope->rows->row(r).signal_name[static_cast<std::size_t>(slot)] == sig.name) {
on_this_row = true;
}
}
gtk_widget_set_sensitive(b, !on_this_row);
gtk_widget_set_tooltip_text(b, on_this_row ? "already on this plot"
: "draw this signal in this window's plot");
g_signal_connect(b, "clicked", G_CALLBACK(+[](GtkButton* b, gpointer user) {
App* a = static_cast<App*>(user);
auto* scope = static_cast<App::ScopeWindow*>(
g_object_get_data(G_OBJECT(b), "udpscope-scope"));
if (scope == nullptr) return;
outlive_the_rebuild(GTK_WIDGET(b));
assign_signal_to_scope(*a, *scope, tagged(b),
GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b),
"udpscope-row")));
}),
&app);
}
gtk_box_append(GTK_BOX(page), scope_box);
}
GtkWidget* off = text_button("Remove from plots", page); GtkWidget* off = text_button("Remove from plots", page);
gtk_widget_set_halign(off, GTK_ALIGN_START); gtk_widget_set_halign(off, GTK_ALIGN_START);
gtk_widget_set_sensitive(off, plotted); gtk_widget_set_sensitive(off, plotted);
+82 -13
View File
@@ -505,24 +505,26 @@ void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals) {
app.updating_panels = false; app.updating_panels = false;
} }
bool assign_signal_to_row(App& app, std::size_t signal, int row) { /// The drop's one-line report, wherever the drop landed: the notice bar and the log, because a
if (signal >= app.model->signals().size()) return false; /// refusal a reader cannot act on is only half an answer.
void drop_notice(App& app, const std::string& what) {
const auto notice = [&](const std::string& what) {
set_notice(app, what, 4); set_notice(app, what, 4);
std::fprintf(stderr, "[udpscope] drop: %s\n", what.c_str()); std::fprintf(stderr, "[udpscope] drop: %s\n", what.c_str());
std::fflush(stderr); std::fflush(stderr);
}; }
/// Resolves a dropped signal to an acquired channel, enabling the signal when it has to be: the
/// channel index and its display name, or -1 when the signal cannot be acquired (a multi-element
/// signal has no channel to bind). Shared by the main window's drop and the scope windows'.
int resolve_drop_channel(App& app, std::size_t signal, std::string* name) {
// The dropped signal's channels are the ones the model built for it; take the // The dropped signal's channels are the ones the model built for it; take the
// first, or the first element if the signal is a vector. // first, or the first element if the signal is a vector.
const std::vector<ChannelInfo>& channels = app.model->channels(); const std::vector<ChannelInfo>& channels = app.model->channels();
int channel = -1; int channel = -1;
std::string name;
for (std::size_t i = 0; i < channels.size(); ++i) { for (std::size_t i = 0; i < channels.size(); ++i) {
if (channels[i].signal == signal) { if (channels[i].signal == signal) {
channel = static_cast<int>(i); channel = static_cast<int>(i);
name = channels[i].name; *name = channels[i].name;
break; break;
} }
} }
@@ -533,13 +535,21 @@ bool assign_signal_to_row(App& app, std::size_t signal, int row) {
for (std::size_t i = 0; i < again.size(); ++i) { for (std::size_t i = 0; i < again.size(); ++i) {
if (again[i].signal == signal) { if (again[i].signal == signal) {
channel = static_cast<int>(i); channel = static_cast<int>(i);
name = again[i].name; *name = again[i].name;
break; break;
} }
} }
} }
return channel;
}
bool assign_signal_to_row(App& app, std::size_t signal, int row) {
if (signal >= app.model->signals().size()) return false;
std::string name;
const int channel = resolve_drop_channel(app, signal, &name);
if (channel < 0) { if (channel < 0) {
notice("cannot acquire '" + app.model->signals()[signal].name + drop_notice(app, "cannot acquire '" + app.model->signals()[signal].name +
"' (multi-element signals are not acquired)"); "' (multi-element signals are not acquired)");
return false; return false;
} }
@@ -554,9 +564,9 @@ bool assign_signal_to_row(App& app, std::size_t signal, int row) {
if (slot < 0) { if (slot < 0) {
// Say what to do about it. A refusal a reader cannot act on is only half an answer: the two ways // Say what to do about it. A refusal a reader cannot act on is only half an answer: the two ways
// out are another plot or another row. // out are another plot or another row.
notice("plot " + app.rows->plot_label(row) + drop_notice(app, "plot " + app.rows->plot_label(row) +
" holds four signals already: drop it on another plot, or add a row with the layout " " holds four signals already: drop it on another plot, or add a row with "
"editor (L)"); "the layout editor (L)");
return false; return false;
} }
// The slot remembers the *signal* (and element), never the channel index: the // The slot remembers the *signal* (and element), never the channel index: the
@@ -567,10 +577,69 @@ bool assign_signal_to_row(App& app, std::size_t signal, int row) {
schedule_layout_save(app); schedule_layout_save(app);
app.relayout_pending = true; app.relayout_pending = true;
rebuild_signal_panels(app); // the row's colour chip now has a trace to name rebuild_signal_panels(app); // the row's colour chip now has a trace to name
notice("plotting '" + name + "' on plot " + app.rows->plot_label(row)); drop_notice(app, "plotting '" + name + "' on plot " + app.rows->plot_label(row));
return true; return true;
} }
bool assign_signal_to_scope(App& app, App::ScopeWindow& scope, std::size_t signal, int row) {
if (scope.rows == nullptr || signal >= app.model->signals().size()) return false;
if (row < 0 || row >= scope.rows->row_count()) return false;
std::string name;
const int channel = resolve_drop_channel(app, signal, &name);
if (channel < 0) {
drop_notice(app, "cannot acquire '" + app.model->signals()[signal].name +
"' (multi-element signals are not acquired)");
return false;
}
int slot = -1;
for (int s = 0; s < RowSet::kSlotsPerRow; ++s) {
if (!scope.rows->slot_bound(row, s)) {
slot = s;
break;
}
}
if (slot < 0) {
drop_notice(app, scope.name + " plot " + scope.rows->plot_label(row) + " holds four signals "
"already: drop it on another plot of that window");
return false;
}
// The slot remembers the *signal* (and element), never the channel index -- the same rule the
// main window's rows follow, for the same renumbering reason.
const std::vector<SignalInfo>& signals = app.model->signals();
const SignalInfo& sig = signals[signal];
scope.rows->assign(row, slot, sig.name, name, signal_color_index(signals, signal));
// The rebuild and the area's re-point are the window's own frame tick's (a drop arrives from
// inside this widget, and a synchronous rebuild would reset the drag controller under it). The
// signal page is refreshed now: its *Plot on* buttons read the scope's rows for "already on".
signal_pages_sync(app);
drop_notice(app, "plotting '" + name + "' on " + scope.name + " plot " +
scope.rows->plot_label(row));
return true;
}
gboolean on_scope_drop_channel(GtkDropTarget* target, const GValue* value, double x, double y,
gpointer user) {
App* app = static_cast<App*>(user);
auto* scope =
static_cast<App::ScopeWindow*>(g_object_get_data(G_OBJECT(target), "udpscope-scope"));
if (scope == nullptr || scope->area == nullptr || scope->rows == nullptr) return FALSE;
if (!G_VALUE_HOLDS_STRING(value)) return FALSE;
const char* text = g_value_get_string(value);
if (text == nullptr || *text == '\0') return FALSE;
char* end = nullptr;
const unsigned long long parsed = std::strtoull(text, &end, 10);
if (end == nullptr || *end != '\0') return FALSE;
const std::size_t signal = static_cast<std::size_t>(parsed);
// Where the drop landed decides the row *of this window*: one area covers its figure's cells.
const int row = std::clamp(static_cast<int>(scope->area->row_at(x, y)), 0,
scope->rows->row_count() - 1);
if (!assign_signal_to_scope(*app, *scope, signal, row)) return FALSE;
if (plot::input::Controller* c = scope->area->row_controller(static_cast<plot::u32>(row))) {
c->on_key(plot::input::KeyEvent{plot::input::kKeyEscape, 0, true});
}
return TRUE;
}
gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y, gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y,
gpointer user) { gpointer user) {
App* app = static_cast<App*>(user); App* app = static_cast<App*>(user);
+2 -2
View File
@@ -443,8 +443,8 @@ constexpr TourStep kTourSteps[] = {
{"plot", nullptr, "The plot", {"plot", nullptr, "The plot",
"Every signal you have switched on is drawn here, all plots sharing one time axis -- and the " "Every signal you have switched on is drawn here, all plots sharing one time axis -- and the "
"plots of a split row share one y as well. The wheel zooms, a drag pans, and a right-click on " "plots of a split row share one y as well. The wheel zooms, a drag pans, and a right-click on "
"a plot opens its own menu: fit, full screen (the button also comes out in a figure's corner " "a plot opens its own menu: fit, full screen and detach (the corner buttons come out as the "
"as the pointer nears it), a second window showing that figure, and the layout editor, which " "pointer nears them), a second window showing that figure, and the layout editor, which "
"splits a row into up to four plots.", "splits a row into up to four plots.",
&App::area_overlay}, &App::area_overlay},
{"badges", nullptr, "The badges", {"badges", nullptr, "The badges",