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
+109
-3
@@ -190,6 +190,14 @@ struct App {
|
||||
/// figure, so there is one widget, one context and one `::render` whatever the row
|
||||
/// count is.
|
||||
GtkWidget* area_widget = nullptr;
|
||||
/// The pointer's last position over the plot area, in the area's coordinates (the overlay's,
|
||||
/// for the toggles' corner), and whether one is over it at all: the full-screen toggles are
|
||||
/// revealed by proximity, so the frame tick reads this. The overlay's own motion controller
|
||||
/// keeps it current, which is why a pointer *on* a revealed toggle still counts as over the
|
||||
/// area -- the toggle is an overlay child, and the event still travels the ancestor.
|
||||
double area_pointer_x = 0.0;
|
||||
double area_pointer_y = 0.0;
|
||||
bool area_pointer_valid = false;
|
||||
plot::gtk4::PlotArea* area = nullptr;
|
||||
/// The GL area wrapped in a `GtkOverlay`, which is what the plot box actually holds.
|
||||
/// The layout editor's buttons are overlay children: each is a small widget placed by
|
||||
@@ -197,6 +205,24 @@ struct App {
|
||||
/// is -- a single transparent child covering the area would swallow every pan, drop and
|
||||
/// hover the editor is not interested in.
|
||||
GtkWidget* area_overlay = nullptr;
|
||||
/// One extra top-level scope window: its own figure and `RowSet`, bound to the same
|
||||
/// `ScopeModel` by the main window's tick -- which is also its repaint pump -- so a scope
|
||||
/// is a second *view* of one acquisition, never a second acquisition. The main window's
|
||||
/// x window (or its pin) is mirrored onto it every frame, so the two look at the same
|
||||
/// span; its own controllers can still pan and zoom, and the mirror reclaims the span on
|
||||
/// the next frame.
|
||||
struct ScopeWindow {
|
||||
GtkWindow* window = nullptr;
|
||||
plot::gtk4::PlotArea* area = nullptr;
|
||||
GtkWidget* widget = nullptr;
|
||||
/// The area's initial plot, owned here: `PlotArea::create()` wants a `Plot&` that
|
||||
/// outlives the widget, and sharing the main window's placeholder would hand two areas
|
||||
/// one plot the moment the figure mode is ever left.
|
||||
plot::Plot placeholder;
|
||||
std::unique_ptr<RowSet> rows;
|
||||
};
|
||||
std::vector<std::unique_ptr<ScopeWindow>> scopes;
|
||||
|
||||
/// On while the layout editor is up. Nothing else about the plot changes: the editor is
|
||||
/// a set of overlay buttons, not a mode the figure knows about.
|
||||
bool layout_editing = false;
|
||||
@@ -204,6 +230,11 @@ struct App {
|
||||
/// The editor's buttons, in the order they were added, so they can be taken off the
|
||||
/// overlay again. Rebuilt when the row count changes, repositioned when the rows move.
|
||||
std::vector<GtkWidget*> layout_buttons;
|
||||
/// The full-screen toggles: one per figure row, on the overlay **always** (not only while
|
||||
/// the editor is up), placed in each row's top-right corner. Rebuilt when the figure row
|
||||
/// count changes (`maximize_buttons_rows` is the count they were built for).
|
||||
std::vector<GtkWidget*> maximize_buttons;
|
||||
int maximize_buttons_rows = -1;
|
||||
/// The boundary a resize drag is on, and the weights it started from: a drag is *relative* to where it
|
||||
/// began, so the beginning has to be kept -- reading the weights while dragging would compound.
|
||||
int layout_drag_row = -1;
|
||||
@@ -261,6 +292,15 @@ struct App {
|
||||
/// What the badge was last set to (`BadgePhase`), so the markup is rewritten on a
|
||||
/// change of phase rather than on every one of the 60 frames a second.
|
||||
int badge_phase = -1;
|
||||
/// The trigger status circle beside it (`main-window.ui` `trigger_status`): green while an
|
||||
/// event is held or a capture is filling, flashing green while armed and waiting, red while
|
||||
/// disarmed, hidden while the trigger is off. Same width-locked logic as the badge: one
|
||||
/// "●" glyph in every state, markup rewritten on a change of the state key only, and the
|
||||
/// flash runs off the same half-second square the badge blinks on.
|
||||
GtkWidget* trigger_status = nullptr;
|
||||
int trigger_circle_state = -1;
|
||||
/// The circle's state as of the last tick, for the exit dump (which cannot read widgets).
|
||||
int dbg_trigger_circle = -1;
|
||||
/// The display-window control: a header-bar button whose label is the current window
|
||||
/// and whose popover holds the number, its unit and the two decade steppers. The label
|
||||
/// is width-locked (see `kWindowLabelChars`) because the header bar sits above the plot
|
||||
@@ -681,6 +721,14 @@ struct App {
|
||||
plot::Range measure_window{};
|
||||
bool measure_window_valid = false;
|
||||
bool measure_was_held = false;
|
||||
/// The **gate band** the last pass saw, in the same stream seconds, clipped to the window the
|
||||
/// pane was about. The gate is per measurement, but the band is one: when any measurement is
|
||||
/// gated, a band that moves -- a drag, a seed, a mode change -- changes what *those*
|
||||
/// measurements describe without the viewport or the pin moving, so the "it moved" question
|
||||
/// is asked of this as well. Booked even when nothing is gated (then it is the pane's own
|
||||
/// window, and the comparison below repeats one the viewport has already answered).
|
||||
plot::Range measure_gate_window{};
|
||||
bool measure_gate_window_valid = false;
|
||||
/// The window **on screen**, in the same stream seconds, as of the last *tick* -- which is the
|
||||
/// only thing `measure_window` is comparable with. Captured here rather than read back in the
|
||||
/// dump for the reason every other dump field is (see verification.md): the dump runs from the
|
||||
@@ -704,12 +752,15 @@ struct App {
|
||||
/// `MeasureChunk`).
|
||||
/// One channel's window, at one HF corner, with every level accumulator the pane asked for.
|
||||
///
|
||||
/// Keyed by `(channel, fc)` and not by channel alone: the corner is a per-measurement setting, so two
|
||||
/// measurements on one signal at different corners are reading two different signals and cannot share a
|
||||
/// walk.
|
||||
/// Keyed by `(channel, fc, gated)` and not by channel alone: the corner and the gate are
|
||||
/// per-measurement settings, so two measurements on one signal at different corners -- or one
|
||||
/// gated and one not -- are reading two different windows and cannot share a walk. A gated
|
||||
/// window is walked over the gate band clipped to the pane's window; an ungated one over the
|
||||
/// pane's window whole.
|
||||
struct MeasureWindow {
|
||||
int channel = -1;
|
||||
double fc = 0.0; ///< the HF corner this window was walked at; 0 is unfiltered
|
||||
bool gated = false; ///< this window is the gate band, not the pane's window whole
|
||||
MeasureStats stats;
|
||||
std::vector<double> pct; ///< the level shares asked for, deduplicated
|
||||
std::vector<double> noise; ///< each entry's band, as a percentage; parallel to `pct`
|
||||
@@ -798,6 +849,29 @@ struct App {
|
||||
/// probes add measurements, so they must not be scheduled in one run (see verification.md), and a
|
||||
/// shared counter would make that a silent mis-step rather than a documented prohibition.
|
||||
int dbg_bandavg_step = 0;
|
||||
/// The gate probe's (`UDPSCOPE_PROBE_GATE_MS`) step counter and bookkeeping: the bounds it
|
||||
/// set and the `s0` it read when it set them, so the second leg can say whether the band
|
||||
/// stayed still while the stream's clock moved under it.
|
||||
int dbg_gate_step = 0;
|
||||
double dbg_gate_x0 = 0.0;
|
||||
double dbg_gate_x1 = 0.0;
|
||||
double dbg_gate_s0 = 0.0;
|
||||
/// The gate probe's settle polls (`dbg_gate_step == 1` re-enters while the ring is younger
|
||||
/// than the window, which is what its whole-periods arithmetic needs), bounded so a stalled
|
||||
/// stream still measures and prints.
|
||||
int dbg_gate_wait = 0;
|
||||
/// The row a gate-region drag was on last frame, or -1. The fall to -1 is the drag's end,
|
||||
/// and the tick saves the session there: the bounds are session state, and a drag that is
|
||||
/// not saved is a session that lies about where the gate is.
|
||||
int gate_drag_last = -1;
|
||||
/// Which step of the crosshair probe runs next (`UDPSCOPE_PROBE_CROSSHAIR_MS`).
|
||||
int dbg_crosshair_step = 0;
|
||||
/// Which step of the maximize probe runs next (`UDPSCOPE_PROBE_MAXIMIZE_MS`).
|
||||
int dbg_maximize_step = 0;
|
||||
std::size_t dbg_maximize_single = 0;
|
||||
int dbg_maximize_hidden = -1;
|
||||
/// Which step of the scope-window probe runs next (`UDPSCOPE_PROBE_SCOPE_MS`).
|
||||
int dbg_scope_step = 0;
|
||||
int cursors_pane_mode = -1;
|
||||
int cursors_pane_rows = -1;
|
||||
unsigned cursors_pane_generation = ~0u;
|
||||
@@ -1440,6 +1514,10 @@ struct MeasureDialog {
|
||||
GtkWidget* band = nullptr;
|
||||
GtkWidget* above = nullptr;
|
||||
GtkWidget* show = nullptr;
|
||||
/// The per-measurement gate: measure only inside the one shared band. Type-independent --
|
||||
/// the gate is about *which samples* answer, and every type reads samples -- so unlike the
|
||||
/// rows above it is shown for every type and never retitled.
|
||||
GtkWidget* gate = nullptr;
|
||||
GtkWidget* window = nullptr;
|
||||
/// The option rows' boxes and labels, so the dialog can *retitle* them per type: the same
|
||||
/// switch means "the pulse is above the level" for a pulse and nothing at all for a period,
|
||||
@@ -1618,6 +1696,12 @@ constexpr int kWindowLabelChars = 9;
|
||||
constexpr int kRowKeyBias = 1;
|
||||
constexpr int kLayoutButtonPx = 24;
|
||||
constexpr int kLayoutButtonPad = 4;
|
||||
/// How far from its corner a full-screen toggle comes out of hiding. The toggles are on the
|
||||
/// screen only while the pointer is near them; this is the slack around the button's own rect
|
||||
/// (a square corner zone a few button-widths wide), large enough that a pointer arrives without
|
||||
/// having to find the exact pixel and small enough that crossing a plot's top edge leaves it
|
||||
/// hidden.
|
||||
constexpr int kMaximizeRevealPx = 48;
|
||||
constexpr const char* kLayoutKindKey = "udpscope-layout-insert";
|
||||
|
||||
/// The ↕ handles' own key: what they are is *not* what the `+` is, and `sync_layout_editor()`
|
||||
@@ -1627,6 +1711,9 @@ constexpr const char* kLayoutResizeKey = "udpscope-layout-resize";
|
||||
constexpr const char* kLayoutCellKey = "udpscope-layout-cell";
|
||||
/// Column resize handles carry the plot index of the right-hand cell.
|
||||
constexpr const char* kLayoutColKey = "udpscope-layout-col";
|
||||
/// The full-screen toggle's key: the figure row it acts on, +1 (0 is not a valid row). Its
|
||||
/// own key because the button is on the overlay *always*, not only while the editor is up.
|
||||
constexpr const char* kMaximizeKey = "udpscope-maximize-row";
|
||||
/// The minimum width a cell may be dragged to, in logical pixels.
|
||||
constexpr double kMinCellPx = 96.0;
|
||||
/// The plotter's own per-cell floor, `kMinCellWidthLogical` in `deps/plotter/src/plot/figure.cpp`
|
||||
@@ -1705,6 +1792,9 @@ int window_unit_for(double s);
|
||||
std::string format_window_value(double s, int unit);
|
||||
std::string format_window(double s);
|
||||
const char* badge_markup(BadgePhase phase);
|
||||
/// The trigger status circle's markup, by state (see `trigger_circle_markup` in
|
||||
/// `ui_status.cpp`): 0 solid green, 1/2 the flash's halves, 3 red.
|
||||
const char* trigger_circle_markup(int state);
|
||||
std::string format_measure(double v, const std::string& unit);
|
||||
void set_notice(App& app, std::string what, int seconds = 4);
|
||||
void set_row_key(GtkWidget* widget, int row);
|
||||
@@ -1862,6 +1952,9 @@ bool event_on_screen(const App& app, const EventRecord& r);
|
||||
bool event_in_group(const App& app, const EventRecord& r);
|
||||
/// The signal chip of a measurement's row, in the colour every other view names that signal with.
|
||||
std::string measure_chip_markup(App& app, const std::string& signal);
|
||||
/// The 1-based number of an active channel (its position among the acquired channels), or 0
|
||||
/// when the signal is not acquired. The number the badge bar and the measurements pane show.
|
||||
int measure_channel_number(App& app, const std::string& signal);
|
||||
std::string row_unit(App& app, int row);
|
||||
std::string slot_unit(App& app, int row, int slot);
|
||||
std::uint32_t bound_slot_mask(App& app);
|
||||
@@ -2002,6 +2095,11 @@ GdkTexture* scale_texture(GdkTexture* source, int width, int height);
|
||||
EventThumbCache* event_thumb_cache(App& app);
|
||||
GdkTexture* event_preview_texture(App& app, const std::string& path);
|
||||
void refresh_events_pane(App& app);
|
||||
/// Points the event cache at the configuration the session is on -- the active one when
|
||||
/// `App::config_name` is empty -- and refreshes the two views of it when the directory changed.
|
||||
/// Every configuration switch calls it next to setting `App::config_name`, so the list on screen
|
||||
/// is always the configuration's own.
|
||||
void retarget_event_store(App& app);
|
||||
/// Opens the `events` page. **Every route that opens that page must use this** and never `slideover_open()`
|
||||
/// directly: the page's list is rebuilt only while the page is open (see `record_event()`), and the
|
||||
/// slide-over has no on-open hook, so the list is refreshed here, once, before the page shows. The refresh
|
||||
@@ -2041,6 +2139,14 @@ gboolean mathwrap_probe_wait(gpointer user);
|
||||
gboolean mathwrap_probe_report(gpointer user);
|
||||
gboolean measure_probe(gpointer user);
|
||||
gboolean levels_probe(gpointer user);
|
||||
gboolean gate_probe_step(gpointer user);
|
||||
gboolean crosshair_probe(gpointer user);
|
||||
gboolean maximize_probe_step(gpointer user);
|
||||
gboolean scope_probe_step(gpointer user);
|
||||
/// A second top-level window showing the same stream through its own figure: `figure_row` >= 0
|
||||
/// copies that figure of the main window (cells, widths, every slot's signal), -1 opens an
|
||||
/// empty one-row scope. The main window's tick binds, acquires and repaints it.
|
||||
void open_scope_window(App& app, int figure_row);
|
||||
gboolean measurepin_probe(gpointer user);
|
||||
gboolean measurepin_held_wait(gpointer user);
|
||||
gboolean measurepin_pause_wait(gpointer user);
|
||||
|
||||
Reference in new issue
Block a user