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.
This commit is contained in:
Martino Ferrari committed 2026-11-06 12:06:21 +01:00
1 parent 0c9333858c
commit a29f38eaa9
9 files changed
+326 -12

No files matched your search

@@ -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
component, the same rule the configuration files use, so a name typed with a slash cannot
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.
+15 -5
View File
@@ -1462,12 +1462,13 @@ 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
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). **Do not run it with `UDPSCOPE_ENABLE_ALL_MS`**: the
enable-all probe's second leg, two seconds in, clears the front plot through the signal page --
exactly the slot this probe copies -- so `copied=''` under that combination is that race and not
a defect in the copy. 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] 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'
@@ -1479,11 +1480,20 @@ a defect in the copy. Measured output:
- `count=2` is both windows having been created and retained; `windows=1` each being a visible
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
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
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.
- 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