A scope or detached window drew its traces and nothing else: no cursors, no gate band, no trigger marker, no measurement marks. They are not per-window state to be recreated -- the cursors are one pair of times, the gate one band, the trigger one instant -- so the main window's row set stays the **owner** and every window is written from it. `sync_scope_instruments()` runs in the main tick after the main's own `sync_cursors()`/ `sync_gate()`: a gesture inside a scope window is read back from *that* window's controllers first (those two calls only adopt while their own plot holds the grab, which is exactly "the drag happened here"), adopted by the owner, and then every window -- the dragged one included -- is written from the owner with the same cursor pair, mode and visibility, gate bounds and visibility, and trigger marker. `RowSet::set_cursors()` is the new pair setter; `trigger_marker_x()`/`_visible()` are the getters the mirror reads. The measurement marks travel by signal name (`RowSet::MeasureMark::signal`, filled by the measures pass), so each window finds the row that plots the signal and draws its ruler or mark there -- or none, when it does not plot it. The search marks are times on the window and are placed on every window by the same per-frame path, and the crosshair-axis-labels preference is restated to open windows when it changes. Probe: `[scope-probe] instruments windows=N cursors=N gate=N trigger=N marks=N (main trigger on=..)`, after adding a real gated `Top` measurement on `sine` -- the gate is restated visible each frame by `update_measures()` and marks come only from `show_on_plot` measurements, so the leg uses the app's own path. Docs: README, the cursors invariant, and the probe readings.
2535 lines
147 KiB
C++
2535 lines
147 KiB
C++
// SPDX-License-Identifier: MIT
|
||
// SPDX-FileCopyrightText: 2026 Martino Ferrari
|
||
//
|
||
// The front end's *internal* interface.
|
||
//
|
||
// `ui.cpp` was one 13-thousand-line translation unit. The window is assembled from several, and this
|
||
// header is what they share: the application state (`App`) and the free functions the front end is
|
||
// built from. Nothing here is part of the app's public surface -- `ui.hpp` is -- and nothing here owns
|
||
// sample data: the model does, and `RowSet` owns the figure.
|
||
//
|
||
// The *definitions* carry the default arguments (they were written that way), so these declarations
|
||
// are generated without them: a default in both places is an error.
|
||
|
||
#pragma once
|
||
|
||
|
||
#include <gtk4/gtk_plot_area.hpp>
|
||
|
||
#include "acquisition.hpp"
|
||
#include "build_info.hpp"
|
||
#include "capture.hpp"
|
||
#include "config.hpp"
|
||
#include "event_store.hpp"
|
||
#include "export.hpp"
|
||
#include "help_window.hpp"
|
||
#include "layout_store.hpp"
|
||
#include "measure.hpp"
|
||
#include "model.hpp"
|
||
#include "plot_host.hpp"
|
||
#include "search.hpp"
|
||
#include "settings.hpp"
|
||
#include "source/source_config.hpp"
|
||
#include "ui_prefs.hpp"
|
||
#include "shortcuts.hpp"
|
||
#include "status_text.hpp"
|
||
#include "waveform.hpp"
|
||
|
||
|
||
|
||
#include <adwaita.h>
|
||
#include <gtk/gtk.h>
|
||
|
||
#include <algorithm>
|
||
#include <array>
|
||
#include <cmath>
|
||
#include <cstdint>
|
||
#include <map>
|
||
#include <memory>
|
||
#include <mutex>
|
||
#include <string>
|
||
#include <unordered_map>
|
||
#include <vector>
|
||
|
||
namespace udpscope {
|
||
namespace ui_impl {
|
||
|
||
enum class EventGroupKind {
|
||
None, ///< nothing: streaming, one event replayed, or a file opened
|
||
Compare, ///< two events in two plots side by side, at one scale, with synchronised axes
|
||
Accumulate, ///< up to four events overlaid in the same traces, translucent, so agreement is brightness
|
||
};
|
||
|
||
// --- the guided tour, shared by both windows (ui_tour.cpp) -------------------
|
||
//
|
||
// The tour is *two* tours now -- the main window's and the source chooser's -- because the chooser is
|
||
// the window a first run meets first and the surfaces it must explain are its own. The machinery is
|
||
// one implementation: a `TourView` is the overlay children (the four dim rectangles, the spotlight
|
||
// outline and the card) plus where the walk stands in it, and either window holds one. What differs
|
||
// is only the *stops*: a stop's target is a widget the owner resolves, so each window keeps its own
|
||
// table (the chooser runs before `App` exists and could not name one of `App`'s fields).
|
||
|
||
/// One stop of a guided tour: the widget to spotlight, the page it stands on, and the two paragraphs
|
||
/// of text beside it. The target is resolved when the tour starts, so the table may name widgets that
|
||
/// do not exist yet.
|
||
struct TourStop {
|
||
GtkWidget* target;
|
||
/// The page the target lives on, or nullptr when the stop needs none. What a page *is* depends on
|
||
/// the view: a named page of its `page_stack` (the chooser), or whatever its `on_page` hook opens
|
||
/// (the main window: a slide-over page id). The tour selects it as the stop is entered, so a target
|
||
/// on a page the user has left is made visible instead of being walked over or spotlighted where it
|
||
/// is not. A stop with no page *closes* what an `on_page` hook opened.
|
||
const char* page;
|
||
const char* title;
|
||
const char* body;
|
||
};
|
||
|
||
/// A tour's widgets and its place in the walk. `hole` is the spotlight's rectangle in the overlay's
|
||
/// own coordinates, kept from the last frame that could measure the target: a frame can come between
|
||
/// two layout passes, where `gtk_widget_compute_bounds()` answers nothing, and a hole that emptied
|
||
/// there would make the spotlight and the card jump.
|
||
struct TourView {
|
||
GtkWidget* overlay = nullptr;
|
||
GtkWidget* dim[4] = {};
|
||
GtkWidget* spot = nullptr;
|
||
GtkWidget* card = nullptr;
|
||
GtkWidget* title = nullptr;
|
||
GtkWidget* body = nullptr;
|
||
GtkWidget* counter = nullptr;
|
||
GtkWidget* back = nullptr;
|
||
GtkWidget* next = nullptr;
|
||
int hole[4] = {0, 0, 0, 0};
|
||
bool running = false;
|
||
int index = 0;
|
||
std::vector<TourStop> plan;
|
||
/// The stack whose pages the stops' `page` names select. A window has exactly one such surface (the
|
||
/// sidebar; the chooser's two pages), so it is the view's and not each stop's. Set where the walk
|
||
/// begins, when the widgets exist; null leaves every stop on the page it finds.
|
||
GtkWidget* page_stack = nullptr;
|
||
/// The alternative to `page_stack` for a surface that is not a stack: when set it *replaces* the
|
||
/// stack mechanism for this view, and is called with each stop's `page` as the stop is entered --
|
||
/// `nullptr` meaning "no page needed" -- and with `nullptr` as the walk ends. The main window's opens
|
||
/// and closes the slide-over through it; the chooser keeps `page_stack`.
|
||
void (*on_page)(void* user, const char* page) = nullptr;
|
||
void* page_user = nullptr;
|
||
/// Optional, with `on_page`: whether the surface `on_page` opened has finished appearing. A stop that
|
||
/// names a page is not spotlit while this answers false -- a panel that is still sliding in is a
|
||
/// target that moves, and a spotlight that follows it for 120 ms reads as a glitch. Null means "ready".
|
||
bool (*page_settled)(void* user) = nullptr;
|
||
/// Runs when the walk passes its last stop (Next/Finish, never Skip): the chooser's hands the tour
|
||
/// on to the application through `Config::tour_pending`.
|
||
void (*on_done)(void* user) = nullptr;
|
||
void* done_user = nullptr;
|
||
};
|
||
|
||
/// One capability panel a source kind contributed to the slide-over. `content` is the kind's own widget
|
||
/// (made insensitive while the source is disconnected); the page wrapping it is named `name`.
|
||
struct SourcePanelPage {
|
||
std::string name; ///< slide-over page id: "source-" + the kind's panel id
|
||
std::string title; ///< the slide-over page's title, and the segmented switch's label
|
||
GtkWidget* content = nullptr;
|
||
};
|
||
|
||
/// The slide-over panel's widgets and state (src/ui_slideover.cpp). `page` is the open page's id, empty when closed.
|
||
struct SlideOver {
|
||
GtkWidget* overlay = nullptr; ///< the GtkOverlay around the plot (main-window.ui `plot_overlay`)
|
||
GtkWidget* revealer = nullptr; ///< the one overlay child; halign END, valign FILL, 360 px
|
||
GtkWidget* title = nullptr; ///< GtkLabel in the panel header
|
||
GtkWidget* stack = nullptr; ///< GtkStack: one scroller per page
|
||
GtkWidget* source_switch = nullptr; ///< AdwToggleGroup, shown on a source page when there are >= 2
|
||
std::vector<std::string> source_pages;
|
||
std::string page;
|
||
bool syncing = false; ///< true while we write the toggle group ourselves
|
||
};
|
||
constexpr int kSlideOverWidth = 360;
|
||
|
||
/// One badge: a button with a name and a state line. `page` is the slide-over page it opens.
|
||
struct Badge {
|
||
GtkWidget* button = nullptr;
|
||
GtkWidget* name = nullptr;
|
||
GtkWidget* state = nullptr;
|
||
std::string page;
|
||
std::string signal_name; ///< a signal badge: the signal it stands for; empty for the others
|
||
std::size_t signal = 0; ///< a signal badge: its index in `ScopeModel::signals()` when it was built
|
||
};
|
||
/// The most signal badges the bar shows; beyond that one "N signals" badge stands for them all.
|
||
inline constexpr std::size_t kMaxSignalBadges = 6;
|
||
/// The badge bar under the plot (src/ui_badges.cpp).
|
||
struct BadgeBar {
|
||
GtkWidget* bar = nullptr; ///< main-window.ui `badge_bar`, fixed height
|
||
GtkWidget* signals_slot = nullptr; ///< left; empty in step 1, step 2 fills it
|
||
GtkWidget* system_slot = nullptr; ///< right: Trigger, Source
|
||
Badge trigger;
|
||
Badge source; ///< opens the first source page; hidden when the source has none
|
||
GtkWidget* signals_row = nullptr; ///< the box inside the scroller: signal badges, then the "+"
|
||
/// One per shown enabled signal; empty when overflowed. `unique_ptr`: each button's "clicked" handler holds
|
||
/// a pointer to its `Badge`, which must not move when the vector does.
|
||
std::vector<std::unique_ptr<Badge>> signal_badges;
|
||
Badge overflow; ///< the "N signals" badge, used only beyond `kMaxSignalBadges`
|
||
Badge plus; ///< the "+" in the signals slot: opens the `signals` page
|
||
};
|
||
|
||
struct App {
|
||
ScopeModel* model = nullptr;
|
||
Acquisition* acq = nullptr;
|
||
RowSet* rows = nullptr;
|
||
|
||
GtkWidget* window = nullptr;
|
||
GtkWidget* plot_box = nullptr;
|
||
/// The `GtkOverlay` around `plot_box` (`main-window.ui`), which the slide-over panel is a child of.
|
||
GtkWidget* plot_overlay = nullptr;
|
||
SlideOver slideover;
|
||
/// The badge bar under the plot (`main-window.ui` `badge_bar`, with its two slots).
|
||
BadgeBar badges;
|
||
GtkWidget* badge_bar = nullptr;
|
||
GtkWidget* badge_signals = nullptr;
|
||
/// Signal names in the order the user left the badges (memory only; names not in it sort last).
|
||
std::vector<std::string> badge_order;
|
||
GtkWidget* badge_system = nullptr;
|
||
/// The one GL area, drawing the `RowSet`'s figure: every row is a row of that
|
||
/// 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
|
||
/// its margins over the row it acts on, so it intercepts pointer events only where it
|
||
/// 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 window's short name, the one the signal page's *Plot on* groups carry ("plot 2" for a
|
||
/// copy, "scope 3" for an empty one): a button must say which window it plots into.
|
||
std::string name;
|
||
/// The drop target wired onto `widget`: the handler reads the *window* off it (each window's
|
||
/// target names its own scope), and the probe calls the handler with it directly.
|
||
GtkDropTarget* drop = nullptr;
|
||
/// This window came from a **detach** -- the figure left the main window to live here -- so it
|
||
/// 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.
|
||
GtkWidget* overlay = nullptr;
|
||
/// Layout editing in this window: its own editor and toggle. A scope is a view with its own
|
||
/// arrangement, so it holds as many figure rows as it is given and edits them with the same
|
||
/// `+` / `×` / Done controls the main window's editor has -- its own, never the main window's.
|
||
bool layout_editing = false;
|
||
GtkWidget* layout_toggle = nullptr;
|
||
std::vector<GtkWidget*> layout_buttons;
|
||
GtkWidget* layout_done = nullptr;
|
||
int layout_buttons_rows = -1;
|
||
std::vector<GtkWidget*> attach_buttons;
|
||
double pointer_x = 0.0;
|
||
double pointer_y = 0.0;
|
||
bool pointer_valid = false;
|
||
/// Frames this window's area was watched for its first paint: a window whose GL context failed
|
||
/// or that never paints says so in its own title (and on stderr) instead of staying a blank
|
||
/// rectangle nobody can diagnose.
|
||
int paint_checks = 0;
|
||
/// 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;
|
||
/// 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;
|
||
|
||
/// 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;
|
||
GtkWidget* layout_toggle = nullptr;
|
||
/// 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 **detach** buttons: one per figure row beside its full-screen toggle, same reveal,
|
||
/// same rebuild cadence -- but only while the layout holds two or more figures (detaching the
|
||
/// only figure would leave the main window with nothing to draw) and never while maximized.
|
||
std::vector<GtkWidget*> detach_buttons;
|
||
/// 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;
|
||
std::vector<float> layout_drag_start;
|
||
/// Column drag state: which plot (right-hand cell), the two starting *plot* widths in logical px
|
||
/// (each plot's `layout().axes.w`, the area the plotter places a cell by -- not the cell rect, which
|
||
/// adds the y and right gutters and the gap), and the cell weights snapshot at drag begin
|
||
/// (`layout_col_apply()` redistributes the pair's weight sum from these, so a drag stays relative to
|
||
/// where it started).
|
||
int layout_col_drag_plot = -1;
|
||
double layout_col_drag_left0 = 0.0;
|
||
double layout_col_drag_right0 = 0.0;
|
||
/// The drag handle's x in the overlay's coordinates (logical px) when the drag began. The handle is
|
||
/// re-placed on the divider every frame, and a gesture reports its offset in the handle's own, moving,
|
||
/// coordinates; `layout_col_total_offset()` adds the handle's displacement since this back.
|
||
double layout_col_drag_handle_x0 = 0.0;
|
||
std::vector<float> layout_col_drag_start;
|
||
/// The editor's way out: one `Done` button in the plot area's top-right corner while
|
||
/// the editor is up. Not part of `layout_buttons`, which is one button per row and per
|
||
/// boundary -- the count the layout probe asserts.
|
||
GtkWidget* layout_done_button = nullptr;
|
||
/// The row count and figure geometry the buttons were placed for. Their positions are
|
||
/// restated only when one of these moves: a `gtk_widget_set_margin_top()` per frame is a
|
||
/// relayout per frame, and overlay children are measured out of the main child's
|
||
/// allocation, so the churn would reach the plot.
|
||
int layout_buttons_rows = 0;
|
||
/// Which step of the layout probe runs next (`UDPSCOPE_PROBE_LAYOUT_MS`). The steps are
|
||
/// a frame apart, because the buttons are placed from the rects the last frame was laid
|
||
/// out with, so the sequence cannot be one function.
|
||
int dbg_layout_step = 0;
|
||
/// Which step of the split probe runs next (`UDPSCOPE_PROBE_SPLIT_MS`).
|
||
int dbg_split_step = 0;
|
||
/// The split probe's round trip: the arrangement saved, and the legacy copy of its file.
|
||
std::string dbg_split_expect;
|
||
std::string dbg_split_legacy_path;
|
||
/// Which step of the cursors probe runs next (`UDPSCOPE_PROBE_CURSORS_MS`).
|
||
int dbg_cursors_step = 0;
|
||
/// Which step of the fit probe runs next (`UDPSCOPE_PROBE_FIT_MS`).
|
||
int dbg_fit_step = 0;
|
||
/// Which step of the x-label-form probe runs next (`UDPSCOPE_PROBE_TIMELABELS_MS`).
|
||
int dbg_timelabels_step = 0;
|
||
/// The cursors probe's own bookkeeping: the stored time before a gestureless move, so the
|
||
/// report can assert the move was ignored.
|
||
double dbg_gestureless_before = 0.0;
|
||
/// What `PlotArea::create()` is handed. The area is in figure mode from the start and
|
||
/// never draws this, but it keeps a reference to the plot it was created with (its
|
||
/// single-plot controller holds one) -- and every plot of the figure is destroyed by a
|
||
/// rebuild, so handing it a row would leave that reference dangling.
|
||
plot::Plot placeholder_plot;
|
||
|
||
GtkWidget* status_label = nullptr;
|
||
/// The activity badge: one glyph whose *colour* is the whole signal, so that blinking
|
||
/// it cannot change its width and drive a relayout (see the status label's comment).
|
||
GtkWidget* status_badge = nullptr;
|
||
/// 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
|
||
/// and a label that grew with the text would relayout everything under it.
|
||
GtkWidget* window_button = nullptr;
|
||
/// The span the control shows, pinned to `kWindowLabelChars` and ellipsized: the bar sits above the
|
||
/// plot, so a label that grew with its text would relayout everything under it on a change of window.
|
||
GtkWidget* window_label = nullptr;
|
||
GtkWidget* window_entry = nullptr;
|
||
GtkWidget* window_unit_drop = nullptr;
|
||
/// The unit `sync_window_display()` last wrote into the dropdown. `GtkDropDown`'s
|
||
/// `notify::selected` arrives *after* the call that caused it returns, so the
|
||
/// `updating_panels` flag is already clear by then and cannot tell our own write from the
|
||
/// user's pick; the entry the sync wrote is rounded for reading, so re-parsing it made the
|
||
/// rounding the window (a 500 s request came back as 499.98 s). A selection that matches
|
||
/// what we synced is ours -- GTK only notifies on a change, so the user cannot produce it.
|
||
int window_sync_unit = -1;
|
||
GtkWidget* trigger_toggle = nullptr;
|
||
/// The trigger panel's own box, kept so the defaults probe can read its spin buttons without
|
||
/// walking the window for them.
|
||
GtkWidget* trigger_panel = nullptr;
|
||
GtkWidget* signal_panel = nullptr;
|
||
GtkWidget* side_box = nullptr;
|
||
/// The dock: the sidebar's one scroller and the box inside it, which holds only the compact readouts
|
||
/// (`build_readouts_dock()`). The full cursors and measurements panels are the slide-over's `cursors`
|
||
/// and `measure` pages. A feature's own on/off never removes a panel (it sets its sensitivity instead).
|
||
GtkWidget* dock_page = nullptr;
|
||
GtkWidget* dock_scroller = nullptr;
|
||
GtkWidget* arm_button = nullptr;
|
||
/// The Force button, beside the Arm/Disarm one: one acquisition now, with the configured pre/post
|
||
/// window. It carries no state of its own -- `sync_arm_buttons()` states the one it has
|
||
/// (sensitivity, from trigger mode), so the two buttons beside each other cannot disagree about
|
||
/// whether the trigger has an engine at all.
|
||
GtkWidget* force_button = nullptr;
|
||
GtkWidget* trigger_signal_drop = nullptr;
|
||
GtkWidget* sidebar_toggle = nullptr;
|
||
GtkWidget* trigger_edge_drop = nullptr;
|
||
GtkWidget* trigger_type_drop = nullptr;
|
||
GtkWidget* trigger_cmp_drop = nullptr;
|
||
GtkWidget* trigger_width_spin = nullptr;
|
||
GtkWidget* trigger_width2_spin = nullptr;
|
||
GtkWidget* trigger_level2_spin = nullptr;
|
||
GtkWidget* trigger_timeout_spin = nullptr;
|
||
/// The `labeled_box` rows a condition may not use: held so `sync_trigger_rows()` can
|
||
/// hide them. A hidden row keeps its widget's value, so switching condition and back
|
||
/// does not lose what was typed.
|
||
GtkWidget* trigger_cmp_row = nullptr;
|
||
GtkWidget* trigger_width_row = nullptr;
|
||
GtkWidget* trigger_width2_row = nullptr;
|
||
GtkWidget* trigger_level2_row = nullptr;
|
||
GtkWidget* trigger_timeout_row = nullptr;
|
||
GtkWidget* trigger_mode_drop = nullptr;
|
||
GtkWidget* trigger_level_spin = nullptr;
|
||
GtkWidget* trigger_pre_spin = nullptr;
|
||
GtkWidget* trigger_holdoff_spin = nullptr;
|
||
GtkWidget* trigger_band_spin = nullptr; ///< EventCondition::noise_band
|
||
GtkWidget* trigger_hf_spin = nullptr; ///< EventCondition::hf_fc, 0 is off
|
||
/// The persistence spin: how many of the most recent triggered acquisitions stay on screen, drawn
|
||
/// dimmer with age (1 = the event alone, which is the display without the feature). Its row is held
|
||
/// for a `.ui` that lost it, as the other rows are.
|
||
GtkWidget* trigger_persist_spin = nullptr;
|
||
GtkWidget* trigger_persist_row = nullptr;
|
||
/// What this frame's persistence plan drew, against what the depth asked for: `sync_layers()`
|
||
/// returns the former and fills the latter, and the two differ exactly when a row had no free
|
||
/// slot left for a layer or the retention budget refused an acquisition. Read by the exit dump and
|
||
/// by the persistence probe, and what the "only some layers fit" notice is keyed on.
|
||
int persist_want = 0;
|
||
int persist_placed = 0;
|
||
/// The last plan a notice was raised for, so a plan that stays short says so once rather than
|
||
/// every frame.
|
||
int persist_noted_depth = 0;
|
||
int persist_noted_want = -1;
|
||
int persist_noted_placed = -1;
|
||
/// The persistence probe's own state: the depth its first leg asks for (kept between steps,
|
||
/// because the steps are separate `GSourceFunc`s), how many polls it has spent waiting for the
|
||
/// retention to fill, and whether that set has stopped growing (which is how a budget that keeps
|
||
/// fewer acquisitions than the depth asks for still gets a report).
|
||
int dbg_persist_depth = 4;
|
||
int dbg_persist_polls = 0;
|
||
std::size_t dbg_persist_last_kept = 0;
|
||
int dbg_persist_stable_polls = 0;
|
||
bool dbg_persist_settled = false;
|
||
/// The phosphor density's controls -- the trigger pane's *Advanced* block: the switch, the
|
||
/// dropdown that chooses what the image fades by, and the two parameter spins, only one of which
|
||
/// is on screen at a time (`sync_trigger_rows()` states that, as it does for the condition rows).
|
||
/// The rows and the expander are held too, so the probe can read what the `.ui` holds and the dump
|
||
/// can walk into the block -- an expander's child is *not* in the widget-parent chain.
|
||
GtkWidget* trigger_density_switch = nullptr;
|
||
GtkWidget* trigger_density_decay_drop = nullptr;
|
||
GtkWidget* trigger_density_tau_spin = nullptr;
|
||
GtkWidget* trigger_density_factor_spin = nullptr;
|
||
GtkWidget* trigger_density_row = nullptr;
|
||
GtkWidget* trigger_density_decay_row = nullptr;
|
||
GtkWidget* trigger_density_tau_row = nullptr;
|
||
GtkWidget* trigger_density_factor_row = nullptr;
|
||
GtkWidget* trigger_advanced = nullptr;
|
||
/// A new acquisition has reached the screen and has not been deposited into the density grid yet.
|
||
/// Set where a snapshot is *framed* -- the held-event branch of the tick -- and when the display
|
||
/// is switched on under an acquisition already on screen, and consumed by the next tick's
|
||
/// `apply_density()`: the deposit needs the plot laid out for the acquisition it deposits, and
|
||
/// that layout is the frame's, one frame after the bind.
|
||
bool density_pending = false;
|
||
/// The previous tick's clock (`g_get_monotonic_time()`), for the density's per-frame fade; 0
|
||
/// before the first frame, which is what makes the first frame's `dt` zero rather than since-boot.
|
||
gint64 density_last_us = 0;
|
||
/// Whether the density was on the last time the tick looked, so switching it on under an
|
||
/// acquisition already on screen deposits that one at once instead of waiting for a latch that
|
||
/// `single` mode may never send.
|
||
bool density_was_on = false;
|
||
/// What the last tick left on screen, for the dump: the rows the density was on, the total ink
|
||
/// and the peak over them, and the bytes the last *frame* uploaded. Captured on the tick because
|
||
/// the dump runs after the window is gone.
|
||
/// The **last tick the density was on**, captured there and not read back from the plots at
|
||
/// exit: the dump runs after the window is gone and after the exit probe has switched the trigger
|
||
/// off, which turns the display off with it -- a dump that asked the plots would say `rows=0`
|
||
/// about a run whose whole tail had the image up.
|
||
int dbg_density_rows = 0;
|
||
int dbg_density_traces = 0;
|
||
unsigned long long dbg_density_cells = 0;
|
||
unsigned long long dbg_density_grid_bytes = 0;
|
||
double dbg_density_ink = 0.0;
|
||
double dbg_density_peak = 0.0;
|
||
/// The most any one frame **uploaded** of the grid: one byte a cell, so this is the resolution
|
||
/// read back as the frame's own cost. A maximum rather than the last value, because the exit
|
||
/// probe changes the display before the dump runs and the last painted frame is then not a frame
|
||
/// of this display.
|
||
long long dbg_density_upload_bytes = 0;
|
||
/// How many ticks it was on for: 0 is "no image was ever drawn", which is what separates a real
|
||
/// reading from a default.
|
||
int dbg_density_frames = 0;
|
||
/// The frame-cost accumulators as of the tick the probe switched the display on, so the cost can
|
||
/// be read **before and after** it inside one run -- the same stream, the same layout, the same
|
||
/// mock. Two runs would differ in everything but the density.
|
||
double dbg_density_tick_sum0 = 0.0;
|
||
int dbg_density_tick_n0 = 0;
|
||
double dbg_density_draw_sum0 = 0.0;
|
||
int dbg_density_draw_n0 = 0;
|
||
/// The density probe's own state: the time constant its second half uses, and how many polls it
|
||
/// has spent waiting for an event to be on screen.
|
||
double dbg_density_tau = 1.0;
|
||
int dbg_density_polls = 0;
|
||
/// The density probe's step, so its sequence is a state machine rather than nested timers.
|
||
int dbg_density_stage = 0;
|
||
GtkWidget* signal_list = nullptr;
|
||
GtkWidget* pause_button = nullptr;
|
||
/// When the pause began, on the **monotonic wall clock**, or 0 while running. The elapsed time
|
||
/// the status bar shows has to be wall time rather than stream time: the producer never stops
|
||
/// while paused (it keeps filling the live ring, which is what makes the resume seamless), so
|
||
/// the stream's own clock advances whether the view is frozen or not and would say nothing
|
||
/// about how long the user has been looking at a still picture.
|
||
gint64 pause_started_us = 0;
|
||
/// The buffer search. Driven a few milliseconds at a time from the frame tick, because its
|
||
/// samples live behind the model's mutex and the producer needs it back between chunks.
|
||
BufferSearch search;
|
||
GtkWidget* search_run_button = nullptr;
|
||
GtkWidget* search_cancel_button = nullptr;
|
||
GtkWidget* search_status_label = nullptr;
|
||
GtkWidget* search_pos_label = nullptr;
|
||
/// "16 of 38 marks shown" -- the pool is smaller than a scan's result, and a display that is
|
||
/// showing a sixth of what it found has to say so.
|
||
GtkWidget* search_marks_label = nullptr;
|
||
GtkWidget* search_prev_button = nullptr;
|
||
GtkWidget* search_next_button = nullptr;
|
||
/// The expander the block lives in, so the probe can read its collapsed state when it reads the
|
||
/// header hint.
|
||
GtkWidget* search_exp = nullptr;
|
||
/// Set true when the last `search_jump()` produced an empty cut — the hit was so close to the
|
||
/// oldest retained sample that `publish_pause_view()` held nothing. Cleared at every jump.
|
||
bool search_jump_empty = false;
|
||
GtkWidget* menu_button = nullptr;
|
||
GtkWidget* cursors_toggle = nullptr;
|
||
/// --- the help affordance layer -------------------------------------------
|
||
///
|
||
/// The overflow menu's two views. The popover's child is a `GtkStack` holding both, and the
|
||
/// *visible child* is the view: swapping the stack rather than rebuilding the box is what keeps
|
||
/// `layout_toggle` — which points into the main view and is read by the tick — a live widget.
|
||
GtkWidget* menu_stack = nullptr;
|
||
GtkWidget* menu_main_view = nullptr;
|
||
GtkWidget* menu_help_view = nullptr;
|
||
/// The Help view's own rows, so the probe can read them and drive them without walking the popover:
|
||
/// the two existing dialogs' entry points and the two new actions.
|
||
GtkWidget* menu_shortcuts_row = nullptr;
|
||
GtkWidget* menu_docs_row = nullptr;
|
||
GtkWidget* menu_tour_row = nullptr;
|
||
/// The two navigation rows: the main view's door into the Help view, and the Back row out of it.
|
||
GtkWidget* menu_help_row = nullptr;
|
||
GtkWidget* menu_back_row = nullptr;
|
||
/// "Enable contextual help": a stateful menu row whose check *is* the setting.
|
||
GtkWidget* contextual_toggle = nullptr;
|
||
/// The "?" each surface carries, one per row of the table in `ui_context_help.cpp`. These are the
|
||
/// heading rows the buttons join — not the surfaces themselves — so a hint is a sibling of a
|
||
/// title rather than an overlay on top of a control. `math_head` only exists while the math
|
||
/// editor is open, which is why the row is skipped when it is null.
|
||
GtkWidget* trigger_head = nullptr;
|
||
GtkWidget* cursors_head = nullptr;
|
||
GtkWidget* measures_head = nullptr;
|
||
GtkWidget* signals_head = nullptr;
|
||
GtkWidget* events_head = nullptr;
|
||
GtkWidget* math_head = nullptr;
|
||
/// The status line's row, and the header bar's slot for the window control: the two surfaces that
|
||
/// are not a panel with a heading.
|
||
GtkWidget* status_row = nullptr;
|
||
GtkWidget* window_control_slot = nullptr;
|
||
/// The hint hosts that are *not* a pane's heading row: a specific control's own row, so the "?" sits
|
||
/// beside the thing it explains -- the "record events" switch, the pre-trigger share row, and the
|
||
/// signals pane's hint line (whose box exists so a hint can join it beside the text).
|
||
GtkWidget* trigger_record_row = nullptr;
|
||
GtkWidget* trigger_pre_row = nullptr;
|
||
/// The search block's expander *header* -- the box the contextual help's "trigger_search" hint joins
|
||
/// as its last child. The header, not the block: the expander's child is out of the widget-parent
|
||
/// chain until the block is expanded, so a hint there is invisible while it is collapsed.
|
||
GtkWidget* trigger_search = nullptr;
|
||
GtkWidget* signals_note = nullptr;
|
||
/// The guided tour. `tour.overlay` is a `GtkOverlay` inserted above the window's content, and the
|
||
/// four dim rectangles, the spotlight and the card are its overlay children; `tour.plan` is the
|
||
/// steps whose target was on screen when the tour started, so a surface that is not there is not
|
||
/// walked over. The machinery is shared with the source chooser's tour -- see `TourView`.
|
||
TourView tour;
|
||
/// The first-run auto-start: set by the timer that fires it, and read by the probe and the dump.
|
||
/// The decision itself is `settings.tour_seen`, which is the whole of "this has happened before".
|
||
bool tour_auto_started = false;
|
||
/// The chooser's own "Guided tour" request (`Config::tour_pending`), recorded when its timer runs.
|
||
/// A separate flag from the auto-start because it is a *different* thing: the user asked for the
|
||
/// tour before the window existed, rather than never having seen it.
|
||
bool tour_requested = false;
|
||
/// The self-check's reading of the manual's launch (`UDPSCOPE_PROBE_HELP_MS`). The launch is
|
||
/// asynchronous and the viewer is another process, so the outcome arrives in a callback and the
|
||
/// probe prints it from a timer afterwards; `attempted` is set at the call, because a launch that
|
||
/// never called back is itself a reading.
|
||
bool dbg_help_launch_attempted = false;
|
||
bool dbg_help_launch_done = false;
|
||
bool dbg_help_launch_ok = false;
|
||
std::string dbg_help_launch_error;
|
||
/// The help layer's own dump readings, captured on the tick while the window is alive (nothing
|
||
/// here may be read in the dump -- see `docs/claude/verification.md`).
|
||
std::string dbg_context_help;
|
||
std::string dbg_help_figures;
|
||
int dbg_tour_steps = -1;
|
||
int dbg_tour_step = -1;
|
||
int dbg_tour_visible = -1;
|
||
/// Which step of the help-affordance probe runs next (`UDPSCOPE_PROBE_HELPUI_MS`), and how many
|
||
/// event controllers the window carried before the tour opened -- the reading that says the tour
|
||
/// left nothing behind.
|
||
int dbg_helpui_step = 0;
|
||
int dbg_helpui_controllers = -1;
|
||
/// `UDPSCOPE_PROBE_TOUR_SURFACE_MS`: which step of that probe runs next.
|
||
int dbg_tour_surface_step = 0;
|
||
/// Where the panel controls ended up, decided once at build time: the dump runs after the window
|
||
/// is destroyed and cannot ask GTK anything about a widget.
|
||
std::string dbg_panel_controls;
|
||
/// The icon each toolbar button carries, and whether the theme resolved it. Recorded at the
|
||
/// same moment and for the same reason, but it guards a different thing: four of the icons are
|
||
/// drawn for this app and shipped inside the binary, and a resource path that never got
|
||
/// registered is *silent* -- GTK substitutes a missing-image placeholder, the button still
|
||
/// works, and every other line of the dump still passes.
|
||
std::string dbg_toolbar_icons;
|
||
std::string dbg_app_icons;
|
||
/// The panes' own switches and the header bar's buttons, read together by the exit probe: the two
|
||
/// must agree, and `dbg_panel_drove_toolbar` is the pane's switch having moved the button.
|
||
int dbg_panel_switch_trigger = -1;
|
||
int dbg_toolbar_switch_trigger = -1;
|
||
int dbg_panel_switch_cursors = -1;
|
||
int dbg_toolbar_switch_cursors = -1;
|
||
int dbg_panel_drove_toolbar = -1;
|
||
/// Both panes' switches being *sensitive* while their panes are off -- read before anything turns
|
||
/// one on. It is the whole point of putting them in the heading: a control inside an insensitive
|
||
/// parent cannot be clicked, so a switch that went insensitive with its own pane could never turn
|
||
/// it back on.
|
||
int dbg_panel_switch_live = -1;
|
||
/// The Arm button, read with the trigger off: it is the pane's one runtime control, so it is the one
|
||
/// thing in the body that *is* disabled with the mode.
|
||
int dbg_arm_sensitive = -1;
|
||
/// The header bar, so the dump can report the order the controls are in: a header bar is a
|
||
/// *titlebar*, not a child of the window, so walking down from the window does not reach it.
|
||
GtkWidget* toolbar = nullptr;
|
||
GtkWidget* cursors_reset = nullptr;
|
||
/// The cursors panel's root, kept because cursors-off disables it rather than hiding it.
|
||
GtkWidget* cursors_panel = nullptr;
|
||
/// The measurements page's root: like `cursors_panel`, insensitive until the stream has signals.
|
||
GtkWidget* measures_panel = nullptr;
|
||
/// The panes' *own* switches, in their header rows, and the panel bodies those switches disable.
|
||
/// The switch and the header bar's button are one control with two places to click: each runs the
|
||
/// same action, and `sync_panel_switches()` re-reads both from the toolbar's state after any
|
||
/// change, so they cannot disagree.
|
||
GtkWidget* trigger_switch = nullptr;
|
||
GtkWidget* cursors_switch = nullptr;
|
||
GtkWidget* trigger_body = nullptr;
|
||
/// True while `sync_panel_switches()` is writing the switches: setting a `GtkSwitch` emits its own
|
||
/// `notify::active`, and without this the program's own write would come back as a click.
|
||
bool panel_switch_sync = false;
|
||
/// The channel a session's trigger names, until `rebuild_trigger_signals()` can resolve it: the
|
||
/// file stores a *name* and the trigger needs an index into the acquired channels, which do not
|
||
/// exist until the stream has been described. Empty means "nothing pending".
|
||
std::string pending_trigger_channel;
|
||
/// The trigger pane's mode. *Locked* is the one a returning user gets: the pane shows a line that
|
||
/// says what it is set to and offers no control that changes the configuration. A first run -- no
|
||
/// session file for this source -- starts in configuration mode, because that is what a first run is
|
||
/// for. The flag lasts for the session: a lock is a mode, not a setting, so it is not persisted.
|
||
/// (The signals have no lock: the badges and the signal pages are direct manipulation.)
|
||
bool trigger_locked = false;
|
||
/// The locked trigger pane's content: a small definition list, because one line cannot hold the
|
||
/// signal, the condition, the level and the mode, and those are what a user reads the pane for.
|
||
GtkWidget* trigger_summary = nullptr;
|
||
static constexpr int kTriggerSummaryRows = 8;
|
||
GtkWidget* trigger_summary_keys[kTriggerSummaryRows] = {};
|
||
GtkWidget* trigger_summary_values[kTriggerSummaryRows] = {};
|
||
/// The compact line's arm control. It lives in the pane's *heading*, the part that stays on screen when
|
||
/// the body is hidden, and `sync_arm_buttons()` states it: present only while trigger mode is on and the
|
||
/// pane is locked, which is the one combination where the body's own Arm button has gone with the body.
|
||
GtkWidget* trigger_summary_arm = nullptr;
|
||
GtkWidget* trigger_edit = nullptr;
|
||
/// The dim label of the `signals` page that says nothing is left to add.
|
||
GtkWidget* signals_empty = nullptr;
|
||
int dbg_trigger_locked = -1;
|
||
int dbg_summary_arm_off = -1; ///< the compact line's arm control with the trigger *off*
|
||
int dbg_summary_arm_visible = -1; ///< the compact line's arm control, locked and with the trigger on
|
||
int dbg_summary_arm_sensitive = -1;
|
||
std::string dbg_summary_arm_label; ///< its text, which is the armed state in the user's words
|
||
int dbg_summary_arm_width = -1; ///< its allocation a frame later: 0 means "not laid out"
|
||
int dbg_trigger_body_visible = -1;
|
||
/// The compact line the locked trigger pane shows. It is the pane's whole content in that mode, so it
|
||
/// is read rather than assumed: empty here is a locked pane showing nothing at all.
|
||
std::string dbg_trigger_summary;
|
||
/// The same line as the exit probe found it, captured before the probe touches the trigger. The locked
|
||
/// pane says one thing about a trigger that is running and another about one that is not, and only a run
|
||
/// whose session has it off shows the second -- so the dump carries both rather than assuming either.
|
||
std::string dbg_trigger_summary_early;
|
||
/// The two [edit] buttons' icons, read in each mode by clicking into it and back: a button whose icon
|
||
/// does not resolve is *blank*, and that is how the missing "done" icon was found.
|
||
std::string dbg_pane_edit_icon;
|
||
std::string dbg_pane_view_icon;
|
||
/// The cursors pane: listing what the instruments read -- T0/T1/dT always, per row an Y1/Y2/dY
|
||
/// where rulers are up, and in `mix` mode the value each cursor crosses each trace at.
|
||
GtkWidget* cursors_mode_drop = nullptr;
|
||
GtkWidget* cursors_body = nullptr;
|
||
/// One line per row (rulers) and one per bound trace (`mix`), in the order the body
|
||
/// was built with. The texts are rewritten each frame; the widgets are rebuilt only
|
||
/// when the rows or the mode change.
|
||
/// One row of the pane's table: a `who` cell and the three value cells, so the numbers
|
||
/// line up as columns the way a table does.
|
||
struct CursorLine {
|
||
GtkWidget* label = nullptr; ///< the `who` cell (a chip's markup, or text)
|
||
GtkWidget* a = nullptr;
|
||
GtkWidget* b = nullptr;
|
||
GtkWidget* d = nullptr;
|
||
int row = -1;
|
||
int slot = -1;
|
||
};
|
||
std::vector<CursorLine> cursors_lines;
|
||
/// The dock's compact copy of the cursors table (`src/ui_dock.cpp`): one line per `cursors_lines`
|
||
/// entry, same order, written from the same strings by `update_cursors_pane()`.
|
||
struct DockCursorLine {
|
||
GtkWidget* label = nullptr;
|
||
GtkWidget* a = nullptr;
|
||
GtkWidget* b = nullptr;
|
||
GtkWidget* d = nullptr;
|
||
};
|
||
std::vector<DockCursorLine> dock_cursor_lines;
|
||
/// The Readouts section at the top of the dock, and the parts of it the rebuilds and the Edit buttons
|
||
/// reach.
|
||
GtkWidget* dock_readouts = nullptr;
|
||
GtkWidget* dock_cursor_grid = nullptr;
|
||
GtkWidget* dock_measures_box = nullptr;
|
||
GtkWidget* dock_cursors_edit = nullptr;
|
||
GtkWidget* dock_measures_edit = nullptr;
|
||
/// One value label per measurement, in `measures` order (the dock's copy of `measure_labels`).
|
||
std::vector<GtkWidget*> dock_measure_labels;
|
||
/// The readouts strip under the plot (`main-window.ui` `readouts_strip`, `src/ui_dock.cpp`): shown
|
||
/// whenever the dock is not beside the plot (`!show-sidebar || collapsed`) *and* there is something to
|
||
/// read (the cursors on, or a measurement; `sync_readouts_strip()`). Its value labels mirror the dock's one for one -- `strip_cursor_deltas` the
|
||
/// `d` cell of each `dock_cursor_lines` entry, `strip_measure_labels` each of `dock_measure_labels` --
|
||
/// and `strip_cursor_who` the cursor lines' `who` cells, all written from the dock setters' arguments.
|
||
GtkWidget* readouts_strip = nullptr;
|
||
GtkWidget* strip_box = nullptr;
|
||
GtkWidget* strip_cursor_box = nullptr; ///< the cursor chips; insensitive with the cursors off
|
||
GtkWidget* strip_measure_box = nullptr; ///< the measurement chips
|
||
GtkWidget* strip_cursor_group = nullptr; ///< the Cursors button and its chips; shown with the cursors on
|
||
GtkWidget* strip_measure_group = nullptr; ///< the Measures button and its chips; shown with measurements
|
||
GtkWidget* strip_sep = nullptr; ///< between the groups; shown when both are
|
||
std::vector<GtkWidget*> strip_cursor_who;
|
||
std::vector<GtkWidget*> strip_cursor_deltas;
|
||
std::vector<GtkWidget*> strip_measure_labels;
|
||
/// The measurements the user set up, and what they last computed. The list is the state;
|
||
/// the results are recomputed on a throttle (the numbers are window statistics, and the
|
||
/// window scrolls every frame -- 60 recomputations a second of every sample is not a
|
||
/// price the frame tick should pay).
|
||
std::vector<Measure> measures;
|
||
std::vector<MeasureResult> measure_results;
|
||
GtkWidget* measures_list = nullptr;
|
||
// --- math channels --------------------------------------------------------
|
||
/// The app's copy of the math channels: what the signal list's ticks mirror into and what the
|
||
/// session file is written from. Every change goes through `apply_math_channels()`, which hands it
|
||
/// to the model -- the model's own copy is what the producer evaluates.
|
||
std::vector<MathChannel> math;
|
||
/// The math editor's builder. The editor's window belongs to the builder's object list, so the builder is
|
||
/// held for as long as the window is up and cleared on the *next* opening rather than with the window --
|
||
/// freeing it under a live window frees the window with it. That is the source form's pattern.
|
||
GtkBuilder* math_dialog_ui = nullptr;
|
||
GtkWidget* math_dialog = nullptr;
|
||
/// One value label per measure, in `measures` order; rebuilt with the list.
|
||
std::vector<GtkWidget*> measure_labels;
|
||
/// The text each label was last set to. The dump runs from the destroy handler, where the
|
||
/// labels are gone, so it prints these instead of reading freed widgets.
|
||
std::vector<std::string> measure_text;
|
||
/// The window the current results were computed over, and whether a capture was held then:
|
||
/// a recomputation is owed when either moves (see `update_measures`).
|
||
///
|
||
/// It is kept in **stream** seconds (through `ScopeModel::stream_at_x0()`), not in the display x
|
||
/// it was read as, because a live view is newest-relative: the axis there is a fixed
|
||
/// `[-window, 0]` whatever arrives, so "the window moved" is invisible in display coordinates
|
||
/// and the pane would compute once and then never again -- the readout frozen on the samples
|
||
/// that happened to be on screen when the pane opened.
|
||
///
|
||
/// **Which** window that is depends on the view: the viewport while the view is live, and the
|
||
/// **pin** (`RowSet::pinned_range()`, captured in `measure_pin_window` below) while it is
|
||
/// pinned. A still frame is measured as a frame, so a gesture inside the pin moves neither this
|
||
/// nor any number.
|
||
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
|
||
/// destroy handler, after the exit probe has switched the trigger on, and `stream_at_x0()` by
|
||
/// then answers for a mode no frame was ever drawn in -- which read as a 45000 % drift on a run
|
||
/// whose every frame was exact.
|
||
///
|
||
/// While the view is pinned this and `measure_window` are *allowed* to differ: the pane
|
||
/// describes the pin, and a gesture may have narrowed what is on screen to a slice of it.
|
||
plot::Range measure_view_window{};
|
||
/// The pin's own extent in the same stream seconds, and whether the view was pinned when the
|
||
/// tick read it. This is the window a pinned view's results describe, so the dump measures its
|
||
/// drift against it -- otherwise a zoomed held event would read as a pane describing a window
|
||
/// the plot has left, which is precisely what it must do there.
|
||
plot::Range measure_pin_window{};
|
||
bool measure_pin_valid = false;
|
||
/// What the two walks over one channel's visible window produced, shared by every measurement
|
||
/// on that signal: the window's statistics, and one accumulator per *distinct level* any of
|
||
/// them asked for. Several measurements on one signal must cost two walks, not two each -- and
|
||
/// the window itself is never stored, which is what the chunked walks bought (see
|
||
/// `MeasureChunk`).
|
||
/// One channel's window, at one HF corner, with every level accumulator the pane asked for.
|
||
///
|
||
/// 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`
|
||
std::vector<MeasureLevel> level; ///< one accumulator per entry of `pct`
|
||
MeasureFilter scan_filter; ///< pass one's state
|
||
MeasureFilter acc_filter; ///< pass two's; a separate walk, so a separate state
|
||
/// Some measurement on this window reads a flat level, so pass one folds the bucket
|
||
/// histogram the flat-level types are answered from. Only then: a pane of means and extremes
|
||
/// should not pay for a density estimate it never reads.
|
||
bool flat_wanted = false;
|
||
bool ok = false;
|
||
};
|
||
std::vector<MeasureWindow> measure_samples;
|
||
/// The window's LOD buckets, reused across recomputes: a 1.7 M-sample window is 26 k of them,
|
||
/// and both passes walk the same list.
|
||
std::vector<MeasureBucket> measure_bucket_scratch;
|
||
/// The ink probe's gather buffer, reused for the same reason: it is refilled on every
|
||
/// painted frame and its size is O(axis pixels).
|
||
std::vector<plot::f32> measure_ink_scratch;
|
||
/// When the pane was last recomputed, for the refresh cadence above.
|
||
gint64 measure_stamp_us = 0;
|
||
bool measure_stamp_valid = false;
|
||
/// What the pane cost: the recomputes it ran and the wall time they took. A readout that is
|
||
/// recomputed per frame over a live window is exactly the kind of thing that is invisible in a
|
||
/// frame mean, so it is measured where it happens and reported in the dump.
|
||
gint64 measure_cost_us = 0;
|
||
int measure_passes = 0;
|
||
/// How much of the window the pane actually *read*: the samples covered, against the LOD
|
||
/// buckets the two walks answered from and the samples they still had to open. That ratio is
|
||
/// the whole question for this pane -- a bucket is 64 samples not read, and the raw tally is
|
||
/// the window's partial edges plus the buckets that straddle a level, which on a clean
|
||
/// waveform is one per crossing. A `raw` near `covered` means the pyramid bought nothing
|
||
/// (a noisy signal, or a window shorter than a bucket) and the cost is back to O(window).
|
||
gint64 measure_fetch_us = 0;
|
||
std::uint64_t measure_covered = 0;
|
||
std::uint64_t measure_bucketed = 0;
|
||
std::uint64_t measure_raw = 0;
|
||
/// The last pass' own duration, which the cadence adapts to.
|
||
gint64 measure_last_pass_us = 0;
|
||
/// The last *valid* result per measurement, which the marks fall back to: a level is a property
|
||
/// of the signal and survives the window it was measured in, so the rulers do not blink off when
|
||
/// a pass lands on a window with no samples (a rebuild, a capture boundary, a mode change).
|
||
std::vector<MeasureResult> measure_last_valid;
|
||
/// Running statistics, one per entry of `measures` and reset whenever that vector is
|
||
/// rebuilt. Not reset by a pause, a latch or a pan: those change what is measured *now*,
|
||
/// not what the measurement means, and a statistic the user cannot keep across them is
|
||
/// not worth having.
|
||
std::vector<MeasureStatsAccum> measure_stats;
|
||
/// The second line of each measurement's row, one per entry of `measures`. Hidden rather
|
||
/// than removed when the toggle is off, so turning it on costs no rebuild -- and a rebuild
|
||
/// is what would reset the statistics the user just asked to see.
|
||
std::vector<GtkWidget*> measure_stats_labels;
|
||
bool measure_stats_on = false;
|
||
GtkWidget* measures_stats_check = nullptr;
|
||
GtkWidget* measures_stats_reset = nullptr;
|
||
/// The marks the last computation produced: re-placed every frame, which is what makes the
|
||
/// measurement cursors read-only.
|
||
///
|
||
/// A **time** mark's `value` is held in stream seconds, like `measure_window`, and converted to
|
||
/// the axis' own x by `place_measure_marks()` on the frame it is placed. That is what keeps a
|
||
/// mark glued to the sample it measured while a live view rolls the data leftwards under it --
|
||
/// a mark is a *result*, so it travels with its datum, which is the opposite of a cursor (an
|
||
/// instrument the user put at a position and that stays there). A level mark's `value` is a y
|
||
/// and has nothing to do with either clock.
|
||
std::vector<RowSet::MeasureMark> measure_marks;
|
||
/// `measure_marks` with the time marks converted to display x, rebuilt in place each frame
|
||
/// rather than allocated: this is on the frame tick's path.
|
||
std::vector<RowSet::MeasureMark> measure_marks_display;
|
||
/// What the dump prints for the header and the panels. Captured while the window is alive:
|
||
/// the dump runs from the destroy handler, where every widget call is a GTK-CRITICAL and
|
||
/// reads nothing.
|
||
int dbg_win_btn_w = -1;
|
||
int dbg_bar_w = -1;
|
||
/// The measurements pane's allocation, for the same reason and more urgently: the dump used
|
||
/// to ask the widget itself, which is where the two documented `Gtk-CRITICAL`s came from --
|
||
/// and a critical is the lucky outcome. The pointer is freed memory by then, so a run that
|
||
/// built and destroyed other windows first (`UDPSCOPE_PROBE_HELP_MS`, three of them) has had
|
||
/// that memory reused and the read is a **SIGSEGV** in the middle of the dump.
|
||
int dbg_measures_pane_w = -1;
|
||
int dbg_measures_pane_h = -1;
|
||
/// The measurement dialog while one is open (null otherwise). A probe needs a handle on it:
|
||
/// it is a window of its own, so the main window's widget tree does not reach its buttons.
|
||
GtkWidget* measure_dialog = nullptr;
|
||
int dbg_measure_step = 0;
|
||
/// The band-average probe's own step counter. Its own rather than the measurement pane's: the two
|
||
/// 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;
|
||
std::size_t dbg_detach_single = 0;
|
||
/// 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;
|
||
/// Which slots were *bound* when the pane was last built, one bit per row/slot
|
||
/// (`kMaxRows * kSlotsPerRow` is 32). `mix` draws a line per bound trace, and a slot's
|
||
/// channel is re-resolved by `bind()` every frame -- so the pane has to follow the bind
|
||
/// and not just the model's generation, or a trace that appears without a structural
|
||
/// change gets no line until something else rebuilds the table.
|
||
std::uint32_t cursors_pane_bound = ~0u;
|
||
|
||
/// The persisted-layout file this data source maps to (--server/--port), a
|
||
/// debounced save handle, and whether the file has been applied once the
|
||
/// CONFIG arrived.
|
||
std::string source_host;
|
||
std::uint16_t source_port = 0;
|
||
/// The configuration of that source this run is on, and its note. A source may have several
|
||
/// named sessions; this is the one its saves go to and the one the dialog marks as current.
|
||
std::string config_name;
|
||
std::string config_comment;
|
||
/// The cache of captured events, and whether latches are being written to it. The store is created
|
||
/// once the source is known (it is keyed on `<host>-<port>`, like the packet capture);
|
||
/// `event_recording` is the trigger panel's switch and starts off; the cache is only useful if
|
||
/// it was already on when the event you wanted happened, so switch it on before the events you want.
|
||
std::unique_ptr<EventStore> event_store;
|
||
/// Whether the cache holds anything, refreshed once a second: what keeps the dock's Events block and the
|
||
/// `events` page live for a source whose events are already written, even with recording switched off.
|
||
bool events_cached = false;
|
||
bool event_recording = false;
|
||
/// The `events` page of the slide-over: its content box, its list, the recording switch (in the
|
||
/// trigger panel, where the trigger's options live), and `events_thumb` -- the picture of the newest
|
||
/// event. Every row carries the picture of *its* event; this is the first one's, which is the one
|
||
/// the probe measures and which the page does not put anywhere special.
|
||
GtkWidget* events_box = nullptr;
|
||
GtkWidget* events_list = nullptr;
|
||
GtkWidget* events_thumb = nullptr;
|
||
GtkWidget* events_record_switch = nullptr;
|
||
/// The Events block of the dock: the box, its list (compact rows, one per `EventRecord`, the record
|
||
/// attached to each row as `udpscope-event`) and the Manage button.
|
||
GtkWidget* dock_events_box = nullptr;
|
||
GtkWidget* dock_events_list = nullptr;
|
||
GtkWidget* dock_events_manage = nullptr;
|
||
/// The comparison's second plot: its own placeholder, area and `RowSet`, and the paned that holds
|
||
/// the two side by side. `rows_b` exists only once the second area is built, and the area is hidden
|
||
/// (so the paned gives it no room and no handle) unless a comparison is up.
|
||
plot::Plot placeholder_plot_b;
|
||
plot::gtk4::PlotArea* area_b = nullptr;
|
||
GtkWidget* area_b_widget = nullptr;
|
||
/// The figure the second area was last pointed at. `set_figure()` rebuilds its per-row input
|
||
/// controllers, so it is only called when the `RowSet` really has a new figure.
|
||
plot::Figure* area_b_figure = nullptr;
|
||
std::unique_ptr<RowSet> rows_b;
|
||
GtkWidget* plot_split = nullptr;
|
||
/// The builders of the panels inside the window. A widget belongs to the builder that built it, and a
|
||
/// panel lives as long as the window, so these do too -- unlike the dialogs', which are replaced.
|
||
std::vector<GtkBuilder*> ui_builders;
|
||
|
||
/// The main window's builder. Held because the window belongs to its object list: it is replaced on
|
||
/// the next build rather than reused, and everything found by id comes out of it.
|
||
/// The split view the sidebar lives in. Its width, its collapse and its overlay are that widget's
|
||
/// business -- `AdwOverlaySplitView` -- and not a paned position's.
|
||
GtkWidget* side_view = nullptr;
|
||
/// Whether the user wants the sidebar: their last word on the toolbar's button. Kept apart from the
|
||
/// button's own state because that state now *follows* the panel, and the panel is forced off while
|
||
/// there is nothing to show -- without this, connecting a source would leave the sidebar closed.
|
||
bool sidebar_wanted = true;
|
||
/// Set while we are writing the split view's or the button's state ourselves. A *binding* cannot
|
||
/// express this: the widget moves `show-sidebar` on its own when it collapses, the binding pushes that
|
||
/// into the button, and the button's handler then records it as the user's wish -- so the sidebar never
|
||
/// comes back when a source connects. An explicit flag is reliable precisely because our writes go
|
||
/// through `update_side_visibility`, which sets the flag around them.
|
||
bool sidebar_syncing = false;
|
||
|
||
GtkBuilder* window_ui = nullptr;
|
||
bool comparing = false;
|
||
/// The last x/y range seen on each side of a comparison, one entry per row: a frame compares them
|
||
/// with the plots' own to tell *which* side the user moved, and copies from that side to the other.
|
||
/// Without it the mirror would be unconditional and the first copy would undo the gesture.
|
||
std::vector<std::array<double, 4>> sync_last_a;
|
||
std::vector<std::array<double, 4>> sync_last_b;
|
||
/// The two group actions under the pane's header, and what they last put on screen.
|
||
GtkWidget* events_compare = nullptr;
|
||
GtkWidget* events_accumulate = nullptr;
|
||
EventGroupKind group_kind = EventGroupKind::None;
|
||
std::vector<EventRecord> group;
|
||
/// True while a stored event is on screen instead of the live stream.
|
||
bool replaying = false;
|
||
/// What the live view was set to when a file view was opened over it, so that *Go live* can put it
|
||
/// back. A file view overwrites all of it and for good reasons -- the window is pinned to the
|
||
/// event's own span because that is what framing an event means, and the trigger goes off because a
|
||
/// file has no edge to wait for -- but none of those reasons survive the return to the stream, and
|
||
/// what the user set is theirs. Taken on the way *in*, and only when the view being left is the live
|
||
/// one, so a replay opened from inside a group does not snapshot the group's settings as "live".
|
||
///
|
||
/// The trigger's channel is kept by **name**: the index is a position in a channel list that a file
|
||
/// view rebuilds (a group renames its channels outright), so putting the index back would select a
|
||
/// different signal, or none.
|
||
struct LiveState {
|
||
bool valid = false;
|
||
double window_s = 0.0;
|
||
bool trigger_on = false;
|
||
TriggerConfig trigger;
|
||
std::string trigger_channel;
|
||
bool level_touched = false;
|
||
};
|
||
LiveState live_state;
|
||
/// Set by *Go live*, cleared by the tick that performs the restore, with the model generation it
|
||
/// has to see move first -- the stream's own channel list is what the stored trigger channel is
|
||
/// resolved against.
|
||
bool live_restore_pending = false;
|
||
unsigned live_restore_generation = 0;
|
||
/// Set when a latch frames a new snapshot, consumed at the *end* of that tick: the record needs the
|
||
/// rows bound to the event (for the thumbnail) and the bind happens after the display decision.
|
||
bool record_pending = false;
|
||
/// The waveform file this run opened (`--open`, or the chooser's Waveforms page), or empty for a
|
||
/// live source. Nothing arrives in this mode, so everything that would report on a stream --
|
||
/// the popover's Stream row, the starvation notice -- reports on the file instead.
|
||
std::string waveform_name;
|
||
/// The preferences, as read at start-up and written by the dialog. Held here rather than read where
|
||
/// they are used: the dialog edits a copy, saves it and hands it back, so the file and the running
|
||
/// program cannot disagree about what a preference is.
|
||
Settings settings;
|
||
/// The preferences window while it is up, and its controls: one at a time, like the configurations
|
||
/// dialog -- a second window editing the same settings would let the two disagree.
|
||
/// The preferences window. A `PrefsForm` rather than fields here because the source chooser opens
|
||
/// the same dialog and runs before `App` exists -- see `ui_prefs.hpp`.
|
||
PrefsForm prefs;
|
||
/// The configurations dialog, while it is up: one at a time, and this is what makes a second
|
||
/// `Configurations…` a present rather than a second window.
|
||
GtkWidget* config_dialog = nullptr;
|
||
/// How that source was reached. Unused at runtime -- `Acquisition` has its own copy -- but
|
||
/// written into the session file, because `<host>_<port>.toml` can say neither, and the startup
|
||
/// chooser needs both to reconnect to a multicast source from a stored row.
|
||
std::string source_multicast;
|
||
std::string source_interface;
|
||
/// The source kind and its settings, so the session can say which kind it belongs to, and the
|
||
/// words that name the source in dialogs (`host:port` for UDPS, `<kind> <identity>` otherwise).
|
||
std::string source_kind = "udps";
|
||
SourceConfig source_opts;
|
||
std::string source_title;
|
||
/// The panels the source kind added to the slide-over, in page order. Empty for UDPS and in file mode.
|
||
std::vector<SourcePanelPage> source_panels;
|
||
bool source_has_datagrams = true; ///< the kind reports raw datagrams; false leaves the capture button disabled
|
||
guint layout_save_source = 0;
|
||
bool layout_loaded = false;
|
||
unsigned signal_generation = ~0u;
|
||
/// Signal name -> the content box of its `signal:<name>` slide-over page (src/ui_signal_page.cpp).
|
||
std::map<std::string, GtkWidget*> signal_pages;
|
||
bool updating_panels = false; ///< guards signal handlers during a rebuild
|
||
bool relayout_pending = false;
|
||
/// True once the renderer's `Style` has been stated (see `tick()`): the renderer does not
|
||
/// exist until the area is realized, so the one-shot has to be a tick's job.
|
||
bool style_stated = false;
|
||
/// Set once the user edits the trigger level, so the panel stops defaulting it
|
||
/// into the middle of the declared range.
|
||
bool trigger_level_touched = false;
|
||
/// A short-lived message shown in the status line (a drop's outcome), so an
|
||
/// action that changes nothing still says why.
|
||
std::string notice;
|
||
gint64 notice_until_us = 0;
|
||
/// Trigger-mode state for the status line: how many latches were on screen last
|
||
/// frame, and whether an event is held (which is what "frozen" means).
|
||
std::uint64_t seen_latches = 0;
|
||
/// Which snapshot was framed last. Separate from `seen_latches` because a Disarm during
|
||
/// a capture publishes a snapshot without an event having fired.
|
||
std::uint64_t seen_event_seq = 0;
|
||
bool event_held = false;
|
||
/// Whether a capture is open on screen: the edge has been accepted and the post window
|
||
/// is filling in view, on the coming event's own clock. It is what keeps
|
||
/// `clamp_to_data()` off the pinned axis and tells the status line apart from "armed".
|
||
bool capturing = false;
|
||
/// The edge of the capture `capturing` refers to, so a second edge accepted before the
|
||
/// first capture closed re-frames instead of being mistaken for the same one.
|
||
double capture_edge = 0.0;
|
||
/// Monotonic time of the last event, and the edge that started the clock. The clock
|
||
/// starts when the *edge* is accepted, not when the capture closes -- that is the
|
||
/// instant the user saw -- and the edge is kept so the latch that ends a live-updated
|
||
/// capture does not restart it.
|
||
/// Wall time, not stream time: "how long since the last event" is a question about the
|
||
/// user's wait, and a stream whose clock runs at any other speed would answer it wrong.
|
||
/// 0 until the first event of the session.
|
||
gint64 last_latch_us = 0;
|
||
double timed_edge = 0.0;
|
||
/// The render gate's memory (see the end of `tick()`). A `Held` or `Paused` view is bound to
|
||
/// an immutable snapshot on a pinned axis, so a frame whose picture cannot have changed does
|
||
/// not need to be drawn again -- and on llvmpipe, or on a battery, re-drawing a frozen event
|
||
/// sixty times a second is the whole cost of the application. The first four are the coarse
|
||
/// signature of what a frozen picture can be re-framed by; a change to any of them opens
|
||
/// `render_open` for `kSettleRenders` frames, so the figure's layout floor has the passes it
|
||
/// needs to converge. `render_skipped` is the floor that makes everything the signature does
|
||
/// *not* name render soon anyway (within `kStaticRenderEvery` ticks), which is what keeps the
|
||
/// gate from being able to stall the picture.
|
||
std::uint64_t render_event_seq = 0;
|
||
double render_x_min = 0.0;
|
||
double render_x_max = 0.0;
|
||
int render_rows = -1;
|
||
int render_open = 0;
|
||
int render_skipped = 0;
|
||
/// Self-check: ticks that queued no render, so the gate is a number rather than a claim.
|
||
int dbg_render_skips = 0;
|
||
double window_limit_s = -1.0; ///< the cap the window control was last clamped against
|
||
/// The rate each channel actually *stored* over the last second, and the declared rate
|
||
/// beside it. The two differ for two reasons now and both are worth seeing: the declared
|
||
/// rate is a claim the stream may not keep, and a window too long to hold at the wire
|
||
/// rate is resampled on ingest (`Partition::time_stride`), so this is also the readout
|
||
/// of how much resolution the current window costs. Re-measured once a second (the same
|
||
/// tick as the channel dump), never per frame.
|
||
std::vector<double> stored_rate;
|
||
std::vector<double> declared_rate;
|
||
/// Self-check: how much the axes' left edge moves between frames.
|
||
int dbg_axes_x_min = 1 << 20;
|
||
int dbg_axes_x_max = -(1 << 20);
|
||
int dbg_axes_frames = 0;
|
||
/// Frames on which some row's x window did not match row 0's. The rows share the
|
||
/// link group's one window, so this is an assertion: it must read 0.
|
||
int dbg_x_desync = 0;
|
||
gint64 last_channels_meas = 0;
|
||
/// Starvation detector: the delivered-frame count and the client's reconnect count a
|
||
/// second ago. A source that serves one client at a time shows up as a reconnect plus
|
||
/// no frames for the second between two samples (see `note_starvation`).
|
||
std::uint64_t starve_frames = 0;
|
||
bool starve_have_prev = false;
|
||
/// Consecutive one-second samples with `frames` unchanged while connected and running.
|
||
int starve_still = 0;
|
||
/// Set while the starvation notice is up, so the stderr line prints on the edges.
|
||
bool starve_reported = false;
|
||
/// Frames drawn with every bound trace slot empty although its channel has
|
||
/// samples: the view was swapped and not yet published.
|
||
int dbg_blank_frames = 0;
|
||
/// The *renderer's* blank: a frame that staged no trace geometry while every bound trace held
|
||
/// samples. `dbg_blank_frames` cannot see this one -- it asks the rings, and a ring that holds
|
||
/// data while the renderer draws nothing is exactly the "the data disappears for a frame" glitch.
|
||
int dbg_render_blank_frames = 0;
|
||
/// The last frame's staged segment count, so the tick can judge the frame the renderer drew.
|
||
unsigned long long dbg_seg_last = 0;
|
||
/// Frames the one area staged: a figure is one `::render` for every row.
|
||
int dbg_frames_painted = 0;
|
||
/// Where a frame's time goes, split at the two boundaries that can be observed from
|
||
/// here: `tick` is this function (bind, acquire, clamp, status), `draw` is everything
|
||
/// the plot renderer did (measured from the frame clock's `before-paint` to the return
|
||
/// of `PlotArea`'s own `::render` handler, which is why the adapter returns FALSE from
|
||
/// it), and `paint` is the whole GTK paint including the toolkit compositing the GL
|
||
/// area's framebuffer. A complaint about lag names one of the three; a summary that
|
||
/// does not separate them cannot say which.
|
||
int dbg_tick_n = 0;
|
||
double dbg_tick_ms_sum = 0.0;
|
||
double dbg_tick_ms_max = 0.0;
|
||
int dbg_paint_n = 0;
|
||
double dbg_paint_ms_sum = 0.0;
|
||
double dbg_paint_ms_max = 0.0;
|
||
double dbg_draw_ms_sum = 0.0;
|
||
double dbg_draw_ms_max = 0.0;
|
||
/// Which paint the worst `draw` was. A mean far below the max says the cost is
|
||
/// concentrated in a few frames but not *which*, and the two answers need opposite
|
||
/// responses: an early frame is the warm-up (the glyph atlas bake and the first
|
||
/// vertex-buffer growth land on it), a late one is a frame whose own geometry was
|
||
/// expensive and is the thing to chase.
|
||
int dbg_draw_max_frame = -1;
|
||
gint64 dbg_paint_start_us = 0;
|
||
GdkFrameClock* dbg_clock = nullptr;
|
||
/// The renderer's own account of what each frame cost it: whether the traces went
|
||
/// through the LOD pyramid or were drawn raw, and how much geometry that staged. The
|
||
/// *path* is the first question to ask of a slow frame -- `raw` frames are O(visible
|
||
/// samples) by construction.
|
||
std::uint64_t dbg_seg_sum = 0;
|
||
std::uint32_t dbg_seg_max = 0;
|
||
std::uint64_t dbg_visible_max = 0;
|
||
int dbg_raw_frames = 0;
|
||
int dbg_lod_frames = 0;
|
||
/// Frames on which at least one trace's geometry was reduced onto the pixel grid (one
|
||
/// vertical segment per column). It is orthogonal to `raw`/`lod` on purpose -- the
|
||
/// reduction is gated on the *density* of the gathered points, not on which level
|
||
/// produced them -- and it is the only reading that distinguishes "the segment count
|
||
/// collapsed because the reduction ran" from "the window got narrower".
|
||
int dbg_column_frames = 0;
|
||
/// Frames on which at least one reduced column was drawn **graded by dwell** rather than
|
||
/// as one flat band (`Style::intensity_grading`, which this app turns on). A subset of
|
||
/// `columns`: grading is a way of drawing the reduction, not a third path, so the two
|
||
/// counts being equal is what says the look is reaching every reduced frame.
|
||
int dbg_graded_frames = 0;
|
||
/// Painted frames on which `draw_traces` drew **no trace at all**, and the last one's index.
|
||
/// This is the observable for "the plot flickers as if there were no data": `staged_segments`
|
||
/// cannot see it, because the grid and the instruments stage geometry of their own, so a frame
|
||
/// that drew every chrome line and not one sample still reports tens of thousands of segments.
|
||
/// A count that is only the frames before the first bind is startup; one that keeps growing
|
||
/// afterwards is the glitch.
|
||
int dbg_notrace_frames = 0;
|
||
int dbg_notrace_last = -1;
|
||
/// Times the file view has *stated* its pin rather than only refreshing its numbers. A file's
|
||
/// extent does not change while it is open, so this must read **1** for a whole run: stating the
|
||
/// pin writes the group's range, which discards whatever gesture the user had left on the axis,
|
||
/// so a count that grows with the frames is both the flicker and the reason no interaction took.
|
||
int dbg_file_repins = 0;
|
||
/// The widest *minimum* width the side column or either pane has ever demanded. The side
|
||
/// column is a fixed 320 px by design, so a figure far above that is a label whose text drove
|
||
/// its own minimum -- the "layout must not depend on content" rule broken, and the only known
|
||
/// mechanism by which the plot's allocation can exceed the window.
|
||
int dbg_min_w_worst = 0;
|
||
int dbg_min_w_reports = 0;
|
||
/// Buffer rebuilds the tick has seen. Each one drops the history behind every trace.
|
||
unsigned dbg_generation_prev = 0;
|
||
bool dbg_generation_seen = false;
|
||
int dbg_generation_changes = 0;
|
||
/// Set by the tick once a bind has put samples on a trace, so the counter above can tell the
|
||
/// frames before any data from the ones after.
|
||
bool dbg_ever_bound = false;
|
||
int dbg_notrace_after_bound = 0;
|
||
/// The frame's one `DisplayMode`, recorded by the tick so the paint probe can name the
|
||
/// state a frame was drawn in without taking a second read of it (the producer can latch
|
||
/// between any two, which is why the mode is decided once per frame).
|
||
DisplayMode dbg_display = DisplayMode::Live;
|
||
/// The **ink** account, recomputed per painted frame the renderer's own way (see
|
||
/// `check_frame_ink`). `staged_segments` and `lod_traces` cannot see a waveform that
|
||
/// vanished for a frame -- the grid and the instruments stage geometry of their own, and a
|
||
/// trace whose gathered points all collapse onto one x is still "drawn" -- so these count
|
||
/// the polyline the filter actually leaves and how much of the axis it spans.
|
||
int dbg_ink_checked = 0;
|
||
int dbg_ink_collapse = 0; ///< fewer than two distinct positions left: nothing to draw
|
||
int dbg_ink_staircase = 0; ///< the filter dropped over three quarters of the points
|
||
int dbg_ink_short = 0; ///< the ink reached under a quarter of the axis' width
|
||
double dbg_ink_worst_cover = 1.0;
|
||
/// The worst distance, in pixels, between where a sample is *drawn* and the x a readout
|
||
/// quotes for the same sample. `gather()` stages x as an f32 relative to an origin the
|
||
/// renderer names, while `Plot::crosshair_point()` reads `x_at()` in exact f64, so this is
|
||
/// the quantisation of that f32 expressed in the unit the user sees it in. Measured on the
|
||
/// raw levels only, where a gathered point *is* a sample; above them a point is a bucket's
|
||
/// envelope and has no single sample to be off from.
|
||
double dbg_ink_worst_offset_px = 0.0;
|
||
int dbg_ink_raw_checked = 0; ///< frames the figure above was measured over
|
||
int dbg_ink_reports = 0;
|
||
/// Frames the badge spent in each `BadgePhase`. Both `lit` and `dim` have to be non-zero
|
||
/// over a run that acquired anything -- that is what "it blinks" means, and a badge
|
||
/// stuck lit looks identical to a working one in a screenshot.
|
||
int dbg_badge_frames[4] = {0, 0, 0, 0};
|
||
int dbg_reset_sensitive = -1;
|
||
std::vector<std::uint64_t> last_stored;
|
||
std::uint64_t dbg_latches_at_disarm = 0;
|
||
std::uint64_t dbg_seq_at_disarm = 0;
|
||
bool dbg_capturing_at_disarm = false;
|
||
int dbg_disarm_polls = 0;
|
||
int dbg_disarm_in_capture = 0;
|
||
/// The force probe's state: the button's stated state on both sides of the trigger toggle (a
|
||
/// width read a frame after the state it belongs to), and the two legs of its press. See
|
||
/// `force_probe()` in the probes' translation unit.
|
||
int dbg_force_off_sensitive = -1;
|
||
int dbg_force_off_visible = -1;
|
||
int dbg_force_off_width = -1; ///< its allocation with the trigger *off*
|
||
int dbg_force_on_sensitive = -1;
|
||
int dbg_force_on_width = -1; ///< ... and with the trigger on: the size the user can press
|
||
int dbg_force_arm_width = -1; ///< the Arm button beside it, so the pair's widths can be compared
|
||
int dbg_force_label_found = -1; ///< `find_labelled_button("Force")` *is* the panel's button
|
||
int dbg_force_leg = 0; ///< 1 while leg A is in flight, 2 for leg B
|
||
int dbg_force_polls = 0;
|
||
int dbg_force_capture_seen = 0; ///< the axis was read while the forced capture was filling
|
||
double dbg_force_capture_lo = 0.0; ///< that axis: the *requested* window, before the latch
|
||
double dbg_force_capture_hi = 0.0;
|
||
double dbg_force_want_pre = 0.0; ///< the request the engine will cut by, at the moment of the press
|
||
double dbg_force_want_post = 0.0;
|
||
std::uint64_t dbg_force_latches_before = 0;
|
||
int dbg_swaps = 0;
|
||
int dbg_axes_w_min = 1 << 20;
|
||
int dbg_axes_w_max = -(1 << 20);
|
||
int dbg_axes_w_prev = -1;
|
||
int dbg_axes_w_changes = 0;
|
||
/// What the pan probe asked for, so the frame after it can be compared against it: a capture
|
||
/// during the probe is not a value either side can recompute, and a capturing lambda cannot be
|
||
/// a `GSourceFunc`.
|
||
double dbg_pan_asked_lo = 0.0;
|
||
double dbg_pan_asked_hi = 0.0;
|
||
bool dbg_pan_asked = false;
|
||
/// Which gesture the probe is on: 1 = the pan was asked and its verdict is due, 2 = the
|
||
/// zoom-out was asked and its verdict is due.
|
||
int dbg_gesture_stage = 0;
|
||
/// A verdict the *tick* owes, one frame after a gesture was asked: 1 pan, 2 zoom-out, 3
|
||
/// zoom-in. Reported from the frame loop rather than a `g_timeout`, because a 300 ms timer
|
||
/// slips past the state it was meant to observe as soon as the frames are heavy -- which is
|
||
/// exactly the load this probe runs under.
|
||
int dbg_gesture_verdict = 0;
|
||
/// The event-zoom probe's remaining steps, counted down by the *tick* for the same reason
|
||
/// the gesture verdicts are: each step's reading has to be taken after the frame's pin and
|
||
/// clamp have run, or it reports the ask rather than the view.
|
||
int dbg_zoom_steps = 0;
|
||
int dbg_zoom_step_no = 0;
|
||
int dbg_axes_w_change_frame = -1;
|
||
int dbg_axes_w_change_side = -1;
|
||
float dbg_scale_min = 1e9f;
|
||
float dbg_scale_max = -1e9f;
|
||
int dbg_vp_w_min = 1 << 20;
|
||
int dbg_vp_w_max = -(1 << 20);
|
||
int dbg_xtick_min = 1 << 20;
|
||
int dbg_xtick_max = -(1 << 20);
|
||
int dbg_gutter_min = 1 << 20;
|
||
int dbg_gutter_max = -(1 << 20);
|
||
int dbg_side_w = 0;
|
||
int dbg_side_h = 0;
|
||
int dbg_signal_w = 0;
|
||
int dbg_signal_h = 0;
|
||
int dbg_signal_row_h = 0;
|
||
int dbg_trigger_w = 0;
|
||
int dbg_trigger_h = 0;
|
||
/// The signal list has rows (the dock has no tab to be "current" on any more).
|
||
int dbg_signal_page_current = 0;
|
||
/// The dock as the exit probe found it: the split view showing the sidebar, the column sensitive (it is
|
||
/// not until the stream has signals), its width, and how many `GtkScrolledWindow`s it holds directly.
|
||
int dbg_dock_shown = -1;
|
||
int dbg_dock_sensitive = -1;
|
||
/// The Cursors and Measurements pages' sensitivity (1 only if both are), read on the tick.
|
||
int dbg_pages_sensitive = -1;
|
||
int dbg_dock_w = -1;
|
||
int dbg_dock_scrollers = -1;
|
||
int dbg_trigger_sensitive = 0;
|
||
int dbg_trigger_items = 0;
|
||
int dbg_trigger_selected = -1;
|
||
/// The signal list's shape, captured where the list is built: how many rows it holds, and how many
|
||
/// of them are streamed signals. The `MATH` divider and the math rows account for the rest, and
|
||
/// `rows == signals + 1 + math` is the invariant the dump checks -- a list that lost its `+`.
|
||
int dbg_signal_rows = 0;
|
||
/// How many rows of the list carry a `GtkDragSource`: 0 -- the signal badges are the drag handles now.
|
||
int dbg_signal_drag_sources = -1;
|
||
/// A drag begun through the first signal badge's source closed the open slide-over (1), or did not (0);
|
||
/// -1 when there is no badge to drag.
|
||
int dbg_drag_closed_slideover = -1;
|
||
std::size_t dbg_signal_plain = 0;
|
||
/// The math channels among those rows, and the number of signals the page should be proposing (counted from
|
||
/// the model, not from the rows built), and whether the "every signal is acquired" label was on screen.
|
||
std::size_t dbg_signal_math_rows = 0;
|
||
std::size_t dbg_signal_addable = 0;
|
||
int dbg_signal_empty_label = -1;
|
||
int dbg_side_column_after_hide = 1;
|
||
std::string dbg_mode_triggered;
|
||
std::string dbg_window_text;
|
||
double dbg_marker_x = 0.0;
|
||
double dbg_marker_max = 0.0;
|
||
double dbg_trigger_time = 0.0;
|
||
int dbg_trigger_time_valid = -1;
|
||
std::string dbg_mode_live;
|
||
int dbg_trigger_sensitive_default = 0;
|
||
|
||
/// The detail string the popover and the exit dump read. The dump used to read the label,
|
||
/// which no longer carries these fields.
|
||
std::string status_detail;
|
||
std::size_t status_detail_max = 0;
|
||
/// The widest glance line this run produced. The bar sits above the plot, so this is the
|
||
/// "layout must not depend on content" reading for the status row.
|
||
std::size_t glance_max_chars = 0;
|
||
/// The last glance line, for the exit dump's `status bar:` line — the dump runs after the
|
||
/// window is destroyed, so it cannot ask the label.
|
||
std::string dbg_glance_last;
|
||
/// Which step of the menu probe runs next (`UDPSCOPE_PROBE_MENUS_MS`).
|
||
int dbg_menus_step = 0;
|
||
/// Which step of the capture probe runs next (`UDPSCOPE_PROBE_CAPTURE_MS`).
|
||
int dbg_capture_step = 0;
|
||
/// Which step of the event-cache probe runs next (`UDPSCOPE_PROBE_EVENTS_MS`), and the bounded wait
|
||
/// it keeps for a second latch (it is counting retries, not time).
|
||
int dbg_events_step = 0;
|
||
int dbg_events_wait = 0;
|
||
/// The plot area's allocated width before the first event was latched (the events probe), compared with
|
||
/// its width after: the events list is a slide-over page and no longer a column beside the plot.
|
||
int dbg_events_plot_w = 0;
|
||
/// The event row the probe selected before it activated one, and how many rows were selected.
|
||
/// Both are read back *after* the activation: a row pointer that is no longer the list's row 0,
|
||
/// or a smaller count, says the activation rebuilt the list and so threw away the user's scroll
|
||
/// position and their comparison selection.
|
||
GtkListBoxRow* dbg_events_row0 = nullptr;
|
||
int dbg_events_selected = 0;
|
||
/// What the *live* view was set to just before the probe opened an event: the time window, the
|
||
/// trigger's mode, its channel (by name, the only stable identity) and its level. Read back after
|
||
/// each way out of a file view, because a file view overwrites all four -- the window is pinned to
|
||
/// the event's own span and the trigger is switched off, since a file has no edge to wait for --
|
||
/// and going live has to put them back. The probe keeps its own expectation rather than asking
|
||
/// `App`: a reading taken from the mechanism under test asserts nothing.
|
||
double dbg_live_window = 0.0;
|
||
bool dbg_live_trigger_on = false;
|
||
std::string dbg_live_trigger_channel;
|
||
double dbg_live_level = 0.0;
|
||
/// Which step of the plot-context-menu probe runs next (`UDPSCOPE_PROBE_ROWMENU_MS`), and the
|
||
/// popover it is measuring.
|
||
int dbg_rowmenu_step = 0;
|
||
GtkWidget* row_menu_popover = nullptr;
|
||
|
||
GtkWidget* status_info_button = nullptr; ///< the ⓘ
|
||
/// The packet-capture toggle, in the detail popover. `toggled` sets the producer's
|
||
/// atomic and nothing else; the tick keeps it honest about connection and failures.
|
||
GtkWidget* capture_button = nullptr;
|
||
/// What the toggle was last set to from this thread, so a producer-side failure
|
||
/// (a full disk) can return the button to off without re-entering its handler.
|
||
bool capture_want_ui = false;
|
||
GtkWidget* status_popover = nullptr;
|
||
/// The popover's thirteen labels: five row headings and eight value lines. Built once; the
|
||
/// tick sets their text. A rebuild here would be a per-frame relayout of a visible widget.
|
||
GtkWidget* popover_label[13] = {};
|
||
int popover_rebuilds = 0;
|
||
};
|
||
struct ConfigDialog {
|
||
App* app = nullptr;
|
||
GtkWidget* window = nullptr;
|
||
GtkWidget* list = nullptr;
|
||
GtkWidget* name = nullptr;
|
||
GtkWidget* comment = nullptr;
|
||
};
|
||
struct WindowUnit {
|
||
const char* name;
|
||
double seconds;
|
||
};
|
||
enum class BadgePhase { Lit = 0, Dim = 1, Idle = 2, Connecting = 3 };
|
||
|
||
struct CalibContext {
|
||
App* app;
|
||
std::size_t signal;
|
||
GtkWidget* gain;
|
||
GtkWidget* offset;
|
||
GtkWidget* window;
|
||
};
|
||
struct DragContext {
|
||
App* app;
|
||
std::size_t signal;
|
||
};
|
||
struct ColorPick {
|
||
App* app;
|
||
std::string signal;
|
||
};
|
||
class MeasureWindowReader {
|
||
public:
|
||
/// `window` is the x range to read, **in the coordinates the axes read** -- the viewport for a
|
||
/// live view, the pin for a pinned one -- and it is handed in rather than read off the plot
|
||
/// here, because *which* window a measurement describes is a decision (`update_measures()`)
|
||
/// that also has to be booked: the pane compares the window it last measured against this one
|
||
/// to decide whether a recompute is owed, and a reader that chose its own would leave the two
|
||
/// disagreeing -- a pinned view recomputing on every gesture frame for a number that cannot
|
||
/// change.
|
||
MeasureWindowReader(App& app, std::size_t channel, plot::Range window) {
|
||
const RowSet::Row& r0 = app.rows->row(0);
|
||
if (r0.plot == nullptr) return;
|
||
if ((snap_ = app.model->display_snapshot()) != nullptr) {
|
||
if (channel >= snap_->values.size()) return;
|
||
const std::vector<f64>& st = snap_->stamps;
|
||
if (snap_->values[channel].size() != st.size()) return;
|
||
const auto lo = std::lower_bound(st.begin(), st.end(), window.min);
|
||
const auto hi = std::upper_bound(st.begin(), st.end(), window.max);
|
||
if (hi <= lo) return;
|
||
off_ = static_cast<std::size_t>(lo - st.begin());
|
||
count_ = static_cast<std::size_t>(hi - lo);
|
||
snap_vals_ = &snap_->values[channel];
|
||
// The snapshot's pyramid is built from sample 0 and nothing is ever dropped from it, so
|
||
// its absolute index space *is* the snapshot's.
|
||
if (channel < snap_->pyramids.size()) pyr_ = &snap_->pyramids[channel];
|
||
origin_ = off_;
|
||
return;
|
||
}
|
||
lock_ = std::unique_lock<std::mutex>(app.model->mutex());
|
||
if (channel >= app.model->channel_count()) return;
|
||
epoch_ = app.model->display_epoch();
|
||
u64 first = 0;
|
||
u64 last = 0;
|
||
app.model->live_window_locked(window.min - epoch_, window.max - epoch_, first, last);
|
||
if (last <= first) return;
|
||
store_ = &app.model->live();
|
||
channel_ = channel;
|
||
first_ = first;
|
||
count_ = static_cast<std::size_t>(last - first);
|
||
// The channel's own pyramid, in the value ring's absolute index space -- the same space
|
||
// `live_window_locked()` answers in, since a channel's stride is 1. It is current as of this
|
||
// frame's `acquire_all()`; the samples produced since are past `window_end()` and are read
|
||
// raw, which bounds that tail at one frame of production.
|
||
pyr_ = &store_->channel(channel).pyramid();
|
||
origin_ = first;
|
||
}
|
||
|
||
std::size_t size() const noexcept { return count_; }
|
||
|
||
/// The whole level-0 LOD buckets lying inside this window, as absolute bucket indices
|
||
/// `[first, last)`. False when the window has none -- a window shorter than a bucket, or one
|
||
/// the pyramid does not reach.
|
||
bool bucket_range(u64& first, u64& last) const {
|
||
if (pyr_ == nullptr || count_ == 0) return false;
|
||
const u64 begin = std::max<u64>(origin_, pyr_->window_base());
|
||
const u64 end = std::min<u64>(origin_ + count_, pyr_->window_end());
|
||
if (end <= begin) return false;
|
||
const u64 stored = pyr_->level_base(0);
|
||
first = std::max<u64>(stored, (begin + plot::data::kBucket - 1) / plot::data::kBucket);
|
||
last = std::min<u64>(stored + pyr_->level_size(0), end / plot::data::kBucket);
|
||
return last > first;
|
||
}
|
||
|
||
/// Window offset of absolute bucket `k`'s first sample.
|
||
std::size_t bucket_offset(u64 k) const noexcept {
|
||
return static_cast<std::size_t>(k * plot::data::kBucket - origin_);
|
||
}
|
||
|
||
MeasureBucket bucket(u64 k) const {
|
||
const plot::data::MinMax mm = pyr_->bucket(0, k);
|
||
MeasureBucket out;
|
||
out.lo = static_cast<double>(mm.min);
|
||
out.hi = static_cast<double>(mm.max);
|
||
out.sum = pyr_->bucket_sum(0, k);
|
||
out.count = static_cast<std::size_t>(pyr_->bucket_count(0, k));
|
||
out.offset = bucket_offset(k);
|
||
out.span = plot::data::kBucket;
|
||
return out;
|
||
}
|
||
|
||
/// `n` values from window offset `at`. The pointer is valid until the next call.
|
||
const f32* values(std::size_t at, std::size_t n) {
|
||
if (snap_vals_ != nullptr) return snap_vals_->data() + off_ + at;
|
||
if (scratch_.size() < n) scratch_.resize(n);
|
||
store_->values(channel_).copy_from(first_ + at, n, scratch_.data());
|
||
return scratch_.data();
|
||
}
|
||
|
||
/// Window sample `i`'s stream time. One sample at a time on purpose: see the class comment.
|
||
double time_of(std::size_t i) const {
|
||
if (snap_vals_ != nullptr) return snap_->stamps[off_ + i];
|
||
f64 stamp = 0.0;
|
||
store_->time().copy_from(first_ + i, 1, &stamp);
|
||
return epoch_ + stamp;
|
||
}
|
||
|
||
private:
|
||
std::shared_ptr<const EventSnapshot> snap_;
|
||
const std::vector<f32>* snap_vals_ = nullptr;
|
||
std::size_t off_ = 0;
|
||
std::unique_lock<std::mutex> lock_;
|
||
BufferStore* store_ = nullptr;
|
||
std::size_t channel_ = 0;
|
||
u64 first_ = 0;
|
||
std::size_t count_ = 0;
|
||
f64 epoch_ = 0.0;
|
||
const plot::data::MinMaxPyramid* pyr_ = nullptr;
|
||
/// Absolute sample index of window offset 0, so a bucket's absolute span becomes an offset.
|
||
u64 origin_ = 0;
|
||
std::vector<f32> scratch_;
|
||
};
|
||
struct MeasureDialog {
|
||
App* app = nullptr;
|
||
int edit = -1;
|
||
GtkWidget* signal_drop = nullptr;
|
||
GtkWidget* type_drop = nullptr;
|
||
GtkWidget* threshold = nullptr;
|
||
/// `Between`'s *other* edge: the same unit as `threshold`, shown only for the one type that reads
|
||
/// two shares (see `measure_uses_band()`).
|
||
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,
|
||
/// and the difference has to be in words, not just in visibility.
|
||
GtkWidget* threshold_row = nullptr;
|
||
GtkWidget* threshold_label = nullptr;
|
||
GtkWidget* band_row = nullptr;
|
||
GtkWidget* band_label = nullptr;
|
||
GtkWidget* side_row = nullptr;
|
||
GtkWidget* side_label = nullptr;
|
||
/// The two noise-rejection rows. `noise` is a percentage of the window's peak-to-peak, `hf` a corner in
|
||
/// Hz with 0 off; both are shown only where they do something (see `measure_dialog_sync`).
|
||
GtkWidget* noise = nullptr;
|
||
GtkWidget* noise_row = nullptr;
|
||
GtkWidget* hf = nullptr;
|
||
GtkWidget* hf_row = nullptr;
|
||
/// The names the signal dropdown was built from, in order. `apply` resolves an index
|
||
/// through *this*, not through the model's signal list: the dropdown lists the plotted
|
||
/// traces, so index `i` here is the i-th trace -- indexing the CONFIG with it selected
|
||
/// whatever signal happened to sit at that position there (the stream's timestamp, for the
|
||
/// first entry, which is what picking the first trace used to store).
|
||
std::vector<std::string> signals;
|
||
};
|
||
struct MathWireRef {
|
||
int node = -1;
|
||
int which = 0;
|
||
};
|
||
enum class MathDrag { None, Node, Wire, Pan };
|
||
|
||
struct MathDialog {
|
||
App* app = nullptr;
|
||
int edit = -1;
|
||
std::vector<MathNode> nodes;
|
||
int output = 0;
|
||
int selected = 0;
|
||
/// What a drag is doing, decided once at `drag-begin` from what is under the press and not
|
||
/// revisited: a gesture that changes its mind mid-drag is unusable. `Wire` carries a loose end
|
||
/// in graph units, which the draw shows so the user can see what is being connected.
|
||
MathDrag drag_mode = MathDrag::None;
|
||
int wire_from = -1;
|
||
int wire_detach = -1;
|
||
int wire_detach_which = 0;
|
||
double wire_x = 0.0;
|
||
double wire_y = 0.0;
|
||
double pan_x0 = 0.0;
|
||
double pan_y0 = 0.0;
|
||
/// `Wire`: the press point in canvas coordinates. The gesture's `dx`/`dy` is relative to this
|
||
/// point, so `press_cx + dx` is always the pointer, even on a detach where `wire_from` is a
|
||
/// different node from the one the pointer touched.
|
||
double press_cx = 0.0;
|
||
double press_cy = 0.0;
|
||
/// Where the box being dragged *was* when the drag began, and which box it is. The gesture
|
||
/// reports the total offset from its own start point, so adding it to a value that the previous
|
||
/// update already moved is a runaway: the box accelerates away from the pointer and leaves the
|
||
/// canvas. `-1` means the drag did not begin on a box.
|
||
double grab_x = 0.0;
|
||
double grab_y = 0.0;
|
||
int drag_node = -1;
|
||
/// The wire `Delete` would remove, or `{-1, 0}`.
|
||
MathWireRef selected_wire{-1, 0};
|
||
/// The view: canvas pixels per graph unit, and where graph (0,0) is drawn. Applied once to the
|
||
/// cairo context by `math_canvas_draw()`; every hit test runs the inverse (`math_to_graph()`)
|
||
/// rather than comparing a pointer position against a graph one, and sizes its tolerance in
|
||
/// *screen* pixels (`kPort / view_scale`) -- a tolerance left in graph units shrinks with the
|
||
/// view and makes a port unhittable zoomed out.
|
||
double view_scale = 1.0;
|
||
double view_x = 0.0;
|
||
double view_y = 0.0;
|
||
GtkWidget* window = nullptr;
|
||
GtkWidget* canvas = nullptr;
|
||
GtkWidget* name = nullptr;
|
||
GtkWidget* unit = nullptr;
|
||
GtkWidget* op_drop = nullptr;
|
||
GtkWidget* source_drop = nullptr;
|
||
GtkWidget* gain = nullptr;
|
||
GtkWidget* offset = nullptr;
|
||
GtkWidget* fc = nullptr;
|
||
GtkWidget* q = nullptr;
|
||
GtkWidget* med = nullptr; ///< MathNode::med_n
|
||
GtkWidget* lo = nullptr;
|
||
GtkWidget* hi = nullptr;
|
||
GtkWidget* rows[7] = {nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr};
|
||
GtkWidget* inputs = nullptr;
|
||
/// The validation line and the button it governs. Both are set from `math_dialog_sync()` and from
|
||
/// nowhere else, so what the line says and what the button allows cannot disagree.
|
||
GtkWidget* problem = nullptr;
|
||
GtkWidget* apply = nullptr;
|
||
/// Why the last gesture was *refused* -- "a node cannot read itself", or a cycle. A refusal has no
|
||
/// standing state to be read back from: it is a thing that did not happen, so `math_dialog_problem()`
|
||
/// cannot describe it and a refused wire otherwise just fails to appear, which reads as the editor
|
||
/// being broken. `math_dialog_sync()` shows this in preference to the graph's own problem and
|
||
/// **consumes it as it writes**, so the next sync -- and every mutation syncs -- replaces it with
|
||
/// the graph's state. `Apply`'s sensitivity still follows `math_dialog_problem()` alone: a refused
|
||
/// wire changed nothing, so a graph that was saveable still is.
|
||
std::string refusal;
|
||
/// True while the form is being written. `gtk_drop_down_set_selected()` emits `notify::selected`
|
||
/// for a programmatic set exactly as for a user one, and the op handler answers a notify by
|
||
/// syncing -- so writing the form **re-entered** `math_dialog_sync()`, and the inner call rewrote
|
||
/// the validation label the outer one had just written. That is how a refusal vanished in the same
|
||
/// call that put it up (the probe's `wire: self through drag-end` read an empty line). A
|
||
/// programmatic set is not a user edit.
|
||
bool syncing = false;
|
||
};
|
||
struct ExportContext {
|
||
App* app = nullptr;
|
||
ExportTable table;
|
||
};
|
||
struct FixedRangeContext {
|
||
App* app;
|
||
/// The row set the range applies to: `nullptr` is the main window's. A scope window's own
|
||
/// dialog writes its window's rows.
|
||
RowSet* rows = nullptr;
|
||
int row;
|
||
GtkWidget* lo;
|
||
GtkWidget* hi;
|
||
GtkWidget* window;
|
||
};
|
||
struct RemoveTarget {
|
||
static constexpr int kAllSlots = -1;
|
||
App* app;
|
||
int row;
|
||
int slot;
|
||
};
|
||
enum class RowMenuView { Main, Remove };
|
||
|
||
struct RowMenuState {
|
||
App* app = nullptr;
|
||
int row = 0;
|
||
GtkWidget* popover = nullptr;
|
||
GtkWidget* box = nullptr;
|
||
RowMenuView view = RowMenuView::Main;
|
||
};
|
||
struct EventSamples {
|
||
std::vector<std::string> names;
|
||
std::vector<f64> times;
|
||
std::vector<std::vector<f32>> columns; ///< [column][row]
|
||
};
|
||
using EventThumbCache = std::unordered_map<std::string, GdkTexture*>;
|
||
struct ConditionProbe {
|
||
double timeout = 0.300;
|
||
/// 0 = pulse width `<` (the glitch), 1 = pulse width `>`, 2 = window, 3 = dropout. Selected
|
||
/// by `UDPSCOPE_PROBE_CONDITION_SHAPE`; the default keeps Task 3's glitch leg.
|
||
int shape = 0;
|
||
int waited = 0;
|
||
};
|
||
struct RowMenuProbeState {
|
||
int main_rows = 0;
|
||
int main_bad = 0;
|
||
int remove_rows = 0;
|
||
int remove_bad = 0;
|
||
int back_rows = 0;
|
||
int back_bad = 0;
|
||
int second_rows = 0;
|
||
int second_bad = 0;
|
||
int remove_row_in_main = 0;
|
||
int remove_row_gone = 0;
|
||
int can_open_again = 0;
|
||
};
|
||
|
||
constexpr int kTickMs = 16;
|
||
/// How often a frozen view is re-drawn anyway, in ticks: the render gate's floor (see `tick()`
|
||
/// and `App::render_skipped`). Four is ~15 Hz, so anything the gate's signature does not name --
|
||
/// a recolour, a measurement's mark, a y range a fit changed -- reaches the screen within ~64 ms
|
||
/// of a still picture, which is below noticing. It is deliberately a small number: the gate's
|
||
/// purpose is to stop spending a GPU on an unchanging frame, not to shave the last frame off.
|
||
constexpr int kStaticRenderEvery = 4;
|
||
/// How many frames one change to the gate's signature is worth (see `tick()` and
|
||
/// `App::render_open`). More than one because the figure lays itself out *per render* and its left
|
||
/// gutter is a monotone floor that takes two passes to converge (the `layout over N frames`
|
||
/// reading): a re-framed frozen view drawn once would sit a pass short of its final gutter until
|
||
/// the floor above fired. Three leaves a pass in hand without being a frame budget.
|
||
constexpr int kSettleRenders = 3;
|
||
/// Below this window width the sidebar has no room beside the plot, and the split view overlays it
|
||
/// instead of squeezing it. See `tick()`.
|
||
constexpr int kSidebarRoom = 900;
|
||
constexpr int kWindowUnitCount = 3;
|
||
constexpr double kWindowMinS = 1.0e-6;
|
||
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()`
|
||
/// places them differently -- one boundary, two controls.
|
||
constexpr const char* kLayoutResizeKey = "udpscope-layout-resize";
|
||
/// Cell `+` buttons carry this key (fr * 8 + pos + 1) so `layout_button()` never matches them.
|
||
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`
|
||
/// (`place_cells()`): a cell is `floor + extra * weight / total`. Both it and `kMinCellPx` are
|
||
/// *logical* px -- the plotter scales its floor by the `ui_scale` the renderer installs, which is
|
||
/// `PlotArea::device_scale()`, the same divisor that turns a layout rect into logical px here.
|
||
constexpr double kPlotterMinCellPx = 64.0;
|
||
constexpr gint64 kMeasureRefreshUs = 100 * 1000;
|
||
constexpr gint64 kMeasureHeavyUs = 5 * 1000;
|
||
constexpr const char* kCursorModeNames[] = {"vertical", "horizontal", "both", "mix"};
|
||
constexpr double kNodeW = 132.0;
|
||
constexpr double kNodeH = 46.0;
|
||
constexpr double kPort = 10.0;
|
||
constexpr int kPortOut = -1;
|
||
constexpr double kMathScaleMin = 0.25;
|
||
constexpr double kMathScaleMax = 4.0;
|
||
constexpr double kMathZoomStep = 1.1;
|
||
constexpr double kWirePick = 6.0;
|
||
constexpr int kZoomGestureSteps = 8;
|
||
constexpr int kZoomWindowSteps = 8;
|
||
constexpr int kEventThumbW = 138;
|
||
/// The row thumbnail's height floor and ceiling. A floor and not a fixed size: the stored preview is
|
||
/// `44 px` per signal, so its proportions depend on how many signals the event has, and a row shows
|
||
/// it at *its* aspect -- a one-signal event is a short row and a six-signal one a tall one. The
|
||
/// ceiling is what keeps a wide event from making a row taller than the pane; the full picture is a
|
||
/// hover away.
|
||
constexpr int kEventThumbH = 46;
|
||
constexpr int kEventThumbMaxH = 104;
|
||
inline int g_stats_phase = 0;
|
||
inline std::vector<std::uint64_t> g_stats_n_before;
|
||
/// The same for the dock-section probe: the accumulator counts recorded while the `measure` page was
|
||
/// still open, so the report can say whether any of them advanced after it was closed.
|
||
inline std::vector<std::uint64_t> g_dock_section_n_before;
|
||
inline double g_trigrenum_edge = 0.0;
|
||
inline std::uint64_t g_trigrenum_latches = 0;
|
||
inline ConditionProbe g_condition;
|
||
inline double g_search_t0 = 0.0;
|
||
constexpr int kMenuRowMaxPx = 30;
|
||
inline RowMenuProbeState g_rowmenu_probe;
|
||
|
||
void install_css();
|
||
void on_menu_row_toggled(GtkToggleButton* button, gpointer);
|
||
GtkWidget* menu_row(const char* label, const char* accel, bool with_check, GtkWidget** out_check);
|
||
GtkWidget* menu_button(const char* label, const char* accel);
|
||
GtkWidget* menu_toggle(const char* label, const char* accel, bool active);
|
||
GtkWidget* menu_separator();
|
||
GtkWidget* labeled_box(const char* label, GtkWidget* child);
|
||
/// Loads a UI file into the window's builders and returns the builder, so the caller can resolve the
|
||
/// objects it needs by id. See `ui_widgets.cpp`.
|
||
GtkBuilder* ui_load(App& app, const char* resource);
|
||
LayoutState collect_layout(App& app);
|
||
void on_config_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user);
|
||
void on_config_forget_clicked(GtkButton* button, gpointer user);
|
||
void on_config_save_as(GtkButton*, gpointer user);
|
||
void config_dialog_refresh(ConfigDialog& d);
|
||
void open_configurations(App& app);
|
||
void save_layout_now(App& app);
|
||
|
||
/// Capability panels (`src/ui_source.cpp`). `build_source_panels` adds the source kind's panels as
|
||
/// pages of the slide-over (`source-<id>`), once, before the first tick. `apply_source_panels` sets every panel's
|
||
/// sensitivity from `connected` -- disabled, never hidden.
|
||
/// `sync_source_panels` is the per-tick call: it applies the model's own connection flag.
|
||
void build_source_panels(App& app);
|
||
void apply_source_panels(App& app, bool connected);
|
||
void sync_source_panels(App& app);
|
||
bool save_configuration_as(App& app, const std::string& name, const std::string& comment);
|
||
gboolean layout_save_tick(gpointer user);
|
||
void schedule_layout_save(App& app);
|
||
void apply_layout(App& app, const LayoutState& st);
|
||
GtkWidget* labeled_box_fixed(const char* label, GtkWidget* child);
|
||
GtkWidget* spin(double min, double max, double step, int digits, double value);
|
||
GtkWidget* dialog_window(GtkWidget* parent, const char* title);
|
||
void dialog_add_button(GtkWidget* window, GtkWidget* button);
|
||
void on_popover_closed(GtkPopover* popover, gpointer);
|
||
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);
|
||
int row_key(GtkWidget* widget);
|
||
int row_at(const App& app, double x, double y);
|
||
void cancel_drag_on(App& app, int row);
|
||
void report_wide_widgets(GtkWidget* root, int threshold, int depth);
|
||
const char* display_mode_name(DisplayMode m);
|
||
void check_frame_ink(App& app);
|
||
int on_render_probe(GtkGLArea*, GdkGLContext*, gpointer user);
|
||
void on_before_paint(GdkFrameClock*, gpointer user);
|
||
void on_after_paint(GdkFrameClock*, gpointer user);
|
||
GtkWidget* make_compare_area(App& app);
|
||
GtkWidget* make_area(App& app);
|
||
void relayout_areas(App& app);
|
||
void clear_layout_editor(App& app);
|
||
void on_layout_row_remove(GtkButton* button, gpointer user);
|
||
void on_layout_row_insert(GtkButton* button, gpointer user);
|
||
void sync_layout_editor(App& app);
|
||
void rebuild_layout_editor(App& app);
|
||
void on_layout_editor_done(GtkButton*, gpointer user);
|
||
void on_layout_editor_toggled(GtkToggleButton* button, gpointer user);
|
||
void on_signal_switch(GObject* obj, GParamSpec*, gpointer user);
|
||
void on_arm_clicked(GtkButton* button, gpointer user);
|
||
TriggerType trigger_type_at(guint i);
|
||
WidthCmp width_cmp_at(guint i);
|
||
void publish_trigger_fields(App* app);
|
||
void sync_trigger_rows(App& app);
|
||
void on_trigger_field_changed(GtkWidget*, gpointer user);
|
||
void on_trigger_drop_changed(GObject*, GParamSpec*, gpointer user);
|
||
void on_trigger_shape_changed(GObject*, GParamSpec*, gpointer user);
|
||
void on_trigger_signal_changed(GObject* obj, GParamSpec*, gpointer user);
|
||
void show_info_dialog(GtkWindow* parent, const SignalInfo& sig);
|
||
void on_info_clicked(GtkButton* button, gpointer user);
|
||
void on_calibrate_apply(GtkButton*, gpointer user);
|
||
/// Which of the calibration's two numbers a dialog edits; `Both` is the full dialog.
|
||
enum class CalibField { Both, Gain, Offset };
|
||
void open_calibration(App& app, GtkWindow* parent, std::size_t signal, CalibField field = CalibField::Both);
|
||
/// The one-entry editor for a streamed signal's unit (an empty entry restores the declared one).
|
||
void open_unit_editor(App& app, GtkWindow* parent, std::size_t signal);
|
||
GdkContentProvider* on_drag_prepare(GtkDragSource*, double, double, gpointer user);
|
||
int signal_color_index(const std::vector<SignalInfo>& signals, std::size_t signal);
|
||
std::string color_hex(plot::Color c);
|
||
std::string chip_markup(plot::Color c);
|
||
bool signal_plotted(App& app, const std::string& name);
|
||
std::string signal_chip_markup(App& app, const std::vector<SignalInfo>& signals, std::size_t signal);
|
||
void on_signal_color_chosen(GObject* source, GAsyncResult* res, gpointer user);
|
||
void open_signal_color(App& app, std::size_t signal);
|
||
void rebuild_signal_list(App& app, const std::vector<SignalInfo>& signals);
|
||
/// Every plottable signal once (the time base skipped, a group's layers folded to the first), in model order.
|
||
/// The one rule the list and the badges share.
|
||
std::vector<std::size_t> listed_signals(const std::vector<SignalInfo>& signals);
|
||
/// Whether more signals are acquired than `kMaxSignalBadges`: the badge bar then shows one "N signals" badge,
|
||
/// and the `signals` page lists the acquired signals as well, since their rows are the only way to a page.
|
||
/// The one rule the bar and the page share.
|
||
bool signal_badges_overflowed(App& app, const std::vector<SignalInfo>& signals);
|
||
/// Puts `signal` (an index into `signals()`) on plot `row` in the first free slot, switching it on first if
|
||
/// need be. Says why not through `set_notice` and returns false when it cannot. The one path a drop and a
|
||
/// signal page's "Plot on" button share.
|
||
bool assign_signal_to_row(App& app, std::size_t signal, int row);
|
||
/// Back to the palette colour for the signal's position.
|
||
void reset_signal_color(App& app, std::size_t signal);
|
||
/// Holds `widget` alive across the current emission: a handler that rebuilds the list or a page destroys the
|
||
/// widget it was emitted from.
|
||
void outlive_the_rebuild(GtkWidget* widget);
|
||
/// Creates the page `signal:<name>` if it is missing, fills it, and returns whether it exists afterwards.
|
||
bool signal_page_ensure(App& app, const std::string& name);
|
||
/// Called at the end of `rebuild_signal_panels`: refills every existing signal page, drops the pages of
|
||
/// signals the model no longer has (closing the slide-over first if one of them is open).
|
||
void signal_pages_sync(App& app);
|
||
gboolean signal_page_probe(gpointer user); // UDPSCOPE_PROBE_SIGNAL_PAGES_MS
|
||
/// The `MATH` divider row in the one channel list, and the `+` it carries.
|
||
GtkWidget* make_math_divider(App& app);
|
||
void on_math_add_clicked(GtkButton* button, gpointer user);
|
||
/// The index in `App::math` of the channel a synthetic signal is named after, or `-1`.
|
||
int math_index_for_signal(App& app, const std::string& name);
|
||
void on_math_edit_clicked(GtkButton* button, gpointer user);
|
||
void on_math_delete_clicked(GtkButton* button, gpointer user);
|
||
void rebuild_trigger_signals(App& app);
|
||
double window_limit(const App& app);
|
||
void sync_window_display(App& app, double seconds);
|
||
void reset_measure_stats(App& app);
|
||
void apply_time_window(App& app, double seconds);
|
||
void on_window_entry_activate(GtkEntry* entry, gpointer user);
|
||
void on_window_unit_changed(GObject* obj, GParamSpec*, gpointer user);
|
||
void on_window_step(GtkButton* button, gpointer user);
|
||
void refresh_window_limits(App& app);
|
||
void on_pause_toggled(GtkToggleButton* button, gpointer user);
|
||
void search_jump(App& app, double t);
|
||
void update_search_panel(App& app);
|
||
void sync_events_availability(App& app);
|
||
/// The preferences dialog: one window, opened from the overflow menu. `apply_theme()` is separate
|
||
/// because the theme is the one preference that takes effect without a restart.
|
||
void open_preferences(App& app);
|
||
gboolean prefs_probe(gpointer user);
|
||
gboolean sidebar_probe(gpointer user);
|
||
gboolean boxzoom_probe(gpointer user);
|
||
/// A labelled row whose control keeps its natural width -- what a `GtkSwitch` needs and what
|
||
/// `labeled_box()`'s expanding child denies it.
|
||
GtkWidget* labeled_switch_row(const char* label, GtkWidget* control);
|
||
/// The layout editor's boundary drag: the press starts it, movement sizes the two rows it separates.
|
||
void on_layout_drag_begin(GtkGestureDrag* gesture, double x, double y, gpointer user);
|
||
void on_layout_drag_update(GtkGestureDrag* gesture, double offset_x, double offset_y, gpointer user);
|
||
void on_layout_drag_end(GtkGestureDrag* gesture, double offset_x, double offset_y, gpointer user);
|
||
void on_trigger_toggled(GtkToggleButton* button, gpointer user);
|
||
plot::u32 measure_color(std::size_t index);
|
||
/// One line naming what a measurement's type is measuring, with the shares it reads them at: the
|
||
/// pane's row tooltip. The row's own label is the type alone (as for every type), so this is where
|
||
/// a user sees the share an `above`/`below`/`between` row is averaging over.
|
||
std::string measure_row_summary(const Measure& m);
|
||
bool visible_samples(App& app, std::size_t channel, std::vector<double>& time, std::vector<double>& value);
|
||
int measure_channel(App& app, const std::string& signal);
|
||
int measure_row(App& app, const std::string& signal);
|
||
std::string measure_unit(App& app, const std::string& signal);
|
||
GtkWidget* measures_row(App& app, std::size_t index, const Measure& m);
|
||
void rebuild_measures_pane(App& app);
|
||
void on_cursor_mode_changed(GObject* obj, GParamSpec*, gpointer user);
|
||
App::CursorLine cursors_line(App& app, const char* who, plot::Color chip, bool has_chip);
|
||
GtkWidget* cursors_header(App& app);
|
||
/// The compact readouts at the top of the dock (`src/ui_dock.cpp`). The strings are the pages' own: the
|
||
/// pages' writers call the `dock_*` setters from the same variable, so a number is formatted once.
|
||
GtkWidget* build_readouts_dock(App& app);
|
||
void dock_rebuild_cursors(App& app);
|
||
void dock_rebuild_measures(App& app);
|
||
void dock_cursor_cell(App& app, std::size_t line, const std::string& a, const std::string& b,
|
||
const std::string& d);
|
||
void dock_measure_value(App& app, std::size_t i, const std::string& text);
|
||
/// The readouts strip under the plot (`src/ui_dock.cpp`): the third reader of the same strings. It is
|
||
/// filled by the `dock_*` rebuilds and written by the `dock_*` setters above (so nothing here formats),
|
||
/// and shown whenever the dock is not beside the plot (`!show-sidebar || collapsed`, a collapsed dock being
|
||
/// an overlay on it) and there is something to read (cursors on or a measurement; each group follows its own
|
||
/// feature, and with neither the strip is hidden): `sync_readouts_strip()` is called by the tick after the show-sidebar reconciliation
|
||
/// and before `set_collapsed()`, so it reads the previous frame's `collapsed`. `dock_set_cursors_sensitive()` is the one writer of the cursor readouts'
|
||
/// sensitivity, for the dock's grid and the strip's chips together.
|
||
void sync_readouts_strip(App& app);
|
||
void dock_set_cursors_sensitive(App& app, bool on);
|
||
/// The Events block under the readouts (`src/ui_dock.cpp`): compact rows (marker, title, time; no
|
||
/// thumbnails) from `event_store->list()`, newest first, and a Manage button for the `events` page.
|
||
/// `dock_rebuild_events()` follows every change of what is *cached*, `dock_mark_events()` every change
|
||
/// of what is *on screen* (in place, no rebuild), `dock_events_set_sensitive()` is the one writer of the
|
||
/// block's sensitivity. All three are no-ops before the block is built.
|
||
GtkWidget* build_events_dock(App& app);
|
||
/// The dock shows at most this many events (the newest), then one dim "... N more, see Manage" row.
|
||
inline constexpr std::size_t kDockEventsMax = 24;
|
||
/// The first lists the store itself; the second takes a list the caller already holds (so one refresh
|
||
/// reads the cache directory once, not twice). Both keep the list's vertical scroll across the rebuild.
|
||
void dock_rebuild_events(App& app);
|
||
void dock_rebuild_events(App& app, const std::vector<EventRecord>& records);
|
||
void dock_mark_events(App& app);
|
||
void dock_events_set_sensitive(App& app, bool available);
|
||
/// A row's first line, and the two questions its mark is made of (`ui_events.cpp`): the one function
|
||
/// for the pane and the dock, so the two cannot disagree about which event is on screen.
|
||
std::string event_row_title(App& app, const EventRecord& r);
|
||
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);
|
||
void rebuild_cursors_pane(App& app);
|
||
void update_cursors_pane(App& app);
|
||
GtkWidget* labelled_row(const char* label, GtkWidget* child, GtkWidget** label_out);
|
||
void measure_dialog_sync(MeasureDialog* d);
|
||
void on_measure_type_changed(GObject* obj, GParamSpec*, gpointer user);
|
||
void on_measure_dialog_apply(GtkButton*, gpointer user);
|
||
void on_measure_dialog_delete(GtkButton*, gpointer user);
|
||
void open_measure_dialog(App& app, int edit_index);
|
||
void place_measure_marks(App& app);
|
||
void place_search_marks(App& app);
|
||
void update_measures(App& app, bool force, bool accumulate);
|
||
void apply_math_channels(App& app);
|
||
|
||
/// Re-reads the file a file view is showing after a structural change rebuilt the buffer and dropped
|
||
/// its history -- see the definition in ui.cpp. True when something was reloaded.
|
||
bool reload_file_view(App& app);
|
||
void add_math_channel(App& app);
|
||
std::vector<std::string> math_source_names(App& app);
|
||
std::string math_dialog_problem(const MathDialog* d);
|
||
GdkRectangle math_node_rect(const MathNode& n);
|
||
void math_input_pos(const MathNode& n, int which, double& x, double& y);
|
||
void math_output_pos(const MathNode& n, double& x, double& y);
|
||
int math_node_at(const MathDialog* d, double x, double y);
|
||
int math_port_at(const MathDialog* d, double x, double y, double tol, int& which);
|
||
void math_to_graph(const MathDialog* d, double cx, double cy, double& gx, double& gy);
|
||
double math_set_scale(MathDialog* d, double want);
|
||
void math_zoom_at(MathDialog* d, double px, double py, double dy);
|
||
void math_view_reset(MathDialog* d);
|
||
void math_view_fit(MathDialog* d);
|
||
void math_tidy(MathDialog* d);
|
||
int math_port_at_canvas(const MathDialog* d, double cx, double cy, int& which);
|
||
int math_node_at_canvas(const MathDialog* d, double cx, double cy);
|
||
double math_seg_dist2(double px, double py, double ax, double ay, double bx, double by);
|
||
MathWireRef math_wire_at(const MathDialog* d, double gx, double gy, double tol);
|
||
MathWireRef math_wire_at_canvas(const MathDialog* d, double cx, double cy);
|
||
bool math_connect(MathDialog* d, int from, int to, int which, std::string* why);
|
||
void math_canvas_draw(GtkDrawingArea*, cairo_t* cr, int width, int height, gpointer user);
|
||
void math_canvas_pressed(GtkGestureClick*, int, double x, double y, gpointer user);
|
||
void math_canvas_drag_begin(GtkGestureDrag*, double x, double y, gpointer user);
|
||
void math_canvas_drag(GtkGestureDrag*, double dx, double dy, gpointer user);
|
||
void math_canvas_drag_end(GtkGestureDrag*, double dx, double dy, gpointer user);
|
||
gboolean math_canvas_scrolled(GtkEventControllerScroll* scroll, double, double dy, gpointer user);
|
||
gboolean math_canvas_key(GtkEventControllerKey*, guint keyval, guint, GdkModifierType state, gpointer user);
|
||
void math_add_node(MathDialog* d, MathOp op);
|
||
void on_math_node_added(GtkButton*, gpointer user);
|
||
void on_math_node_deleted(GtkButton*, gpointer user);
|
||
void on_math_output_set(GtkButton*, gpointer user);
|
||
void on_math_param_changed(GtkWidget*, gpointer user);
|
||
void on_math_source_changed(GObject*, GParamSpec*, gpointer user);
|
||
void on_math_op_changed(GObject* object, GParamSpec*, gpointer user);
|
||
void math_dialog_sync_form(MathDialog* d);
|
||
void math_dialog_sync(MathDialog* d);
|
||
void on_math_apply(GtkButton*, gpointer user);
|
||
GtkWidget* math_row(const char* label, GtkWidget* widget, GtkWidget** row_out);
|
||
void open_math_dialog(App& app, int edit);
|
||
GtkWidget* build_measures_panel(App& app);
|
||
GtkWidget* build_cursors_panel(App& app);
|
||
void on_cursors_toggled(GtkToggleButton* button, gpointer user);
|
||
void on_fit_clicked(GtkButton*, gpointer user);
|
||
void on_fit_x_clicked(GtkButton*, gpointer user);
|
||
void on_cursors_reset(GtkButton*, gpointer user);
|
||
void on_export_response(GObject* source, GAsyncResult* result, gpointer user);
|
||
void on_export_clicked(GtkButton*, gpointer user);
|
||
void on_about_clicked(GtkButton*, gpointer user);
|
||
void on_help_clicked(GtkButton*, gpointer user);
|
||
void on_shortcuts_clicked(GtkButton*, gpointer user);
|
||
void on_y_auto_clicked(GtkButton* button, gpointer user);
|
||
void on_fixed_range_apply(GtkButton*, gpointer user);
|
||
void on_y_fixed_clicked(GtkButton* button, gpointer user);
|
||
void remove_trace(App& app, int row, int slot);
|
||
int remove_signal_from_plots(App& app, const std::string& name);
|
||
std::vector<std::string> disable_dependent_math_channels(App& app, const std::string& name);
|
||
void on_remove_clicked(GtkButton* button, gpointer user);
|
||
GtkWidget* menu_icon_button(const char* icon, const char* label);
|
||
void on_row_menu_show_remove(GtkButton*, gpointer user);
|
||
void on_row_menu_back(GtkButton*, gpointer user);
|
||
void row_menu_fill(RowMenuState& st);
|
||
GtkWidget* open_row_menu(App& app, int row, double x, double y, GtkWidget* parent, bool autohide = true);
|
||
void on_right_click(GtkGestureClick* gesture, int, double x, double y, gpointer user);
|
||
gboolean on_drop_channel(GtkDropTarget*, const GValue* value, double x, double y, gpointer user);
|
||
/// The same drop, onto a **scope window's** plot: the target carries the window, the row is the
|
||
/// window's own cell under the pointer, and the assignment lands in that window's row set.
|
||
gboolean on_scope_drop_channel(GtkDropTarget*, const GValue* value, double x, double y, gpointer user);
|
||
/// Plots `signal` on `row` of a scope window's row set (the signal page's *Plot on* buttons and the
|
||
/// drop target both end here). False, with a notice, when the signal cannot be acquired or the plot
|
||
/// holds four signals already.
|
||
bool assign_signal_to_scope(App& app, App::ScopeWindow& scope, std::size_t signal, int row);
|
||
/// Runs one keyboard accelerator. Scope windows carry the same shortcuts as the main window: the
|
||
/// view-level ones (`L` -- the layout editor) act on the window the key arrived in; the rest reach
|
||
/// 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);
|
||
/// Writes the main window's instruments -- the two cursors, the gate band, the trigger marker --
|
||
/// into every scope window, and adopts one dragged *there* back into the main window first. Called
|
||
/// once per frame from the main tick, after the main's own `sync_cursors()`/`sync_gate()`.
|
||
void sync_scope_instruments(App& app);
|
||
/// The main window's measurement marks, translated to each scope window's own rows (by signal
|
||
/// name) and placed there: a window that plots a measured signal draws its mark.
|
||
void mirror_measure_marks(App& app);
|
||
/// 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
|
||
/// window's** row set.
|
||
void on_scope_right_click(GtkGestureClick* gesture, int n_press, double x, double y,
|
||
gpointer user);
|
||
/// Reads each pane's own switch back off the header bar's button, and the other way round: the two are
|
||
/// one control in two places, and `panel_switch_sync` keeps the write from looking like a click.
|
||
void sync_panel_switches(App& app);
|
||
/// The trigger's four enumerations as words: the panel's dropdown labels and the session file's
|
||
/// vocabulary, in one place so the two cannot drift.
|
||
const char* trigger_type_text(TriggerType t);
|
||
TriggerType trigger_type_from_text(const std::string& s);
|
||
const char* trigger_edge_text(TriggerEdge e);
|
||
TriggerEdge trigger_edge_from_text(const std::string& s);
|
||
const char* width_cmp_text(WidthCmp c);
|
||
WidthCmp width_cmp_from_text(const std::string& s);
|
||
const char* trigger_mode_text(TriggerMode m);
|
||
TriggerMode trigger_mode_from_text(const std::string& s);
|
||
/// The panel's controls from the model, and the session's trigger into both.
|
||
void write_trigger_widgets(App& app);
|
||
/// The compact line the trigger pane shows while it is locked, and the lock itself: the pane's body and
|
||
/// its [edit] button follow from `trigger_locked`.
|
||
void sync_trigger_summary(App& app);
|
||
|
||
/// Both arm buttons -- the panel's and the compact line's -- from the one armed state the producer keeps in
|
||
/// `model->trigger_armed()`. The label was written from four places (the click, the toolbar toggle, the
|
||
/// curator's re-read, the start-up default) and a second button would have needed a fifth copy of the same
|
||
/// two lines; the drift that costs is a button reading "Arm" while the engine is armed. Sensitivity, and
|
||
/// the compact button's visibility, are stated here too and read from the toolbar toggle -- the control
|
||
/// that owns `enabled`, per the restore rules -- so no caller needs the model lock to state any of it.
|
||
void sync_arm_buttons(App& app);
|
||
void apply_pane_lock(App& app);
|
||
void on_pane_edit_clicked(GtkButton* button, gpointer user);
|
||
void apply_trigger_layout(App& app, const LayoutState::Trigger& t);
|
||
void on_panel_switch_toggled(GObject* obj, GParamSpec*, gpointer user);
|
||
void update_side_visibility(App& app);
|
||
void on_sidebar_toggled(GtkToggleButton* button, gpointer user);
|
||
void rebuild_signal_panels(App& app);
|
||
void svg_declared_size(const char* data, gsize size, int* w, int* h);
|
||
std::string icon_report(GtkIconTheme* theme, const char* name, const char* resource);
|
||
void note_panel_controls(App& app);
|
||
void report_event_zoom(App& a, const char* what);
|
||
void update_status_popover(App& app, const udpscope::StatusSnapshot& s);
|
||
std::uint64_t capture_file_bytes(std::uint32_t slots);
|
||
std::string abbrev_home(const std::string& path);
|
||
void update_capture_tooltip(App& app);
|
||
void update_capture_row(App& app);
|
||
bool record_event(App& app);
|
||
bool replay_event(App& app, const EventRecord& record);
|
||
bool go_live_again(App& app);
|
||
void snapshot_live_state(App& app);
|
||
void restore_live_state(App& app);
|
||
bool read_event_samples(const std::string& path, EventSamples* out, std::string* error);
|
||
std::vector<EventRecord> selected_events(App& app);
|
||
void update_group_buttons(App& app);
|
||
bool build_event_group(App& app, const std::vector<EventRecord>& records, EventGroupKind kind, std::string* error);
|
||
void sync_compare_axes(App& app);
|
||
void on_group_clicked(GtkButton* button, gpointer user);
|
||
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
|
||
/// is skipped when the page is already open -- it is current then, and a rebuild would drop the
|
||
/// Compare/Accumulate selection and the scroll position. Returns what `slideover_open()` returns.
|
||
bool open_events_page(App& app);
|
||
/// Re-marks which row is the event currently on screen, **without rebuilding the list**.
|
||
///
|
||
/// The mark is the only thing about a row that a replay changes -- the picture and the numbers
|
||
/// are the file's -- and rebuilding the list to restate it threw away the scroll position and
|
||
/// the selection with it, so reaching the thirtieth event meant scrolling to it again after
|
||
/// every click, and a selection made for a comparison was lost by the act of looking at one of
|
||
/// its members. Call this for a change of what is on screen, and `refresh_events_pane()` only
|
||
/// for a change of what is *cached*.
|
||
void mark_events_on_screen(App& app);
|
||
void on_event_row_activated(GtkListBox*, GtkListBoxRow* row, gpointer user);
|
||
GtkWidget* build_events_pane(App& app);
|
||
gboolean tick(gpointer user);
|
||
void on_events_record_toggled(GObject* o, GParamSpec*, gpointer user);
|
||
GtkWidget* build_trigger_panel(App& app);
|
||
GtkWidget* build_signal_panel(App& app);
|
||
void note_starvation(App& app);
|
||
GtkWidget* build_window_control(App& app);
|
||
void run_app_action(App& app, AppAction action);
|
||
gboolean on_app_key_pressed(GtkEventControllerKey*, guint keyval, guint, GdkModifierType state, gpointer user);
|
||
GtkWidget* build_toolbar(App& app);
|
||
void on_activate(GtkApplication* application, gpointer user);
|
||
gboolean exit_probe_step3(gpointer user);
|
||
gboolean exit_probe_step2(gpointer user);
|
||
GtkWidget* find_labelled_button(GtkWidget* w, const char* label);
|
||
gboolean trig_trace_probe(gpointer user);
|
||
gboolean eventzoom_wait(gpointer user);
|
||
gboolean eventzoom_probe(gpointer user);
|
||
gboolean math_probe(gpointer user);
|
||
gboolean mathwrap_probe(gpointer user);
|
||
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, bool detached = false);
|
||
/// Detaches figure row `fr` into a window of its own: the figure is copied into a new scope
|
||
/// window -- which is what makes the window *detached*, the one kind with an attach control --
|
||
/// and removed from the main window's rows. Refused, changing nothing, for a one-figure layout
|
||
/// or a maximized one: detaching then would leave the main window with nothing to draw.
|
||
void detach_figure(App& app, int fr);
|
||
/// Puts a detached window's figure back: a figure row is appended to the main window carrying
|
||
/// the window's arrangement (cells, weights, every slot's signal in its colour) and the window
|
||
/// closes. The window must be a detached one; anything else returns without changing.
|
||
void attach_scope_window(App& app, App::ScopeWindow& scope);
|
||
gboolean measurepin_probe(gpointer user);
|
||
gboolean measurepin_held_wait(gpointer user);
|
||
gboolean measurepin_pause_wait(gpointer user);
|
||
gboolean measurepin_live_wait(gpointer user);
|
||
gboolean measure_trace(gpointer user);
|
||
gboolean stats_probe_report(gpointer user);
|
||
gboolean stats_probe(gpointer user);
|
||
gboolean dock_section_probe_report(gpointer user);
|
||
gboolean dock_section_probe(gpointer user);
|
||
gboolean dock_session_probe(gpointer user); // UDPSCOPE_PROBE_DOCK_SESSION_MS
|
||
/// The slide-over panel (`src/ui_slideover.cpp`): one panel at a time over the plot's right edge, a page
|
||
/// per setup surface, each page in a scroller of its own.
|
||
void build_slideover(App& app); // once, after the plot_overlay exists
|
||
bool slideover_add_page(App& app, const std::string& id, const std::string& title, GtkWidget* content);
|
||
/// Retitles a page in place (`slideover_add_page`'s title, for a page whose subject changed: a signal
|
||
/// page carries its channel's number, and the stream can renumber). False when the id is unknown.
|
||
bool slideover_set_title(App& app, const std::string& id, const std::string& title);
|
||
bool slideover_has_page(const App& app, const std::string& id);
|
||
/// The inverse of `slideover_add_page`: false when the id is unknown or is the open page.
|
||
bool slideover_remove_page(App& app, const std::string& id);
|
||
bool slideover_open(App& app, const std::string& id); // false when the id is unknown
|
||
void slideover_close(App& app);
|
||
bool slideover_toggle(App& app, const std::string& id); // open if closed or on another page, else close
|
||
const std::string& slideover_page(const App& app); // "" when closed
|
||
bool slideover_escape(App& app); // closes if open; true when it did
|
||
/// x, y in the overlay's coordinates; closes unless the point is inside the panel; true when it closed.
|
||
bool slideover_click_away(App& app, double x, double y);
|
||
gboolean slideover_probe(gpointer user); // UDPSCOPE_PROBE_SLIDEOVER_MS
|
||
/// The badge bar (`src/ui_badges.cpp`): setup is reached by the object it configures.
|
||
/// The Trigger badge's state word. Pure, so the probe can assert the whole table.
|
||
const char* trigger_badge_state(bool enabled, bool armed, bool pending, bool has_event);
|
||
void build_badge_bar(App& app); // after build_slideover, the toolbar (it reads trigger_toggle) and build_source_panels (the Source badge's page)
|
||
void sync_badge_bar(App& app); // per tick and on every slide-over open/close; only writes what changed
|
||
gboolean badges_probe(gpointer user); // UDPSCOPE_PROBE_BADGES_MS
|
||
/// Rebuilds the signal badges (one per enabled signal, or the "N signals" overflow) from the model.
|
||
void sync_signal_badges(App& app);
|
||
gboolean signal_badges_probe(gpointer user); // UDPSCOPE_PROBE_SIGNAL_BADGES_MS
|
||
gboolean source_kinds_probe_report(gpointer user);
|
||
gboolean source_kinds_probe(gpointer user);
|
||
/// MCC E-1608 end to end against `tools/mock_e1608.py`: the stream, the calibration, the panels and
|
||
/// their three controls, the capture button and a hand-checked decode. `_report` is the second phase.
|
||
/// `_reconnect` reads the state after the mock dropped the link; `_resumed` shows the stream moved on.
|
||
gboolean e1608_probe(gpointer user);
|
||
gboolean e1608_probe_report(gpointer user);
|
||
gboolean e1608_probe_reconnect(gpointer user);
|
||
gboolean e1608_probe_resumed(gpointer user);
|
||
/// The generator's update rate: starts a Sine on DAC 0 through the panel, reads the achieved rate the
|
||
/// source measured and the Analog out panel's "Update rate" row (`_report`, 2.5 s later), then stops
|
||
/// it and checks both read "off" (`_stopped`).
|
||
gboolean e1608_gen_probe(gpointer user);
|
||
gboolean e1608_gen_probe_report(gpointer user);
|
||
gboolean e1608_gen_probe_stopped(gpointer user);
|
||
/// The E-1608 form's "Scan network" row: presses Scan against the mock, then (phases `_fill`,
|
||
/// `_rescan`) clicks a result to fill the address, "Use MAC" to fill the MAC, scans again and checks
|
||
/// the results were replaced, not added to.
|
||
gboolean e1608_scan_probe(gpointer user);
|
||
gboolean e1608_scan_probe_fill(gpointer user);
|
||
gboolean e1608_scan_probe_rescan(gpointer user);
|
||
/// The flat UDP (MARTe2 UDPSender) source: the signal-list codec and a hand-computed decode, then,
|
||
/// against `tools/mock_flatudp.py`, the stream, the value continuity, the wrong-size accounting and
|
||
/// the capture button (`_report`, a second later). `_gap` starts a 100 ms poll that waits for the
|
||
/// stream, then for the mock's silence to drop the source (whenever it falls), then (`_resumed`) for
|
||
/// the stream to come back.
|
||
gboolean flatudp_probe(gpointer user);
|
||
gboolean flatudp_probe_report(gpointer user);
|
||
gboolean flatudp_probe_gap(gpointer user);
|
||
gboolean flatudp_probe_resumed(gpointer user);
|
||
gboolean pan_probe(gpointer user);
|
||
gboolean arm_probe(gpointer user);
|
||
gboolean trigrenum_probe_step3(gpointer user);
|
||
gboolean trigrenum_probe_step2(gpointer user);
|
||
gboolean trigrenum_probe(gpointer user);
|
||
gboolean fields_probe(gpointer user);
|
||
gboolean condition_probe_report(gpointer user);
|
||
gboolean condition_probe_arm(gpointer user);
|
||
gboolean condition_probe(gpointer user);
|
||
gboolean search_probe_marks(gpointer user);
|
||
gboolean search_probe_report(gpointer user);
|
||
gboolean search_probe_run(gpointer user);
|
||
gboolean search_probe(gpointer user);
|
||
gboolean window_probe(gpointer user);
|
||
GtkWidget* layout_button(App& a, bool insert, int index);
|
||
GtkWidget* layout_cell_button(App& a, int fr, int pos);
|
||
GtkWidget* layout_col_handle(App& a, int plot);
|
||
/// Records the starting widths (logical px) and weights of the cell pair a column drag on `plot`
|
||
/// (the right-hand cell) begins with. False when `plot` has no left neighbour.
|
||
bool layout_col_drag_begin(App& app, int plot);
|
||
void layout_col_apply(App& app, int plot, double offset_px);
|
||
/// A column drag's pointer travel from what its gesture reports: `offset` is relative to the handle, which
|
||
/// has moved from `handle_begin` to `handle_now` (same units, the overlay's) since the drag began.
|
||
double layout_col_total_offset(double offset, double handle_now, double handle_begin);
|
||
void layout_probe_report(App& a, const char* what);
|
||
/// The split in the report lines, from the `RowSet` alone (no widget is asked, so the exit dump can
|
||
/// use them): cells per figure row (`1,2,1`) and each figure row's cell weights, rows divided by `|`
|
||
/// and empty for an unsplit row (`|0.3,0.7|`).
|
||
std::string layout_cells_text(const RowSet& rs);
|
||
std::string layout_colweights_text(const RowSet& rs);
|
||
void layout_click(GtkWidget* b);
|
||
void layout_probe_next(App& a, int step, guint delay_ms);
|
||
gboolean layout_probe_step(gpointer user);
|
||
void split_probe_report(App& a, const char* tag);
|
||
void split_probe_next(App& a);
|
||
gboolean split_probe_step(gpointer user);
|
||
gboolean enable_all_probe(gpointer user);
|
||
gboolean disarm_probe_step2(gpointer user);
|
||
gboolean disarm_probe_step3(gpointer user);
|
||
gboolean disarm_probe_wait(gpointer user);
|
||
gboolean disarm_probe_step1(gpointer user);
|
||
int persist_probe_depth();
|
||
void persist_probe_report(App& a, const char* what);
|
||
void persist_probe_set_depth(App& a, int depth);
|
||
gboolean persist_probe_wait(gpointer user);
|
||
gboolean persist_probe(gpointer user);
|
||
|
||
/// The phosphor density's one step for a frame: state every row's `plot::Plot` with the
|
||
/// configuration the trigger holds, advance its fade by the frame's own `dt`, and deposit the
|
||
/// acquisition on screen if one has arrived since the last step. `display` and `live_display` are
|
||
/// passed in rather than re-read, so the step, the bind and the status line all describe one frame.
|
||
///
|
||
/// The density is on while an acquisition is on screen -- `Held`, and `Capturing` as it fills --
|
||
/// and off on a live view, a paused one, or a file: the display the user asked for is a *trigger*
|
||
/// display. Off is not "paused": switching it off (and leaving trigger mode, and opening a file)
|
||
/// drops the image at once, so a fresh presentation starts empty rather than showing a stale image
|
||
/// at another scale.
|
||
void apply_density(App& a, DisplayMode display, bool live_display);
|
||
|
||
/// The density probe (`UDPSCOPE_PROBE_DENSITY_MS`): the display's own numbers read back off the
|
||
/// plots -- total ink and peak with it off, on, and after one decay step of each kind.
|
||
void density_probe_report(App& a, const char* what);
|
||
/// States the switch and/or the fade through the panel's own widgets, the way a user would.
|
||
void density_probe_set(App& a, bool on, bool by_time);
|
||
gboolean density_probe(gpointer user);
|
||
gboolean density_probe_step(gpointer user);
|
||
void force_probe_report(App& a, const char* leg);
|
||
gboolean force_probe_press(gpointer user);
|
||
gboolean force_probe_poll(gpointer user);
|
||
gboolean force_probe_step2(gpointer user);
|
||
gboolean force_probe(gpointer user);
|
||
void pause_probe_report(App& a, const char* what);
|
||
gboolean pause_probe(gpointer user);
|
||
gboolean export_probe(gpointer user);
|
||
gboolean exit_probe_step1(gpointer user);
|
||
void cursors_probe_report(App& a, const char* what);
|
||
void fit_probe_report(App& a, const char* what);
|
||
gboolean timelabels_probe_step(gpointer user);
|
||
void fit_probe_zoom_in(App& a);
|
||
gboolean fit_probe_step(gpointer user);
|
||
void cursors_probe_next(App& a, int step, guint delay_ms);
|
||
gboolean cursors_probe_step(gpointer user);
|
||
void measure_dialog_click(App& a, const char* label);
|
||
void measure_dialog_report(App& a, const char* what);
|
||
void measures_probe_next(App& a, int step, guint delay_ms);
|
||
gboolean measures_probe_step(gpointer user);
|
||
void bandavg_probe_next(App& a, int step, guint delay_ms);
|
||
void bandavg_report(App& a, const char* what);
|
||
int bandavg_index(App& a, MeasureType type, const char* signal);
|
||
gboolean bandavg_probe_step(gpointer user);
|
||
gboolean zoomkeys_probe(gpointer user);
|
||
void menus_probe_next(App& a, int step, guint delay_ms);
|
||
void menu_row_labels(GtkWidget* box, GtkWidget** label, GtkWidget** accel);
|
||
gboolean menus_probe_step(gpointer user);
|
||
gboolean windowpop_probe_step(gpointer user);
|
||
|
||
/// Closes the time-window popover when a press lands elsewhere in the window (see ui.cpp). Declared for
|
||
/// the probe, which drives it and presses the button to check the one case that must *not* close it.
|
||
void on_window_popover_press(GtkGestureClick* gesture, int n_press, double x, double y, gpointer user);
|
||
gboolean color_probe(gpointer user);
|
||
gboolean reseq_probe(gpointer user);
|
||
void capture_probe_next(App& a, int step, guint delay_ms);
|
||
gboolean capture_probe_step(gpointer user);
|
||
gboolean configs_probe(gpointer user);
|
||
void rowmenu_probe_next(App& a, int step, guint delay_ms);
|
||
GtkWidget* row_menu_find(GtkWidget* container, const char* want);
|
||
void row_menu_measure(GtkWidget* box, const char* tag, int* rows_out, int* bad_out);
|
||
gboolean rowmenu_probe_step(gpointer user);
|
||
int signal_tick_visible(App& app);
|
||
int group_visible_traces(App& app, int row);
|
||
void report_events_availability(App& a, const char* what);
|
||
void events_probe_next(App& a, int step, guint delay_ms);
|
||
gboolean events_probe_step(gpointer user);
|
||
gboolean events_group_probe_step(gpointer user);
|
||
void events_group_probe_next(App& a, int step, guint delay_ms);
|
||
gboolean drop_probe(gpointer user);
|
||
gboolean noise_probe(gpointer user);
|
||
gboolean noise_probe_setup(gpointer user);
|
||
gboolean noise_probe_math(gpointer user);
|
||
gboolean noise_probe_denoise(gpointer user);
|
||
void collect_spins(GtkWidget* w, std::vector<double>* out);
|
||
gboolean defaults_probe(gpointer user);
|
||
gboolean keys_probe(gpointer user);
|
||
gboolean help_probe(gpointer user);
|
||
void helpui_probe_next(App& a, int step, guint delay_ms);
|
||
gboolean helpui_probe_step(gpointer user);
|
||
/// `UDPSCOPE_PROBE_TOUR_SURFACE_MS`: a stop that stands on another page of a stack, entered from the
|
||
/// page it is not on -- the sidebar put on Measure, the walk to the signals stop, the reading of the
|
||
/// page and of the spotlight, and then the same walk with the sidebar *collapsed*.
|
||
void tour_surface_probe_next(App& a, guint delay_ms);
|
||
|
||
// --- the help affordance layer (ui_context_help.cpp, ui_tour.cpp) ----------
|
||
//
|
||
// Two things live here and they are not the same thing: the *contextual help* is a "?" on every
|
||
// surface the app can explain, and the *guided tour* is one walk over five of them. Both read the
|
||
// one setting the Help menu's toggle writes.
|
||
|
||
/// Adds or removes the "?" affordances so they match `settings.contextual_help`, and does nothing
|
||
/// else. Idempotent, so it can be called after the window is built, on every change of the setting
|
||
/// and once more when a dialog that carries one of the hosts is opened.
|
||
void context_help_sync(App& app);
|
||
/// How many "?" affordances are on screen, writing their ids space-separated into `ids`. Found by
|
||
/// the data key rather than kept in a list, so a host that died with a dialog cannot leave a stale
|
||
/// pointer behind.
|
||
int context_help_count(App& app, std::string* ids);
|
||
/// The menu row's handler: writes the setting, saves the file and re-states the layer.
|
||
void on_contextual_help_toggled(GtkToggleButton* button, gpointer user);
|
||
/// The "?" for one surface by id, for a host that only exists while a dialog is open (the math
|
||
/// editor is rebuilt on every open, so it cannot be a row the window's own build visits).
|
||
void context_help_add(App& app, const char* id, GtkWidget* host);
|
||
/// The same affordance for a surface that is *not* in `App`'s table -- the source chooser's, which
|
||
/// runs before `App` exists and so cannot name one of its fields. `host` is a `GtkBox` and the button
|
||
/// joins it as its last child, which is the rule the table's own rows follow; `on` false takes any
|
||
/// hint off `host` instead. `shortcut` may be "".
|
||
void context_help_button(GtkWidget* host, const char* id, const char* title, const char* body,
|
||
const char* shortcut, bool on);
|
||
/// How many hints a widget subtree carries, and their ids, for a window that has no `App` (the
|
||
/// chooser's probe). `context_help_count()` is this with `App::window` as the root.
|
||
int context_help_count_in(GtkWidget* root, std::string* ids);
|
||
/// The size of the first *visible* hint button in a subtree, read the way the probe's geometry is
|
||
/// (`gtk_widget_compute_bounds()`), and the number a shrinking affordance is checked against. Both
|
||
/// are 0 when the layer is off or nothing on screen carries one.
|
||
void context_help_size_in(GtkWidget* root, int* width, int* height);
|
||
/// `context_help_size_in()` with `App::window` as the root.
|
||
void context_help_size(App& app, int* width, int* height);
|
||
|
||
/// Builds a tour's overlay children (the four dim rectangles, the spotlight outline and the card)
|
||
/// inside `overlay`. Called once, as the overlay is inserted, by either window that draws a tour.
|
||
void tour_build(TourView& view, GtkWidget* overlay);
|
||
/// Starts at the first stop. `stops` is the whole table, in walk order: the stops whose surface is
|
||
/// not on screen are filtered out here, by the one rule both windows use (a wrong page is kept and
|
||
/// selected, a collapsed pane is dropped). `on_done` (may be null) runs when the walk passes its last
|
||
/// stop, and is how the chooser hands off.
|
||
void tour_begin(TourView& view, std::vector<TourStop> stops, void (*on_done)(void* user),
|
||
void* done_user);
|
||
/// Moves `delta` stops, finishing the walk (and calling `on_done`) on the way past the last one.
|
||
void tour_advance(TourView& view, int delta);
|
||
/// Ends the walk without finishing it: every child of the overlay is hidden and nothing is left on
|
||
/// screen. Skip and Escape.
|
||
void tour_end(TourView& view);
|
||
/// Called from the tick while the walk runs: re-measures the target each frame, so a window resize
|
||
/// keeps the spotlight on the widget, and places the card where it does not cover it.
|
||
void tour_update(TourView& view);
|
||
/// The widget the current stop points at, or nullptr when the walk is not on a stop.
|
||
GtkWidget* tour_target(TourView& view);
|
||
/// The part of `target` that is on screen, in `overlay`'s coordinates: its bounds intersected with every
|
||
/// scrolled window above it. A target scrolled out of sight entirely is measured and *empty*: true with a
|
||
/// zero-size rectangle. False only when it cannot be measured yet (no allocation, not in the overlay).
|
||
/// The spotlight is drawn from it, and the probe reads the spotlight against it.
|
||
bool tour_target_bounds(GtkWidget* target, GtkWidget* overlay, graphene_rect_t* out);
|
||
|
||
/// Builds the *window's* tour inside `app.tour.overlay`. Called once, as the overlay is inserted.
|
||
void build_tour(App& app);
|
||
/// Starts the window's tour at its first step and writes `tour_seen`. Safe to call while it already
|
||
/// runs -- it restarts at step 0.
|
||
void start_tour(App& app);
|
||
/// Moves `delta` steps, ending the tour on the way past the last one.
|
||
void tour_step(App& app, int delta);
|
||
/// Ends the tour: every child of the overlay is hidden and nothing of the tour is left on screen.
|
||
void tour_dismiss(App& app);
|
||
/// The window's tour, re-measured each frame.
|
||
void update_tour(App& app);
|
||
bool tour_active(const App& app);
|
||
/// The widget the current tour step points at, or nullptr when the tour is not on a step. The probe
|
||
/// reads the spotlight against it.
|
||
GtkWidget* tour_target(App& app);
|
||
/// The steps this run actually walks (targets that were on screen when it started), and where it is.
|
||
int tour_step_count(const App& app);
|
||
int tour_step_index(const App& app);
|
||
/// The first-run auto-start's callback, scheduled only when `settings.tour_seen` is false.
|
||
gboolean tour_autostart(gpointer user);
|
||
/// The chooser's "Guided tour" request (`Config::tour_pending`): the same delayed start, but it
|
||
/// sets `tour_requested` rather than `tour_auto_started`, so the probe and the dump can tell the two
|
||
/// apart. Scheduled only when the config carries the request.
|
||
gboolean tour_start_requested(gpointer user);
|
||
/// Captures the layer's own readings and the bounds of the surfaces the help's figures label, while
|
||
/// the window is alive -- the dump runs after it is destroyed and may not ask GTK anything.
|
||
void capture_help_state(App& app);
|
||
|
||
int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisition& acq,
|
||
RowSet& rows);
|
||
|
||
} // namespace ui_impl
|
||
} // namespace udpscope
|