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

+53 -3
View File
@@ -448,7 +448,8 @@ bool load_layout(const std::string& path, LayoutState& out) {
std::string line;
// 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;
bool plot_section_seen = false;
while (std::getline(in, line)) {
@@ -459,6 +460,14 @@ bool load_layout(const std::string& path, LayoutState& out) {
section = Section::Plot;
plot_section_seen = true;
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]]") {
section = Section::Calibration;
st.calibration.emplace_back();
@@ -608,6 +617,35 @@ bool load_layout(const std::string& path, LayoutState& out) {
} 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()) {
if (key == "signals") {
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";
o << "relative_time = " << (state.relative_time ? "true" : "false") << "\n";
if (state.capture_packets > 0) o << "capture_packets = " << state.capture_packets << "\n";
for (const auto& row : state.rows) {
o << "\n[[plot]]\n";
const auto write_row = [&](const LayoutState::Row& row) {
// 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.
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 << "]\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) {
o << "\n[[calibration]]\nname = " << toml_escape(cal.name) << "\ngain = " << cal.gain