Gated measurements #2
No files matched your search
@@ -145,7 +145,13 @@ every frame, and pumps its render — so the two windows never disagree about th
|
||||
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. A scope window also **carries its own layout editor**: `L` (or the grid button in its corner,
|
||||
"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 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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
+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
|
||||
|
||||
@@ -36,6 +36,19 @@ struct LayoutState {
|
||||
double window_s = 0.0; ///< 0 keeps the default window
|
||||
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 {
|
||||
std::string name;
|
||||
double gain = 1.0;
|
||||
|
||||
+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));
|
||||
|
||||
@@ -225,6 +225,10 @@ struct App {
|
||||
/// 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.
|
||||
@@ -252,6 +256,12 @@ struct App {
|
||||
std::unique_ptr<RowSet> rows;
|
||||
};
|
||||
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;
|
||||
|
||||
@@ -2090,6 +2100,16 @@ bool assign_signal_to_scope(App& app, App::ScopeWindow& scope, std::size_t signa
|
||||
/// 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);
|
||||
/// 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
|
||||
|
||||
@@ -14,6 +14,46 @@ namespace ui_impl {
|
||||
|
||||
/// 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.
|
||||
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 st;
|
||||
st.window_s = app.rows->time_window();
|
||||
@@ -64,6 +104,12 @@ LayoutState collect_layout(App& app) {
|
||||
if (model.signals()[si].math || !on[si]) continue;
|
||||
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
|
||||
// writing those out would freeze a colour that is meant to follow the signal list.
|
||||
for (const auto& [name, rgb] : app.rows->signal_colors()) {
|
||||
@@ -437,6 +483,10 @@ void apply_layout(App& app, const LayoutState& st) {
|
||||
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.measure_stats_on = st.measure_stats;
|
||||
app.updating_panels = true;
|
||||
|
||||
+47
-3
@@ -1769,6 +1769,11 @@ gboolean scope_probe_step(gpointer user) {
|
||||
if (a->window == nullptr) return G_SOURCE_REMOVE;
|
||||
switch (a->dbg_scope_step) {
|
||||
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();
|
||||
for (std::size_t i = 0; i < signals.size(); ++i) {
|
||||
if (signals[i].name == "sine") a->model->set_signal_enabled(i, true);
|
||||
@@ -1805,9 +1810,22 @@ gboolean scope_probe_step(gpointer user) {
|
||||
}
|
||||
const App::ScopeWindow& copy = *a->scopes[0];
|
||||
const App::ScopeWindow& empty = *a->scopes[1];
|
||||
const std::string copied = copy.rows->row(0).signal_name[0];
|
||||
const bool copied_bound = copy.rows->row(0).channel[0] >= 0;
|
||||
const bool empty_ok = empty.rows->row(0).signal_name[0].empty();
|
||||
// The first non-empty slot of the copied row: the copy is slot-for-slot faithful, and slot 0
|
||||
// is empty whenever an earlier probe (the enable-all one disables a signal at its end) left
|
||||
// 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 &&
|
||||
gtk_widget_get_visible(GTK_WIDGET(copy.window)) &&
|
||||
gtk_widget_get_visible(GTK_WIDGET(empty.window));
|
||||
@@ -1973,6 +1991,32 @@ gboolean scope_probe_step(gpointer user) {
|
||||
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);
|
||||
return G_SOURCE_REMOVE;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user