scope: full-screen figures, second windows, gated measurements, per-configuration events
A batch of view and instrument work, with the probes and docs for each. - **Full screen per figure row**: a toggle on each row's top-right corner, revealed by pointer proximity (`kMaximizeRevealPx` around its placed rect, per row, read from an overlay-level motion controller so a revealed button under the pointer cannot flicker); the plot menu offers the same as *Full screen this figure* / *Restore the layout*. The arrangement is saved and restored through `RowState`, so nothing plotted moves. A layout of one figure carries no toggle. - **Second window**: `open_scope_window()` copies a figure (cells, width shares, signals in their colours) or opens an empty one, with its own `RowSet` and `PlotArea`; the main window's tick binds, acquires and repaints it, mirroring the time window and pin. Entries: the plot menu's *Open this plot in a new window* and the overflow menu's *New scope window*. - **Layout editor from the plot**: the plot menu gains *Edit the layout...* / *Done editing the layout*, flipping the app's one toggle. - **Measurements gate**: `MeasureOptions::gate` restricts a measurement to the held/pause snapshot (live falls back to the live ring), persisted with the session and shown in the measure dialog. - **Crosshair axis badges**: a preference (`crosshair_axis_labels`, on by default) and the plotter's badges where the lines meet the axes, in the axes' own formats; the plot menu and the probe read them. - **Event cache per configuration**: `EventStore` is keyed `<source>/<configuration>/`, `retarget_event_store()` follows every configuration switch, and a pre-configuration cache (loose files) is adopted into the configuration's folder on first start. - **GTK4 lifecycle**: the main window's handlers move from the non-existent `"destroy"` signal to `close-request`; scope windows are closed with it and the application quits explicitly (the exit run's hang). The exit probe closes with `gtk_window_close()`. - Probes: `[maximize-probe]` (single-figure rule and the corner reveal), `[scope-probe]`, `[split-probe]` cell crosshair leg, `[prefs-probe]` store keying, `[configs]` store key, `[rowmenu]` layout-editor entry; docs, help pages, README and AGENT.md updated. Also fixes a probe-only use-after-free: `toggle_maximize_figure_row` replaces the figure object, so the area must be re-pointed (the button's path does; the probe's raw call did not).
This commit is contained in:
1 parent
bfe13630f9
commit
bdf0ff7ddc
39 files changed
+2267
-136
No files matched your search
+117
-1
@@ -133,6 +133,13 @@ class RowSet {
|
||||
/// it keeps its position whatever the cursor toggle says.
|
||||
plot::u32 trigger_marker = 0;
|
||||
|
||||
/// The measurement gate: one shaded interval per plot, whose bounds are the gate's own
|
||||
/// pair of instruments -- not the cursors above, which stay what the cursors pane reads.
|
||||
/// Every row carries its own `Region` because each plot shades its own axes rect, but
|
||||
/// they all draw the one shared interval (`gate_x0_`/`gate_x1_` below), the way the
|
||||
/// vertical cursors are one time on every row.
|
||||
plot::u32 gate_region = 0;
|
||||
|
||||
/// The measurements' readout marks: a pool created with the plot (a `Plot` cannot add or
|
||||
/// drop a cursor after the fact) and parked unless a measurement is using the slot.
|
||||
std::array<plot::u32, kMeasureMarks> measure_h{};
|
||||
@@ -211,6 +218,18 @@ class RowSet {
|
||||
/// Shows `rows` figure rows of one cell each (so `rows` plots, any split undone). A changed
|
||||
/// arrangement rebuilds the figure, since a `Figure`'s shape is fixed at construction.
|
||||
void set_row_count(int rows);
|
||||
|
||||
/// One figure row filling the plot area, and back -- the "full screen this row" toggle.
|
||||
///
|
||||
/// The arrangement, the weights and **every row's state** are saved on the way in and
|
||||
/// restored on the way out, so a maximize is a *view*: nothing a user plotted moves, and
|
||||
/// nothing survives a restore that was not there before it. Volatile like a zoom -- the
|
||||
/// session keeps the arrangement, not the view. Calling it while maximized restores, so a
|
||||
/// click on the eye-catching button is its own way back; a figure row outside the current
|
||||
/// arrangement (the maximized one, by definition a single row) is ignored when not
|
||||
/// maximized.
|
||||
void toggle_maximize_figure_row(int fr);
|
||||
bool figures_maximized() const noexcept { return maximized_; }
|
||||
/// Inserts an empty single-cell figure row at `at` (0 = above the first row,
|
||||
/// `figure_row_count()` = below the last), pushing the rows at and below it down with their
|
||||
/// cells. Returns false at `kMaxRows` plots.
|
||||
@@ -322,6 +341,11 @@ class RowSet {
|
||||
void set_y_range(int row, double lo, double hi);
|
||||
|
||||
void set_cursors_visible(bool on);
|
||||
/// The crosshair's axis badges (the y and x values the crosshair's lines meet the axes at,
|
||||
/// drawn in the label strips): one flag for the whole set, restated to every new plot by
|
||||
/// `install()` -- a rebuilt row keeps the setting, the way it keeps the cursor mode.
|
||||
void set_crosshair_axis_labels(bool on);
|
||||
bool crosshair_axis_labels() const noexcept { return crosshair_axis_labels_; }
|
||||
/// What the cursors are (see `CursorMode`). Switching parks the instruments the mode
|
||||
/// does not use rather than removing them: a `Plot` cannot drop a cursor.
|
||||
void set_cursor_mode(CursorMode mode);
|
||||
@@ -369,12 +393,16 @@ class RowSet {
|
||||
|
||||
/// One measurement's readout mark on a row: a *level* (drawn as a horizontal ruler) or a
|
||||
/// *time* (a vertical marker). Which one a measurement needs is the measurement's own
|
||||
/// business; the row is the one that plots its signal.
|
||||
/// business; the row is the one that plots its signal. A time mark's `label` is the badge
|
||||
/// text the plot draws beside it (its own time, the pane's formatter), so a column of
|
||||
/// marks reads as numbers and not as bare lines; a level ruler writes its own value, so
|
||||
/// its label stays empty.
|
||||
struct MeasureMark {
|
||||
int row = 0;
|
||||
bool level = false;
|
||||
double value = 0.0;
|
||||
plot::u32 color = 0;
|
||||
std::string label;
|
||||
};
|
||||
|
||||
/// Places the measurements' marks and parks every pool slot they do not use. The marks are
|
||||
@@ -494,6 +522,33 @@ class RowSet {
|
||||
/// Called once a frame from the tick, with `drag_row` < 0 when no cursor is held.
|
||||
void sync_cursors(int drag_row);
|
||||
|
||||
// --- the measurement gate -------------------------------------------------
|
||||
|
||||
/// Shows (or hides) the gate on every row. Bounds that were never seeded are seeded
|
||||
/// first (`reset_gate()`), so the first enable puts the band under the data instead of
|
||||
/// parking it wherever zero is.
|
||||
void set_gate_visible(bool on);
|
||||
/// Whether the gate is on. One flag for the whole set, like the cursors' own: a per-row
|
||||
/// copy is something a rebuild can clear behind the user's back.
|
||||
bool gate_visible() const noexcept { return gate_on_; }
|
||||
/// The gate's bounds, in the **display** x the axes read -- the cursors' own coordinate
|
||||
/// space. A live view is newest-relative, so a gate left at `[-0.5, -0.1]` keeps meaning
|
||||
/// "half a second ago to a tenth of a second ago" and the data flows under it; a held
|
||||
/// event's stamps are edge-relative, so there the band names a fixed slice of the frozen
|
||||
/// frame. Either way it stays where it was put, which is what a gate has to do.
|
||||
double gate_x0() const noexcept { return gate_x0_; }
|
||||
double gate_x1() const noexcept { return gate_x1_; }
|
||||
/// Restates the gate's bounds (ordered, both finite) and redraws every row.
|
||||
void set_gate(double x0, double x1);
|
||||
/// Puts the gate over the middle half of the current x window -- where
|
||||
/// `reset_cursors()` puts the vertical pair -- and remembers it as seeded.
|
||||
void reset_gate();
|
||||
/// Adopts the gate bounds a gesture is moving, on `drag_row` (the row whose controller
|
||||
/// holds a region), and mirrors them to every other row. Same contract as
|
||||
/// `sync_cursors()`: reading outside a drag reads our own echo. Called once a frame from
|
||||
/// the tick, with `drag_row` < 0 when no region drag is open.
|
||||
void sync_gate(int drag_row);
|
||||
|
||||
/// Puts both vertical cursors at a quarter and three quarters of the x window, and
|
||||
/// the two rulers at a third and two thirds of each row's y range. Also switches the
|
||||
/// cursors on, so the button cannot look dead.
|
||||
@@ -585,6 +640,46 @@ class RowSet {
|
||||
/// objects the traces point at, and those belong to the row's *position*, not to its
|
||||
/// contents. `rebuild_all()` re-deals them straight after.
|
||||
static void carry_row(Row& dst, const Row& src);
|
||||
/// One row's persistent state, without the plot-bound `slots`: a `SlotSeries` owns a
|
||||
/// `unique_ptr`, so a `Row` is not copy-assignable, and the state a save needs is exactly
|
||||
/// what `carry_row()` copies. The maximize toggle saves it for every row on the way in and
|
||||
/// re-applies it on the way out, so a view change moves no signal.
|
||||
struct RowState {
|
||||
std::array<std::string, kSlotsPerRow> signal_name;
|
||||
std::array<std::string, kSlotsPerRow> name;
|
||||
std::array<int, kSlotsPerRow> channel;
|
||||
std::array<int, kSlotsPerRow> color_index;
|
||||
std::array<int, kSlotsPerRow> layer;
|
||||
std::array<float, kSlotsPerRow> slot_alpha;
|
||||
double ruler_y = 0.0;
|
||||
double ruler_b_y = 0.0;
|
||||
};
|
||||
static RowState row_state_of(const Row& r) {
|
||||
RowState s;
|
||||
s.signal_name = r.signal_name;
|
||||
s.name = r.name;
|
||||
s.channel = r.channel;
|
||||
s.color_index = r.color_index;
|
||||
s.layer = r.layer;
|
||||
s.slot_alpha = r.slot_alpha;
|
||||
s.ruler_y = r.ruler_y;
|
||||
s.ruler_b_y = r.ruler_b_y;
|
||||
return s;
|
||||
}
|
||||
static void apply_row_state(Row& r, const RowState& s) {
|
||||
r.signal_name = s.signal_name;
|
||||
r.name = s.name;
|
||||
r.channel = s.channel;
|
||||
r.color_index = s.color_index;
|
||||
r.layer = s.layer;
|
||||
r.slot_alpha = s.slot_alpha;
|
||||
r.ruler_y = s.ruler_y;
|
||||
r.ruler_b_y = s.ruler_b_y;
|
||||
// What it was bound to belonged to the row this state came *from*; the tick's `bind()`
|
||||
// re-resolves every slot next frame, and clearing stops the frame in between from
|
||||
// drawing the moved row's data under the new row's legend (the `carry_row()` rule).
|
||||
for (SlotSeries& slot : r.slots) slot.bind(nullptr);
|
||||
}
|
||||
/// Back to an empty row: nothing plotted, the default palette entries, no cursors.
|
||||
static void clear_row(Row& r);
|
||||
|
||||
@@ -596,6 +691,9 @@ class RowSet {
|
||||
/// at the end of the gesture (`Controller::end_drag()`), and a value read out of the plot
|
||||
/// mid-drag has already been through that act.
|
||||
void place_cursors(Row& r, plot::CursorPlacement placement = plot::CursorPlacement::Snapped);
|
||||
/// Writes a row's gate region from the shared bounds -- parked outside the axis when the
|
||||
/// gate is off, the cursors' own convention for taking an instrument off screen.
|
||||
void place_gate(Row& r);
|
||||
|
||||
std::unique_ptr<plot::Figure> figure_;
|
||||
/// The one x link group every visible row belongs to. Groups are numbered from 1.
|
||||
@@ -635,6 +733,17 @@ class RowSet {
|
||||
std::vector<plot::InvalidSpan> spans_;
|
||||
CursorMode cursor_mode_ = CursorMode::Vertical;
|
||||
bool cursors_on_ = false;
|
||||
bool crosshair_axis_labels_ = true;
|
||||
/// The maximize toggle's saved arrangement (see `toggle_maximize_figure_row()`): every
|
||||
/// row's persistent state, the cells, the weights and the plot count, kept only while
|
||||
/// `maximized_`. States, not `Row`s: a `Row` is not copyable (its slots own a
|
||||
/// `unique_ptr`) and the figure handles belong to the position anyway.
|
||||
bool maximized_ = false;
|
||||
std::array<RowState, static_cast<std::size_t>(kMaxRows)> saved_row_states_{};
|
||||
std::vector<int> saved_cells_;
|
||||
std::vector<std::vector<float>> saved_cell_weights_;
|
||||
std::vector<float> saved_row_weights_;
|
||||
int saved_rows_ = 0;
|
||||
bool relative_x_ = false;
|
||||
double x_origin_ = 0.0;
|
||||
/// What was last *written* to each row's own vertical cursors. A row is only evidence of a
|
||||
@@ -648,6 +757,13 @@ class RowSet {
|
||||
/// The one stream time each vertical cursor is at, whatever row's copy was last moved.
|
||||
double cursor_a_x_ = 0.0;
|
||||
double cursor_b_x_ = 0.0;
|
||||
/// The gate's shared interval and its on/off flag (`gate_visible()`). `gate_seeded_`
|
||||
/// distinguishes "never placed" from "placed at the window's own origin", which the
|
||||
/// first enable needs: seeding must put the band over the data, not over x = 0.
|
||||
bool gate_on_ = false;
|
||||
bool gate_seeded_ = false;
|
||||
double gate_x0_ = 0.0;
|
||||
double gate_x1_ = 0.0;
|
||||
std::array<bool, static_cast<std::size_t>(kMaxRows)> row_dirty_{};
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user