diff --git a/src/plot_host.cpp b/src/plot_host.cpp index 770a70b..96fa5ae 100644 --- a/src/plot_host.cpp +++ b/src/plot_host.cpp @@ -5,6 +5,7 @@ #include #include +#include namespace udpscope { namespace { @@ -67,6 +68,9 @@ constexpr float kMeasureDashOff = 4.0f; /// a box, so there was a widget gap; a figure tiles its viewport, and without a gap two /// stacked frames read as one tall plot with a line through it. constexpr plot::f32 kRowGap = 6.0f; +/// The gap between two cells of a split row, device pixels: the same as the rows', so the +/// plots read as a grid. +constexpr plot::f32 kColGap = 6.0f; /// The suffix that names a persistence layer, in the accumulate path's own vocabulary -- /// ` (A)`, ` (B)`, ` #4` -- so that `layer_base_name()` takes it back off, and the signal list, the @@ -153,11 +157,56 @@ void RowSet::clear_signal_color(const std::string& signal) { RowSet::RowSet() { rebuild_all(); } +int RowSet::cells_in_row(int fr) const { + return fr >= 0 && fr < figure_row_count() ? cells_per_row_[static_cast(fr)] : 0; +} + +int RowSet::first_plot_of_row(int fr) const { + int p = 0; + for (int r = 0; r < fr && r < figure_row_count(); ++r) p += cells_per_row_[static_cast(r)]; + return p; +} + +int RowSet::figure_row_of(int plot) const { + if (plot < 0 || plot >= rows_) return -1; + int p = 0; + for (int r = 0; r < figure_row_count(); ++r) { + p += cells_per_row_[static_cast(r)]; + if (plot < p) return r; + } + return -1; +} + +int RowSet::cell_of(int plot) const { + const int fr = figure_row_of(plot); + return fr < 0 ? -1 : plot - first_plot_of_row(fr); +} + +const std::vector& RowSet::cell_weights(int fr) const { + static const std::vector none; + return fr >= 0 && fr < static_cast(cell_weights_.size()) + ? cell_weights_[static_cast(fr)] + : none; +} + +std::string RowSet::plot_label(int plot) const { + const int fr = figure_row_of(plot); + if (fr < 0) return std::to_string(plot + 1); + if (cells_per_row_[static_cast(fr)] == 1) return std::to_string(fr + 1); + return std::to_string(fr + 1) + static_cast('a' + cell_of(plot)); +} + void RowSet::set_row_count(int rows) { const int want = std::clamp(rows, 1, kMaxRows); - if (want == rows_) return; + const bool all_single = + std::all_of(cells_per_row_.begin(), cells_per_row_.end(), [](int c) { return c == 1; }); + if (want == rows_ && all_single) return; + // `n` single-cell rows. The plots keep their flattened slots (and so their contents), which is + // what the old "n rows" meant. rows_ = want; - // A `Figure`'s row count is fixed at construction, so a new count is a new figure. + cells_per_row_.assign(static_cast(want), 1); + cell_weights_.assign(static_cast(want), {}); + // A `Figure`'s shape is fixed at construction, so a new count is a new figure. rebuild_all(); } @@ -194,23 +243,103 @@ void RowSet::clear_row(Row& r) { bool RowSet::insert_row(int at) { if (rows_ >= kMaxRows) return false; - const int pos = std::clamp(at, 0, rows_); + const int fr = std::clamp(at, 0, figure_row_count()); + const int pos = first_plot_of_row(fr); for (int i = rows_; i > pos; --i) carry_row(row(i), row(i - 1)); clear_row(row(pos)); ++rows_; + cells_per_row_.insert(cells_per_row_.begin() + fr, 1); + cell_weights_.insert(cell_weights_.begin() + fr, std::vector{}); rebuild_all(); return true; } bool RowSet::remove_row(int at) { - if (rows_ <= 1 || at < 0 || at >= rows_) return false; - for (int i = at; i + 1 < rows_; ++i) carry_row(row(i), row(i + 1)); - --rows_; - clear_row(row(rows_)); + if (figure_row_count() <= 1 || at < 0 || at >= figure_row_count()) return false; + const int pos = first_plot_of_row(at); + const int n = cells_per_row_[static_cast(at)]; + // The rows below move up by the removed row's cell count; the `n` slots freed at the end go back + // to empty. + for (int i = pos; i + n < rows_; ++i) carry_row(row(i), row(i + n)); + for (int i = rows_ - n; i < rows_; ++i) clear_row(row(i)); + rows_ -= n; + cells_per_row_.erase(cells_per_row_.begin() + at); + cell_weights_.erase(cell_weights_.begin() + at); rebuild_all(); return true; } +bool RowSet::insert_cell(int fr, int at) { + if (fr < 0 || fr >= figure_row_count()) return false; + if (cells_per_row_[static_cast(fr)] >= kMaxCellsPerRow || rows_ >= kMaxRows) { + return false; + } + const int n = cells_per_row_[static_cast(fr)]; + const int k = std::clamp(at, 0, n); + const int p = first_plot_of_row(fr) + k; + for (int i = rows_; i > p; --i) carry_row(row(i), row(i - 1)); + clear_row(row(p)); + ++rows_; + ++cells_per_row_[static_cast(fr)]; + auto& w = cell_weights_[static_cast(fr)]; + if (!w.empty()) { + // The new cell takes the mean share, so it neither dwarfs nor vanishes beside its neighbours. + float mean = 0.0f; + for (const float v : w) mean += v; + mean /= static_cast(w.size()); + w.insert(w.begin() + std::min(k, static_cast(w.size())), mean); + } + rebuild_all(); + return true; +} + +bool RowSet::remove_cell(int plot) { + const int fr = figure_row_of(plot); + if (fr < 0) return false; + if (cells_per_row_[static_cast(fr)] == 1) { + return remove_row(fr); // false when it is the last row + } + for (int i = plot; i + 1 < rows_; ++i) carry_row(row(i), row(i + 1)); + --rows_; + clear_row(row(rows_)); + const int k = plot - first_plot_of_row(fr); + --cells_per_row_[static_cast(fr)]; + auto& w = cell_weights_[static_cast(fr)]; + if (cells_per_row_[static_cast(fr)] < 2) { + w.clear(); + } else if (k < static_cast(w.size())) { + w.erase(w.begin() + k); + } + rebuild_all(); + return true; +} + +void RowSet::set_cell_weights(int fr, std::vector w) { + if (fr < 0 || fr >= figure_row_count()) return; + cell_weights_[static_cast(fr)] = std::move(w); + if (figure_ != nullptr) apply_cell_weights(); +} + +void RowSet::set_cells_per_row(std::vector cells) { + std::vector next; + int total = 0; + for (int c : cells) { + c = std::clamp(c, 1, kMaxCellsPerRow); + if (total + c > kMaxRows) c = kMaxRows - total; + if (c < 1) break; + next.push_back(c); + total += c; + } + if (next.empty()) { + next.push_back(1); + total = 1; + } + cells_per_row_ = std::move(next); + cell_weights_.assign(cells_per_row_.size(), {}); + rows_ = total; + rebuild_all(); +} + void RowSet::install(Row& r, int /*row_index*/, plot::Plot& p) { r.plot = &p; r.axis_x = r.plot->add_x_axis( @@ -302,14 +431,36 @@ void RowSet::set_row_weights(std::vector weights) { void RowSet::apply_row_weights() { if (row_weights_.empty() || figure_ == nullptr) return; - row_weights_.resize(static_cast(rows_), 1.0f); + row_weights_.resize(static_cast(figure_row_count()), 1.0f); (void)figure_->set_row_weights(row_weights_); } +void RowSet::apply_cell_weights() { + if (figure_ == nullptr) return; + cell_weights_.resize(cells_per_row_.size()); + for (int fr = 0; fr < figure_row_count(); ++fr) { + auto& w = cell_weights_[static_cast(fr)]; + const int n = cells_per_row_[static_cast(fr)]; + if (n < 2) { + w.clear(); + continue; + } + if (w.empty()) { + figure_->clear_cell_weights(static_cast(fr)); + continue; + } + w.resize(static_cast(n), 1.0f); + (void)figure_->set_cell_weights(static_cast(fr), w); + } +} + void RowSet::rebuild_all() { - figure_ = std::make_unique(static_cast(rows_)); + const std::vector cells(cells_per_row_.begin(), cells_per_row_.end()); + figure_ = std::make_unique(std::span(cells)); figure_->set_row_gap(kRowGap); + figure_->set_col_gap(kColGap); apply_row_weights(); + apply_cell_weights(); x_group_ = figure_->add_x_link(); for (int i = 0; i < kMaxRows; ++i) { Row& r = row(i); @@ -917,7 +1068,7 @@ bool RowSet::update_time_labels() { if (!relative_x_) r.plot->set_x_axis_origin(r.axis_x, 0.0); // The caption belongs with the labels, which a figure draws once, on the bottom row (the // one whose x axis is the figure's). Every other row gets an empty one, which draws nothing. - const bool bottom = i == rows_ - 1; + const bool bottom = i >= first_plot_of_row(figure_row_count() - 1); if (!relative_x_ || !bottom) { r.plot->set_x_note(""); continue; diff --git a/src/plot_host.hpp b/src/plot_host.hpp index e809cf0..45d62f5 100644 --- a/src/plot_host.hpp +++ b/src/plot_host.hpp @@ -155,8 +155,43 @@ class RowSet { double ruler_b_y = 0.0; }; + /// The number of **plots** (cells over all figure rows), which is what `row(i)` indexes. With no + /// row split it is also the number of figure rows; `figure_row_count()` is the latter. int row_count() const noexcept { return rows_; } + /// A figure row holds one to `kMaxCellsPerRow` plots side by side, sharing the row's height and + /// the figure's one x window. The flattened plot index (`row(i)`, `Row::plot`, the figure's + /// `plot(i)`) runs row-major: row 0's cells left to right, then row 1's. + static constexpr int kMaxCellsPerRow = 4; + int figure_row_count() const noexcept { return static_cast(cells_per_row_.size()); } + /// How many cells figure row `fr` holds; 0 for a row that does not exist. + int cells_in_row(int fr) const; + /// The flattened index of figure row `fr`'s first cell; `row_count()` for `fr == + /// figure_row_count()`. + int first_plot_of_row(int fr) const; + /// The figure row plot `plot` is in, or -1 for an index that is not a plot. + int figure_row_of(int plot) const; + /// Plot `plot`'s 0-based position inside its row, or -1. + int cell_of(int plot) const; + /// Figure row `fr`'s cell width shares (empty means equal; always empty for a single-cell row). + const std::vector& cell_weights(int fr) const; + /// What the UI calls a plot: the figure row's number (`"2"`) for a single-cell row, the number + /// and a letter (`"2a"`..`"2d"`) for a split one. + std::string plot_label(int plot) const; + + /// Adds an empty cell to figure row `fr` at position `at` (clamped to the row), pushing the + /// cells at and after it right. False at `kMaxCellsPerRow` cells or `kMaxRows` plots. + bool insert_cell(int fr, int at); + /// Removes plot `plot`. A single-cell row removes the row itself (`remove_row()`); false when + /// that would leave no plot. + bool remove_cell(int plot); + /// Figure row `fr`'s cell width shares, normalised by the plotter; empty means equal. + void set_cell_weights(int fr, std::vector w); + /// Replaces the whole arrangement: `cells[r]` plots in figure row `r`, each clamped to + /// `1..kMaxCellsPerRow` and the total to `kMaxRows`. Weights are reset; plot contents stay in + /// their flattened slots. + void set_cells_per_row(std::vector cells); + /// The alpha every trace is drawn with. The *group* views use it: accumulating events wants their /// traces translucent so that agreement between them reads as brightness, which is the whole point /// of a persistence display. It is mixed into the palette colour where the trace is created, so it @@ -169,22 +204,24 @@ class RowSet { /// trace set or the row count changes, so weights that lived only in the figure would be lost on the /// first drop or row change after a user had sized their layout. void set_row_weights(std::vector weights); - /// Sizes the stored weights to the rows that exist and hands them to the figure. + /// Sizes the stored weights to the figure rows that exist and hands them to the figure. Weights + /// are per **figure row**, not per plot. void apply_row_weights(); const std::vector& row_weights() const noexcept { return row_weights_; } - /// Shows `rows` rows. A changed count rebuilds the figure, since a `Figure`'s row - /// count is fixed at construction. + /// Shows `rows` figure rows of one cell each (so `rows` plots, any split undone). A changed + /// arrangement rebuilds the figure, since a `Figure`'s shape is fixed at construction. void set_row_count(int rows); - /// Inserts an empty row at `at` (0 = above the first row, `row_count()` = below the - /// last), pushing the rows at and below it down. Returns false at `kMaxRows`. + /// Inserts an empty single-cell figure row at `at` (0 = above the first row, + /// `figure_row_count()` = below the last), pushing the rows at and below it down with their + /// cells. Returns false at `kMaxRows` plots. /// /// The rows below move as *rows*: their trace assignments, colours and cursors follow /// them, so inserting above row 1 does not renumber what row 1 was plotting. The /// figure's `Plot`s are not what moves -- they are rebuilt and re-dealt -- which is /// the same mechanism a row-count change already used. bool insert_row(int at); - /// Removes row `at` and pulls the rows below it up. Returns false when only one row - /// is left: a figure with no rows has nothing to draw and no way back. + /// Removes figure row `at` (with all its cells) and pulls the rows below it up. Returns false + /// when only one row is left: a figure with no rows has nothing to draw and no way back. bool remove_row(int at); Row& row(int i) { return storage_[static_cast(i)]; } const Row& row(int i) const { return storage_[static_cast(i)]; } @@ -563,9 +600,15 @@ class RowSet { std::unique_ptr figure_; /// The one x link group every visible row belongs to. Groups are numbered from 1. plot::u32 x_group_ = 0; + /// The number of plots; `sum(cells_per_row_) == rows_` always. int rows_ = 1; + /// Cells per figure row, and each row's cell width shares (empty = equal; kept empty for a + /// single-cell row). Both are state of the `RowSet`, restated to every new figure. + std::vector cells_per_row_{1}; + std::vector> cell_weights_ = std::vector>(1); + void apply_cell_weights(); float trace_alpha_ = 1.0f; - std::vector row_weights_; + std::vector row_weights_; ///< per figure row double window_s_ = 2.0; double trigger_marker_x_ = 0.0; bool trigger_marker_on_ = false; diff --git a/src/ui.cpp b/src/ui.cpp index e5b9c79..aa57cbc 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -6003,6 +6003,9 @@ int run_ui(int argc, char** argv, const Config& cfg, ScopeModel& model, Acquisit if (const char* env = g_getenv("UDPSCOPE_PROBE_LAYOUT_MS")) { g_timeout_add(static_cast(std::atoi(env)), layout_probe_step, &app); } + if (const char* env = g_getenv("UDPSCOPE_PROBE_SPLIT_MS")) { + g_timeout_add(static_cast(std::atoi(env)), split_probe_step, &app); + } if (const char* env = g_getenv("UDPSCOPE_TRIG_TRACE_MS")) { g_timeout_add(static_cast(std::atoi(env)), trig_trace_probe, &app); } diff --git a/src/ui_app.hpp b/src/ui_app.hpp index b08d21c..319c085 100644 --- a/src/ui_app.hpp +++ b/src/ui_app.hpp @@ -221,6 +221,8 @@ struct App { /// 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; /// 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`). @@ -2095,6 +2097,9 @@ 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); +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); diff --git a/src/ui_probe.cpp b/src/ui_probe.cpp index 561a853..9f9a616 100644 --- a/src/ui_probe.cpp +++ b/src/ui_probe.cpp @@ -3796,6 +3796,219 @@ gboolean layout_probe_step(gpointer user) { return G_SOURCE_REMOVE; } +// --- the horizontal plot split (`UDPSCOPE_PROBE_SPLIT_MS`) ------------------------------------- +// +// Each step changes the arrangement of cells, re-points the area at the new figure, and lets the +// next step (250 ms on) read what a frame was actually laid out with. + +void split_probe_report(App& a, const char* tag) { + RowSet& rs = *a.rows; + const double scale = (a.area != nullptr && a.area->device_scale() > 0.0f) + ? static_cast(a.area->device_scale()) : 1.0; + const plot::FigureLayout& fl = rs.figure().layout(); + const int frows = rs.figure_row_count(); + // tile: every row's cells fill its rect edge to edge, share its y and height, and are one + // `col_gap` apart. + int tile = fl.cells.size() == static_cast(rs.row_count()) && + fl.rows.size() == static_cast(frows) + ? 1 : 0; + std::string cells_s; + std::string widths_s; + std::string weights_s; + for (int fr = 0; fr < frows && tile; ++fr) { + const int first = rs.first_plot_of_row(fr); + const int n = rs.cells_in_row(fr); + const plot::Rect& row = fl.rows[static_cast(fr)]; + const plot::Rect& c0 = fl.cells[static_cast(first)]; + const plot::Rect& cn = fl.cells[static_cast(first + n - 1)]; + if (std::fabs(c0.x - row.x) > 0.5f) tile = 0; + if (std::fabs(cn.right() - row.right()) > 0.5f) tile = 0; + for (int k = 0; k < n; ++k) { + const plot::Rect& c = fl.cells[static_cast(first + k)]; + if (std::fabs(c.y - row.y) > 0.5f || std::fabs(c.h - row.h) > 0.5f) tile = 0; + if (k > 0) { + const plot::Rect& prev = fl.cells[static_cast(first + k - 1)]; + if (std::fabs((c.x - prev.right()) - fl.col_gap) > 0.5f) tile = 0; + } + } + } + for (int fr = 0; fr < frows; ++fr) { + const int first = rs.first_plot_of_row(fr); + const int n = rs.cells_in_row(fr); + cells_s += (fr == 0 ? "" : ",") + std::to_string(n); + widths_s += fr == 0 ? "" : "|"; + weights_s += fr == 0 ? "" : "|"; + for (int k = 0; k < n; ++k) { + char buf[32]; + const std::size_t idx = static_cast(first + k); + std::snprintf(buf, sizeof(buf), "%s%.0f", k == 0 ? "" : ",", + idx < fl.cells.size() ? static_cast(fl.cells[idx].w) / scale : -1.0); + widths_s += buf; + } + const std::vector& w = rs.cell_weights(fr); + if (w.empty()) { + weights_s += "-"; + } else { + for (std::size_t k = 0; k < w.size(); ++k) { + char buf[32]; + std::snprintf(buf, sizeof(buf), "%s%g", k == 0 ? "" : ",", static_cast(w[k])); + weights_s += buf; + } + } + } + // x_equal: every plot's x range is plot 0's. cursors_aligned: every plot's two vertical cursors + // sit where plot 0's do. + int x_equal = 1; + int aligned = 1; + const RowSet::Row& r0 = rs.row(0); + if (r0.plot != nullptr) { + const plot::Range ref = r0.plot->x_range(r0.axis_x); + const double ref_a = r0.plot->cursor(r0.cursor_a).x; + const double ref_b = r0.plot->cursor(r0.cursor_b).x; + const double span = std::max(1e-12, std::fabs(ref.max - ref.min)); + for (int p = 1; p < rs.row_count(); ++p) { + const RowSet::Row& r = rs.row(p); + if (r.plot == nullptr) { x_equal = 0; aligned = 0; continue; } + const plot::Range x = r.plot->x_range(r.axis_x); + if (std::fabs(x.min - ref.min) > 1e-9 * span || std::fabs(x.max - ref.max) > 1e-9 * span) { + x_equal = 0; + } + if (std::fabs(r.plot->cursor(r.cursor_a).x - ref_a) > 1e-9 * span) aligned = 0; + if (std::fabs(r.plot->cursor(r.cursor_b).x - ref_b) > 1e-9 * span) aligned = 0; + } + } + std::string traces; + for (int p = 0; p < rs.row_count(); ++p) { + traces += p == 0 ? "" : "|"; + traces += rs.plot_label(p) + ":"; + bool any = false; + const RowSet::Row& row = rs.row(p); + for (int s = 0; s < RowSet::kSlotsPerRow; ++s) { + const std::string& n = row.signal_name[static_cast(s)]; + if (n.empty()) continue; + traces += (any ? "," : "") + n; + any = true; + } + if (!any) traces += "-"; + } + std::fprintf(stderr, + "[split-probe] %-14s plots=%d frows=%d cells=[%s] widths=[%s] tile=%d x_equal=%d " + "cursors_aligned=%d weights=[%s] traces=[%s]\n", + tag, rs.row_count(), frows, cells_s.c_str(), widths_s.c_str(), tile, x_equal, + aligned, weights_s.c_str(), traces.c_str()); + std::fflush(stderr); +} + +namespace { +/// After any structural change: the new figure is the area's, and the frame tick re-lays it out. +void split_probe_restructured(App& a) { + if (a.area != nullptr) a.area->set_figure(&a.rows->figure()); + a.relayout_pending = true; +} + +/// Channel `c` onto plot `p` (the first free slot), by the same name/colour route a drop takes. +void split_probe_assign(App& a, int p, std::size_t c) { + if (c >= a.model->channel_count() || p < 0 || p >= a.rows->row_count()) return; + const ChannelInfo& ch = a.model->channels()[c]; + a.rows->assign(p, 0, ch.name, ch.name, signal_color_index(a.model->signals(), ch.signal)); +} + +void split_probe_clear_all(App& a) { + for (int r = 0; r < RowSet::kMaxRows; ++r) { + for (int s = 0; s < RowSet::kSlotsPerRow; ++s) a.rows->assign(r, s, "", "", 0); + } +} +} // namespace + +void split_probe_next(App& a) { + ++a.dbg_split_step; + g_timeout_add(250, split_probe_step, &a); +} + +gboolean split_probe_step(gpointer user) { + App* a = static_cast(user); + if (a->window == nullptr || a->rows == nullptr) return G_SOURCE_REMOVE; + RowSet& rs = *a->rows; + switch (a->dbg_split_step) { + case 0: { + rs.set_cells_per_row({1}); + split_probe_clear_all(*a); + // The `enable_all_probe` assignment, in one call and onto plot 0's slots. + std::vector all(a->model->signals().size()); + for (std::size_t i = 0; i < all.size(); ++i) all[i] = i; + a->model->set_signals_enabled(all, true); + int slot = 0; + for (std::size_t c = 0; c < a->model->channel_count() && slot < RowSet::kSlotsPerRow; ++c) { + const ChannelInfo& ch = a->model->channels()[c]; + rs.assign(0, slot++, ch.name, ch.name, signal_color_index(a->model->signals(), ch.signal)); + } + split_probe_restructured(*a); + split_probe_next(*a); + break; + } + case 1: + split_probe_report(*a, "start"); + rs.insert_cell(0, 1); + split_probe_clear_all(*a); + split_probe_assign(*a, 0, 0); + split_probe_assign(*a, 1, 1); + rs.set_cursors_visible(true); + rs.reset_cursors(); + split_probe_restructured(*a); + split_probe_next(*a); + break; + case 2: + split_probe_report(*a, "split"); + rs.set_cell_weights(0, {3.0f, 1.0f}); + split_probe_restructured(*a); + split_probe_next(*a); + break; + case 3: + split_probe_report(*a, "weights 3:1"); + rs.insert_row(1); + rs.insert_cell(1, 1); + split_probe_restructured(*a); + split_probe_next(*a); + break; + case 4: { + split_probe_report(*a, "row+cell"); + // Fill row 0 to its cap, then keep adding rows to the plot cap; the first refusal of each kind + // is what is being read. + std::string cell_calls; + int refused_cell = 0; + for (int i = 0; i < RowSet::kMaxCellsPerRow + 1; ++i) { + const bool ok = rs.insert_cell(0, rs.cells_in_row(0)); + cell_calls += (i == 0 ? "" : ",") + std::to_string(ok ? 1 : 0); + if (!ok) { ++refused_cell; break; } + } + std::string row_calls; + int refused_row = 0; + for (int i = 0; i < RowSet::kMaxRows; ++i) { + const bool ok = rs.insert_row(rs.figure_row_count()); + row_calls += (i == 0 ? "" : ",") + std::to_string(ok ? 1 : 0); + if (!ok) { ++refused_row; break; } + } + std::fprintf(stderr, + "[split-probe] caps-calls insert_cell=[%s] refused_cell=%d insert_row=[%s] " + "refused_row=%d plots=%d\n", + cell_calls.c_str(), refused_cell, row_calls.c_str(), refused_row, rs.row_count()); + split_probe_restructured(*a); + split_probe_next(*a); + break; + } + case 5: + split_probe_report(*a, "caps"); + rs.remove_cell(rs.first_plot_of_row(0) + rs.cells_in_row(0) - 1); + split_probe_restructured(*a); + split_probe_next(*a); + break; + default: + split_probe_report(*a, "remove"); + break; + } + return G_SOURCE_REMOVE; +} + gboolean enable_all_probe(gpointer user) { App* a = static_cast(user); // In one call, the way a restored session does it: enabling them one at a time re-plans