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:
1 parent
0c9333858c
commit
a29f38eaa9
9 files changed
+326
-12
No files matched your search
+53
-3
@@ -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
|
||||
|
||||
Reference in new issue
Block a user