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
+110
@@ -728,6 +728,11 @@ void open_scope_window(App& app, int figure_row, bool detached) {
|
||||
for (auto it = a->scopes.begin(); it != a->scopes.end(); ++it) {
|
||||
if ((*it)->window != window) continue;
|
||||
App::ScopeWindow* victim = it->release();
|
||||
// A closed window is remembered -- its arrangement is the session's, and
|
||||
// *Reopen closed windows* brings it back. Attaching is the exception: the
|
||||
// figure is on the main window again, so a reopen entry would be a
|
||||
// duplicate of what is already on screen.
|
||||
if (victim->remember_on_close) remember_closed_window(*a, *victim);
|
||||
a->scopes.erase(it);
|
||||
g_idle_add_full(G_PRIORITY_DEFAULT_IDLE,
|
||||
+[](gpointer p) -> gboolean {
|
||||
@@ -1503,6 +1508,93 @@ void set_scope_layout_editing(App& app, App::ScopeWindow& scope, bool on) {
|
||||
if (scope.widget != nullptr) gtk_gl_area_queue_render(GTK_GL_AREA(scope.widget));
|
||||
}
|
||||
|
||||
void sync_reopen_scopes_item(App& app) {
|
||||
if (app.reopen_scopes_item == nullptr) return;
|
||||
const std::size_t n = app.closed_windows.size();
|
||||
const std::string label =
|
||||
n == 0 ? std::string("Reopen closed windows")
|
||||
: "Reopen closed windows (" + std::to_string(n) + ")";
|
||||
gtk_button_set_label(GTK_BUTTON(app.reopen_scopes_item), label.c_str());
|
||||
gtk_widget_set_sensitive(app.reopen_scopes_item, n > 0 ? TRUE : FALSE);
|
||||
}
|
||||
|
||||
App::ScopeWindow* open_scope_window_from_state(App& app, const LayoutState::Window& state) {
|
||||
// The window itself comes from the one constructor every window uses -- an empty scope window,
|
||||
// the same one the overflow menu makes -- and then its arrangement is dealt into its row set.
|
||||
open_scope_window(app, -1, state.detached);
|
||||
if (app.scopes.empty()) return nullptr;
|
||||
App::ScopeWindow* scope = app.scopes.back().get();
|
||||
if (scope->rows == nullptr || state.rows.empty()) {
|
||||
scope->name = state.name;
|
||||
if (scope->window != nullptr) gtk_window_set_title(scope->window, state.name.c_str());
|
||||
return scope;
|
||||
}
|
||||
// The cells per figure row, from the `row` keys the same way `apply_layout()` reads the main
|
||||
// layout: a file that names the figure row of every plot carries its splits, an older one is
|
||||
// one cell per row.
|
||||
std::vector<int> cells;
|
||||
{
|
||||
int prev = -1;
|
||||
for (const LayoutState::Row& r : state.rows) {
|
||||
const int fr = r.row < 0 ? prev + 1 : r.row;
|
||||
if (fr != prev) cells.push_back(1);
|
||||
else if (++cells.back() > RowSet::kMaxCellsPerRow) cells.back() = RowSet::kMaxCellsPerRow;
|
||||
prev = fr;
|
||||
}
|
||||
if (cells.size() > static_cast<std::size_t>(RowSet::kMaxRows)) cells.resize(RowSet::kMaxRows);
|
||||
}
|
||||
scope->rows->set_cells_per_row(cells);
|
||||
std::vector<float> row_weights;
|
||||
row_weights.reserve(cells.size());
|
||||
std::size_t at = 0;
|
||||
for (const int n : cells) {
|
||||
row_weights.push_back(static_cast<float>(state.rows[at].weight));
|
||||
at += static_cast<std::size_t>(n);
|
||||
}
|
||||
scope->rows->set_row_weights(std::move(row_weights));
|
||||
at = 0;
|
||||
for (std::size_t fr = 0; fr < cells.size(); ++fr) {
|
||||
const std::size_t n = static_cast<std::size_t>(cells[fr]);
|
||||
if (n >= 2) {
|
||||
std::vector<float> widths;
|
||||
for (std::size_t c = 0; c < n; ++c) widths.push_back(static_cast<float>(state.rows[at + c].width));
|
||||
scope->rows->set_cell_weights(static_cast<int>(fr), std::move(widths));
|
||||
}
|
||||
at += n;
|
||||
}
|
||||
// The slots, by name (the channel index is re-resolved every frame by `bind()`). The copy is
|
||||
// taken under the model's lock: the producer replaces the list on every CONFIG.
|
||||
std::vector<SignalInfo> signals;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(app.model->mutex());
|
||||
signals = app.model->signals();
|
||||
}
|
||||
const auto color_of = [&](const std::string& name) {
|
||||
for (std::size_t si = 0; si < signals.size(); ++si) {
|
||||
if (signals[si].name == name) return signal_color_index(signals, si);
|
||||
}
|
||||
return app.rows->signal_colors().count(name) != 0
|
||||
? static_cast<int>(app.rows->signal_colors().at(name))
|
||||
: 0;
|
||||
};
|
||||
for (int i = 0; i < scope->rows->row_count() &&
|
||||
static_cast<std::size_t>(i) < state.rows.size();
|
||||
++i) {
|
||||
const LayoutState::Row& row = state.rows[static_cast<std::size_t>(i)];
|
||||
for (int sl = 0; sl < RowSet::kSlotsPerRow && static_cast<std::size_t>(sl) < row.signals.size(); ++sl) {
|
||||
const std::string& name = row.signals[static_cast<std::size_t>(sl)];
|
||||
if (name.empty()) continue;
|
||||
scope->rows->assign(i, sl, name, name, color_of(name));
|
||||
}
|
||||
}
|
||||
if (scope->area != nullptr) scope->area->set_figure(&scope->rows->figure());
|
||||
scope->name = state.name;
|
||||
if (scope->window != nullptr) {
|
||||
gtk_window_set_title(scope->window, (state.name + (state.detached ? " (detached)" : "")).c_str());
|
||||
}
|
||||
return scope;
|
||||
}
|
||||
|
||||
gboolean on_scope_key(GtkEventControllerKey* controller, guint keyval, guint, GdkModifierType state,
|
||||
gpointer user) {
|
||||
App& app = *static_cast<App*>(user);
|
||||
@@ -1569,6 +1661,7 @@ void attach_scope_window(App& app, App::ScopeWindow& scope) {
|
||||
}
|
||||
}
|
||||
GtkWindow* window = scope.window;
|
||||
scope.remember_on_close = false; // its figure is going back to the main window
|
||||
if (window != nullptr) gtk_window_destroy(window); // erases the scope, refreshes the pages
|
||||
app.relayout_pending = true;
|
||||
if (app.area != nullptr) app.area->set_figure(&app.rows->figure());
|
||||
@@ -6227,6 +6320,23 @@ GtkWidget* build_toolbar(App& app) {
|
||||
}),
|
||||
&app);
|
||||
gtk_box_append(GTK_BOX(menu_box), scope_item);
|
||||
// The windows a session remembers but a launch does not open: the entry carries their count and
|
||||
// opens them on request.
|
||||
app.reopen_scopes_item = menu_button("Reopen closed windows", nullptr);
|
||||
gtk_widget_set_tooltip_text(app.reopen_scopes_item,
|
||||
"open the extra windows the session remembers (they are saved, "
|
||||
"never opened at start)");
|
||||
g_signal_connect(app.reopen_scopes_item, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
App& a = *static_cast<App*>(user);
|
||||
std::vector<LayoutState::Window> windows = std::move(a.closed_windows);
|
||||
a.closed_windows.clear();
|
||||
for (const LayoutState::Window& w : windows) open_scope_window_from_state(a, w);
|
||||
sync_reopen_scopes_item(a);
|
||||
schedule_layout_save(a);
|
||||
}),
|
||||
&app);
|
||||
gtk_box_append(GTK_BOX(menu_box), app.reopen_scopes_item);
|
||||
sync_reopen_scopes_item(app);
|
||||
GtkWidget* prefs_item = menu_button("Preferences\u2026", nullptr);
|
||||
g_signal_connect(prefs_item, "clicked", G_CALLBACK(+[](GtkButton*, gpointer user) {
|
||||
open_preferences(*static_cast<App*>(user));
|
||||
|
||||
Reference in new issue
Block a user