// 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 #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 "ui_prefs.hpp" #include "shortcuts.hpp" #include "status_text.hpp" #include "waveform.hpp" #include #include #include #include #include #include #include #include #include #include #include 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 }; struct App { ScopeModel* model = nullptr; Acquisition* acq = nullptr; RowSet* rows = nullptr; GtkWidget* window = nullptr; GtkWidget* plot_box = 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; 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; /// 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 layout_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 layout_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 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 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 sidebar's tab stack and its switcher. The stack is what chooses which panels are on /// screen; a feature's own on/off never does (it sets its panel's sensitivity instead). GtkWidget* side_stack = nullptr; GtkWidget* side_switcher = nullptr; /// The page name the stack is showing, and the one value the session file persists about the /// sidebar. "signals" on a first run. std::string side_tab = "signals"; GtkWidget* arm_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 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; /// 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; /// 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 two sidebar pages' own icons and whether the theme resolved them, read on the first tick /// like the two lines above. std::string dbg_page_icons; /// What the switcher is: how many pills it holds, and how many labels it is *drawing*. The label /// count is the one that says the pages are icons rather than words: `narrow` hides them, and the /// titles stay set because they are the pages' names for a screen reader and for the tooltip. int dbg_switcher_pills = -1; int dbg_switcher_labels = -1; int dbg_switcher_names = -1; /// The pills' icon as it is *rendered*: a 32x16 glyph in a 32px box is drawn 1:1, which is what /// keeps its 2px bars crisp. int dbg_switcher_icon_w = -1; int dbg_switcher_icon_h = -1; /// The switcher's own size, beside it: the pills carry 32px icon boxes now, so the tab bar's height /// is what it costs. int dbg_switcher_w = -1; int dbg_switcher_h = -1; /// 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; int dbg_switcher_policy = -1; /// Whether `name_side_tabs()` has named the pills: it can only run once the switcher is mapped. bool side_tabs_named = false; /// 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 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; /// True while a drag has the overlaid sidebar hidden: what it was showing before, so `drag-end` puts it /// back rather than guessing. bool sidebar_hidden_for_drag = 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 two panes' modes. *Locked* is the one a returning user gets: the trigger pane shows a line /// that says what it is set to, the signal list shows only the signals that are on, and neither /// offers a 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 flags last for the /// session: a lock is a mode, not a setting, so it is not persisted. bool trigger_locked = false; bool signals_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] = {}; GtkWidget* trigger_edit = nullptr; GtkWidget* signals_edit = nullptr; int dbg_trigger_locked = -1; int dbg_signals_locked = -1; 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 cursors_lines; /// 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 measures; std::vector 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 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 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 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. plot::Range measure_window{}; bool measure_window_valid = false; bool measure_was_held = 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)` and not by channel alone: the corner is a per-measurement setting, so two /// measurements on one signal at different corners are reading two different signals and cannot share a /// walk. struct MeasureWindow { int channel = -1; double fc = 0.0; ///< the HF corner this window was walked at; 0 is unfiltered MeasureStats stats; std::vector pct; ///< the level shares asked for, deduplicated std::vector noise; ///< each entry's band, as a percentage; parallel to `pct` std::vector 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 bool ok = false; }; std::vector 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 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 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 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 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 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 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 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; 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 `-`, like the packet capture); /// `event_recording` is the trigger panel's switch and defaults on -- the cache is only useful if /// it was already on when the event you wanted happened. std::unique_ptr event_store; /// Whether the cache holds anything, refreshed once a second: what keeps the history button on the bar /// for a source whose events are already written, even with recording switched off. bool events_cached = false; bool event_recording = false; /// The events pane: its list, the revealer that holds it, 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 pane does not put anywhere special. GtkWidget* events_revealer = nullptr; GtkWidget* events_list = nullptr; GtkWidget* events_thumb = nullptr; GtkWidget* events_record_switch = nullptr; /// The toolbar's event-history toggle: the pane is opened *on demand* from here (the item's own /// arrangement), and the toggle only exists while trigger mode does -- there are no events without /// a trigger, and a button that can never have anything to show is a button spent on nothing. GtkWidget* events_toggle = nullptr; /// The paned that holds the events pane and the plot, and the width the divider was last dragged /// to. 240 by default: enough for the preview and the numbers beside it, and not a plot's worth. GtkWidget* events_divider = nullptr; int events_pane_width = 240; /// 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 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 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> sync_last_a; std::vector> 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 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 `_.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; guint layout_save_source = 0; bool layout_loaded = false; unsigned signal_generation = ~0u; 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 stored_rate; std::vector 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 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; 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; int dbg_signal_page_current = 0; 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 channels 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`: `rows - 1`, the divider not being draggable. int dbg_signal_drag_sources = -1; /// The drag-with-an-overlaid-sidebar pair: the sidebar hid for the drag, and came back after it. int dbg_drag_hid_sidebar = -1; int dbg_drag_restored_sidebar = -1; std::size_t dbg_signal_plain = 0; 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_switcher_visible = 0; 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 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: MeasureWindowReader(App& app, std::size_t channel) { const RowSet::Row& r0 = app.rows->row(0); if (r0.plot == nullptr) return; const plot::Range view = r0.plot->x_range(r0.axis_x); if ((snap_ = app.model->display_snapshot()) != nullptr) { if (channel >= snap_->values.size()) return; const std::vector& st = snap_->stamps; if (snap_->values[channel].size() != st.size()) return; const auto lo = std::lower_bound(st.begin(), st.end(), view.min); const auto hi = std::upper_bound(st.begin(), st.end(), view.max); if (hi <= lo) return; off_ = static_cast(lo - st.begin()); count_ = static_cast(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(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(view.min - epoch_, view.max - epoch_, first, last); if (last <= first) return; store_ = &app.model->live(); channel_ = channel; first_ = first; count_ = static_cast(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(origin_, pyr_->window_base()); const u64 end = std::min(origin_ + count_, pyr_->window_end()); if (end <= begin) return false; const u64 stored = pyr_->level_base(0); first = std::max(stored, (begin + plot::data::kBucket - 1) / plot::data::kBucket); last = std::min(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(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(mm.min); out.hi = static_cast(mm.max); out.sum = pyr_->bucket_sum(0, k); out.count = static_cast(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 snap_; const std::vector* snap_vals_ = nullptr; std::size_t off_ = 0; std::unique_lock 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 scratch_; }; struct MeasureDialog { App* app = nullptr; int edit = -1; GtkWidget* signal_drop = nullptr; GtkWidget* type_drop = nullptr; GtkWidget* threshold = nullptr; GtkWidget* above = nullptr; GtkWidget* show = 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* 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 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 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; 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 names; std::vector times; std::vector> columns; ///< [column][row] }; using EventThumbCache = std::unordered_map; 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; 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"; 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 g_stats_n_before; /// The same for the tab-switch probe: the accumulator counts recorded while the Measure page was /// still current, so the report can say whether any of them advanced afterwards. inline std::vector g_side_tab_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); 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); 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); void open_calibration(App& app, GtkWindow* parent, std::size_t signal); GdkContentProvider* on_drag_prepare(GtkDragSource*, double, double, gpointer user); int signal_color_index(const std::vector& 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& 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& signals); /// The `MATH` divider row in the one channel list, and the `+` it carries. GtkWidget* make_math_divider(App& app, bool with_add); 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); /// Restates the events pane's divider position once a frame; see the definition. void sync_events_divider(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); bool visible_samples(App& app, std::size_t channel, std::vector& time, std::vector& 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); 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 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 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); void on_side_tab_changed(GObject*, GParamSpec*, gpointer user); void select_side_tab(App& app, const char* name); /// 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); /// Hides the sidebar for the length of a drag when it is *overlaid*, and puts it back after. With the /// split view collapsed the sidebar covers the plot, so a drag that starts in the signal list has nowhere /// to land: the pointer is over the sidebar, not over a drop target. void row_drag_sidebar(App& app, bool dragging); /// The compact line the trigger pane shows while it is locked, and the lock itself: the panes' /// bodies, their [edit] buttons and the signal list's rows all follow from `trigger_locked` and /// `signals_locked`. void sync_trigger_summary(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 selected_events(App& app); void update_group_buttons(App& app); bool build_event_group(App& app, const std::vector& 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); /// 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 measure_probe(gpointer user); gboolean measure_trace(gpointer user); gboolean stats_probe_report(gpointer user); gboolean stats_probe(gpointer user); gboolean side_tab_probe_report(gpointer user); gboolean side_tab_probe(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); void layout_probe_report(App& a, const char* what); void layout_click(GtkWidget* b); void layout_probe_next(App& a, int step, guint delay_ms); gboolean layout_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); 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); 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* out); gboolean defaults_probe(gpointer user); gboolean keys_probe(gpointer user); gboolean help_probe(gpointer user); int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisition& acq, RowSet& rows); } // namespace ui_impl } // namespace udpscope