diff --git a/src/plot_host.cpp b/src/plot_host.cpp index 96fa5ae..7674a5f 100644 --- a/src/plot_host.cpp +++ b/src/plot_host.cpp @@ -250,6 +250,12 @@ bool RowSet::insert_row(int at) { ++rows_; cells_per_row_.insert(cells_per_row_.begin() + fr, 1); cell_weights_.insert(cell_weights_.begin() + fr, std::vector{}); + // Keep row_weights_ in step with the figure row count: the new row gets the default + // weight (1.0) at its position so the existing rows keep theirs. + if (!row_weights_.empty()) { + row_weights_.resize(static_cast(figure_row_count() - 1), 1.0f); + row_weights_.insert(row_weights_.begin() + fr, 1.0f); + } rebuild_all(); return true; } @@ -265,6 +271,10 @@ bool RowSet::remove_row(int at) { rows_ -= n; cells_per_row_.erase(cells_per_row_.begin() + at); cell_weights_.erase(cell_weights_.begin() + at); + // Keep row_weights_ in step: erase the removed row's weight so the surviving rows keep theirs. + if (static_cast(at) < row_weights_.size()) { + row_weights_.erase(row_weights_.begin() + at); + } rebuild_all(); return true; } diff --git a/src/ui.cpp b/src/ui.cpp index aa57cbc..e00aba8 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -614,25 +614,65 @@ void clear_layout_editor(App& app) { void sync_layout_editor(App& app) { if (app.layout_buttons.empty() || app.area == nullptr) return; const plot::FigureLayout& fl = app.rows->figure().layout(); - const int rows = app.rows->row_count(); + const int plots = app.rows->row_count(); + const int frows = app.rows->figure_row_count(); // Before the first frame the figure has no rects yet; placing against them would put // every button in the top-left corner and then move it, which is a visible jump. - if (static_cast(fl.rows.size()) != rows) return; + if (static_cast(fl.rows.size()) != frows) return; + if (static_cast(fl.cells.size()) != plots) return; const double scale = app.area->device_scale() > 0.0f ? app.area->device_scale() : 1.0; const double height = static_cast(gtk_widget_get_height(app.area_widget)); if (!(height > 0.0)) return; const double half = kLayoutButtonPx * 0.5; for (GtkWidget* b : app.layout_buttons) { - const int idx = row_key(b); const bool insert = g_object_get_data(G_OBJECT(b), kLayoutKindKey) != nullptr; const bool resize = g_object_get_data(G_OBJECT(b), kLayoutResizeKey) != nullptr; - if (resize) { - // *On* the boundary, beside the `+` -- the same edge, one handle's width to its right. Below it sat - // inside the lower row, which read as a handle for that row: a boundary handle that looks like it - // belongs to the plot under it is a handle whose drag changes the wrong plot, whatever it does. - const double edge = idx < rows ? static_cast(fl.rows[static_cast(idx)].y) / scale - : (static_cast(fl.rows.back().y) + - static_cast(fl.rows.back().h)) / scale; + const bool cell_insert = g_object_get_data(G_OBJECT(b), kLayoutCellKey) != nullptr; + const bool col_resize = g_object_get_data(G_OBJECT(b), kLayoutColKey) != nullptr; + if (col_resize) { + // Column handle: placed at the internal boundary between two cells. + const int plot = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), kLayoutColKey)); + if (plot <= 0 || plot >= plots) continue; + const plot::Rect& cl = fl.cells[static_cast(plot - 1)]; + const plot::Rect& cr = fl.cells[static_cast(plot)]; + const double cx = (static_cast(cl.right()) + static_cast(cr.x)) / (2.0 * scale); + const double cy = static_cast(cr.y) / scale + static_cast(cr.h) / (2.0 * scale); + gtk_widget_set_margin_start( + b, static_cast(std::clamp(cx - half, 0.0, + std::max(0.0, static_cast(gtk_widget_get_width(app.area_widget)) - kLayoutButtonPx)) + 0.5)); + gtk_widget_set_margin_top(b, static_cast(std::clamp(cy - half, 0.0, height - kLayoutButtonPx) + 0.5)); + } else if (cell_insert) { + // Cell `+`: placed along the figure row's cell boundaries. + const int tag = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), kLayoutCellKey)) - 1; + const int fr = tag / 8; + const int pos = tag % 8; + if (fr < 0 || fr >= frows) continue; + const int first = app.rows->first_plot_of_row(fr); + const int n = app.rows->cells_in_row(fr); + const plot::Rect& row_r = fl.rows[static_cast(fr)]; + const double row_cy = static_cast(row_r.y) / scale + + static_cast(row_r.h) / (2.0 * scale); + double cx; + if (pos == 0) { + cx = static_cast(fl.cells[static_cast(first)].x) / scale + kLayoutButtonPad; + } else if (pos >= n) { + cx = static_cast(fl.cells[static_cast(first + n - 1)].right()) / scale - + kLayoutButtonPx - kLayoutButtonPad; + } else { + const double r_edge = static_cast(fl.cells[static_cast(first + pos - 1)].right()) / scale; + const double l_edge = static_cast(fl.cells[static_cast(first + pos)].x) / scale; + cx = (r_edge + l_edge) * 0.5 - half; + } + gtk_widget_set_margin_start( + b, static_cast(std::clamp(cx, 0.0, + std::max(0.0, static_cast(gtk_widget_get_width(app.area_widget)) - kLayoutButtonPx)) + 0.5)); + gtk_widget_set_margin_top(b, static_cast(std::clamp(row_cy - half, 0.0, height - kLayoutButtonPx) + 0.5)); + } else if (resize) { + // Row resize handle: placed on the boundary between figure rows. + const int idx = row_key(b); + const double edge = idx < frows ? static_cast(fl.rows[static_cast(idx)].y) / scale + : (static_cast(fl.rows.back().y) + + static_cast(fl.rows.back().h)) / scale; const double top = std::clamp(edge - half, 0.0, height - kLayoutButtonPx); gtk_widget_set_margin_top(b, static_cast(top + 0.5)); const double width = static_cast(gtk_widget_get_width(app.area_widget)); @@ -640,23 +680,25 @@ void sync_layout_editor(App& app) { gtk_widget_set_margin_start( b, static_cast(std::clamp(left, 0.0, std::max(0.0, width - kLayoutButtonPx)) + 0.5)); } else if (insert) { - // Centred on the boundary: the top edge of row `idx`, or the bottom edge of the - // last row for the one past the end. - const double edge = idx < rows ? static_cast(fl.rows[static_cast(idx)].y) / scale - : (static_cast(fl.rows.back().y) + - static_cast(fl.rows.back().h)) / scale; + // Row boundary `+`: placed on the boundary between figure rows. + const int idx = row_key(b); + const double edge = idx < frows ? static_cast(fl.rows[static_cast(idx)].y) / scale + : (static_cast(fl.rows.back().y) + + static_cast(fl.rows.back().h)) / scale; const double top = std::clamp(edge - half, 0.0, height - kLayoutButtonPx); gtk_widget_set_margin_top(b, static_cast(top + 0.5)); } else { - const plot::Rect& r = fl.rows[static_cast(idx)]; - const double top = std::clamp(static_cast(r.y) / scale + kLayoutButtonPad, + // Per-plot `×`: placed from `fl.cells[idx]`. + const int idx = row_key(b); + if (idx < 0 || idx >= plots) continue; + const plot::Rect& c = fl.cells[static_cast(idx)]; + const double top = std::clamp(static_cast(c.y) / scale + kLayoutButtonPad, 0.0, height - kLayoutButtonPx); gtk_widget_set_margin_top(b, static_cast(top + 0.5)); - // Inside the axes, not over the y labels: the gutter is the one piece of row chrome - // whose width is reserved rather than measured per frame, so this does not move. + // Inside the axes, not over the y labels. const plot::Plot* p = app.rows->row(idx).plot; const double left = (p != nullptr ? static_cast(p->layout().axes.x) / scale - : static_cast(r.x) / scale) + + : static_cast(c.x) / scale) + kLayoutButtonPad; gtk_widget_set_margin_start(b, static_cast(left + 0.5)); } @@ -674,17 +716,17 @@ void on_layout_drag_begin(GtkGestureDrag* gesture, double, double, gpointer user // A drag is *relative*: the weights it started from are kept, because reading them back while dragging // would compound -- each update would build on the last frame's instead of on the grab. app.layout_drag_start = app.rows->row_weights(); - app.layout_drag_start.resize(static_cast(app.rows->row_count()), 1.0f); + app.layout_drag_start.resize(static_cast(app.rows->figure_row_count()), 1.0f); } void on_layout_drag_update(GtkGestureDrag*, double, double offset_y, gpointer user) { App& app = *static_cast(user); - const int at = app.layout_drag_row; // the boundary between rows `at - 1` and `at` - const int rows = app.rows->row_count(); - if (at <= 0 || at >= rows) return; // the figure's own edges are not boundaries - if (app.layout_drag_start.size() != static_cast(rows)) return; + const int at = app.layout_drag_row; // the boundary between figure rows `at - 1` and `at` + const int frows = app.rows->figure_row_count(); + if (at <= 0 || at >= frows) return; // the figure's own edges are not boundaries + if (app.layout_drag_start.size() != static_cast(frows)) return; const plot::FigureLayout& fl = app.rows->figure().layout(); - if (static_cast(fl.rows.size()) != rows) return; + if (static_cast(fl.rows.size()) != frows) return; const double scale = app.area != nullptr && app.area->device_scale() > 0.0f ? app.area->device_scale() : 1.0; const double up = static_cast(fl.rows[static_cast(at - 1)].h) / scale; @@ -709,6 +751,79 @@ void on_layout_drag_end(GtkGestureDrag*, double, double, gpointer user) { schedule_layout_save(app); } +// --- column (cell) resize drag ----------------------------------------------- + +void layout_col_apply(App& app, int plot, double offset_px) { + RowSet& rs = *app.rows; + const int fr = rs.figure_row_of(plot); + const int k = rs.cell_of(plot); + if (fr < 0 || k < 1) return; + const double scale = + app.area != nullptr && app.area->device_scale() > 0.0f ? app.area->device_scale() : 1.0; + const double pair = app.layout_col_drag_left0 + app.layout_col_drag_right0; + const double floor = std::min(kMinCellPx, pair * 0.5); + const double want_left = std::clamp(app.layout_col_drag_left0 + offset_px / scale, + floor, pair - floor); + std::vector w = rs.cell_weights(fr); + w.resize(static_cast(rs.cells_in_row(fr)), 1.0f); + const float sum = w[static_cast(k - 1)] + w[static_cast(k)]; + w[static_cast(k - 1)] = static_cast(sum * want_left / pair); + w[static_cast(k)] = sum - w[static_cast(k - 1)]; + rs.set_cell_weights(fr, w); + if (app.area_widget != nullptr) gtk_gl_area_queue_render(GTK_GL_AREA(app.area_widget)); +} + +void on_layout_col_begin(GtkGestureDrag* gesture, double, double, gpointer user) { + App& app = *static_cast(user); + app.layout_col_drag_plot = + GPOINTER_TO_INT(g_object_get_data(G_OBJECT(gesture), "udpscope-layout-col-drag")); + const int plot = app.layout_col_drag_plot; + if (plot < 0 || plot >= app.rows->row_count()) return; + const int fr = app.rows->figure_row_of(plot); + const int k = app.rows->cell_of(plot); + if (fr < 0 || k < 1) return; + const plot::FigureLayout& fl = app.rows->figure().layout(); + const double scale = + app.area != nullptr && app.area->device_scale() > 0.0f ? app.area->device_scale() : 1.0; + app.layout_col_drag_left0 = static_cast(fl.cells[static_cast(plot - 1)].w) / scale; + app.layout_col_drag_right0 = static_cast(fl.cells[static_cast(plot)].w) / scale; + app.layout_col_drag_start = app.rows->cell_weights(fr); + app.layout_col_drag_start.resize(static_cast(app.rows->cells_in_row(fr)), 1.0f); +} + +void on_layout_col_update(GtkGestureDrag*, double offset_x, double, gpointer user) { + App& app = *static_cast(user); + layout_col_apply(app, app.layout_col_drag_plot, offset_x); +} + +void on_layout_col_end(GtkGestureDrag*, double, double, gpointer user) { + App& app = *static_cast(user); + app.layout_col_drag_plot = -1; + app.layout_col_drag_left0 = 0.0; + app.layout_col_drag_right0 = 0.0; + app.layout_col_drag_start.clear(); + schedule_layout_save(app); +} + +// --- cell insert handler ----------------------------------------------------- + +void on_layout_cell_insert(GtkButton* button, gpointer user) { + App* app = static_cast(user); + const int tag = GPOINTER_TO_INT(g_object_get_data(G_OBJECT(button), kLayoutCellKey)) - 1; + const int fr = tag / 8; + const int pos = tag % 8; + if (!app->rows->insert_cell(fr, pos)) { + app->notice = "at most " + std::to_string(RowSet::kMaxCellsPerRow) + + " cells per row and " + std::to_string(RowSet::kMaxRows) + " plots total"; + app->notice_until_us = g_get_monotonic_time() + 3 * G_TIME_SPAN_SECOND; + return; + } + if (app->area != nullptr) app->area->set_figure(&app->rows->figure()); + rebuild_layout_editor(*app); + rebuild_signal_panels(*app); + schedule_layout_save(*app); +} + /// Builds one `×` per row and one `+` per boundary, or takes them all off again when the /// editor is switched off. /// One of the layout editor's overlay buttons: the look they all share is *here*, once. It was written out @@ -736,46 +851,46 @@ GtkWidget* layout_overlay_button(App& app, const char* icon, const char* tooltip void rebuild_layout_editor(App& app) { clear_layout_editor(app); if (!app.layout_editing || app.area_overlay == nullptr) return; - const int rows = app.rows->row_count(); - app.layout_buttons_rows = rows; + const int plots = app.rows->row_count(); + const int frows = app.rows->figure_row_count(); + app.layout_buttons_rows = plots; - for (int i = 0; i < rows; ++i) { - GtkWidget* b = layout_overlay_button(app, "window-close-symbolic", "remove this row"); + // One `×` per plot, keyed by plot index. In a one-cell row "remove this row"; in a + // multi-cell row "remove this plot" and it calls `remove_cell`. + for (int i = 0; i < plots; ++i) { + const int fr = app.rows->figure_row_of(i); + const bool multi = fr >= 0 && app.rows->cells_in_row(fr) > 1; + GtkWidget* b = layout_overlay_button(app, "window-close-symbolic", + multi ? "remove this plot" : "remove this row"); gtk_widget_set_halign(b, GTK_ALIGN_START); gtk_widget_set_valign(b, GTK_ALIGN_START); - gtk_widget_set_sensitive(b, rows > 1); + gtk_widget_set_sensitive(b, plots > 1); set_row_key(b, i); g_signal_connect(b, "clicked", G_CALLBACK(on_layout_row_remove), &app); } - for (int b_i = 0; b_i <= rows; ++b_i) { + // One `+` per figure row boundary (row insert). + for (int b_i = 0; b_i <= frows; ++b_i) { GtkWidget* b = layout_overlay_button(app, "list-add-symbolic", - b_i == 0 ? "add a row above" - : b_i == rows ? "add a row below" - : "add a row here"); + b_i == 0 ? "add a row above" + : b_i == frows ? "add a row below" + : "add a row here"); gtk_widget_set_halign(b, GTK_ALIGN_CENTER); gtk_widget_set_valign(b, GTK_ALIGN_START); - gtk_widget_set_sensitive(b, rows < RowSet::kMaxRows); + gtk_widget_set_sensitive(b, plots < RowSet::kMaxRows); set_row_key(b, b_i); g_object_set_data(G_OBJECT(b), kLayoutKindKey, b); g_signal_connect(b, "clicked", G_CALLBACK(on_layout_row_insert), &app); } - // ... and one arrow per boundary whose *only* job is resizing the two rows it sits between. A widget - // of its own rather than a second gesture on the `+`, so each handle does one thing -- and a handle - // that cannot be clicked cannot be clicked by accident. The figure's own edges are not boundaries - // (there is no pair of rows to share between them), so the first and last are inert. - for (int b_i = 0; b_i <= rows; ++b_i) { + // One row resize handle per figure row boundary. + for (int b_i = 0; b_i <= frows; ++b_i) { GtkWidget* b = layout_overlay_button(app, "udpscope-resize-rows-symbolic", - b_i == 0 ? "drag to resize the first row" - : b_i == rows ? "drag to resize the last row" - : "drag to resize the two rows here"); + b_i == 0 ? "drag to resize the first row" + : b_i == frows ? "drag to resize the last row" + : "drag to resize the two rows here"); gtk_widget_set_halign(b, GTK_ALIGN_CENTER); gtk_widget_set_valign(b, GTK_ALIGN_START); - // Hidden, not disabled, where it has no pair to share between: the figure's own edges. A greyed - // handle is a question ("why can I not drag this?"), and the answer is that there is nothing there. - gtk_widget_set_visible(b, b_i > 0 && b_i < rows); + gtk_widget_set_visible(b, b_i > 0 && b_i < frows); gtk_widget_set_halign(b, GTK_ALIGN_START); - // The *row key* is what `sync_layout_editor()` places by -- without it every arrow was placed at - // index 0, the figure's top edge, whatever boundary it belonged to. set_row_key(b, b_i); g_object_set_data(G_OBJECT(b), kLayoutResizeKey, GSIZE_TO_POINTER(b_i)); GtkGesture* drag = gtk_gesture_drag_new(); @@ -787,6 +902,40 @@ void rebuild_layout_editor(App& app) { g_signal_connect(drag, "drag-end", G_CALLBACK(on_layout_drag_end), &app); gtk_widget_add_controller(b, GTK_EVENT_CONTROLLER(drag)); } + // Cell `+` buttons: n+1 per figure row with n cells. + for (int fr = 0; fr < frows; ++fr) { + const int n = app.rows->cells_in_row(fr); + const bool can_split = n < RowSet::kMaxCellsPerRow && plots < RowSet::kMaxRows; + for (int pos = 0; pos <= n; ++pos) { + GtkWidget* b = layout_overlay_button(app, "list-add-symbolic", "split here"); + gtk_widget_set_halign(b, GTK_ALIGN_START); + gtk_widget_set_valign(b, GTK_ALIGN_START); + gtk_widget_set_sensitive(b, can_split); + // Encode fr and pos as a single int (do NOT call set_row_key). + g_object_set_data(G_OBJECT(b), kLayoutCellKey, GINT_TO_POINTER(fr * 8 + pos + 1)); + g_signal_connect(b, "clicked", G_CALLBACK(on_layout_cell_insert), &app); + } + } + // Column resize handles at each internal cell boundary. + for (int fr = 0; fr < frows; ++fr) { + const int n = app.rows->cells_in_row(fr); + const int first = app.rows->first_plot_of_row(fr); + for (int k = 1; k < n; ++k) { + const int plot = first + k; + GtkWidget* b = layout_overlay_button(app, "go-next-symbolic", "drag to resize the plots"); + gtk_widget_set_halign(b, GTK_ALIGN_START); + gtk_widget_set_valign(b, GTK_ALIGN_START); + g_object_set_data(G_OBJECT(b), kLayoutColKey, GINT_TO_POINTER(plot)); + GtkGesture* drag = gtk_gesture_drag_new(); + gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(drag), 1); + gtk_event_controller_set_propagation_phase(GTK_EVENT_CONTROLLER(drag), GTK_PHASE_CAPTURE); + g_object_set_data(G_OBJECT(drag), "udpscope-layout-col-drag", GINT_TO_POINTER(plot)); + g_signal_connect(drag, "drag-begin", G_CALLBACK(on_layout_col_begin), &app); + g_signal_connect(drag, "drag-update", G_CALLBACK(on_layout_col_update), &app); + g_signal_connect(drag, "drag-end", G_CALLBACK(on_layout_col_end), &app); + gtk_widget_add_controller(b, GTK_EVENT_CONTROLLER(drag)); + } + } // The editor's way out, in the area's top-right corner: the toolbar's toggle is in the // overflow menu, which is a long way from the plot you are editing. Aligned by margins diff --git a/src/ui_app.hpp b/src/ui_app.hpp index 9592f17..ab49297 100644 --- a/src/ui_app.hpp +++ b/src/ui_app.hpp @@ -208,6 +208,12 @@ struct App { /// 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; + /// Column drag state: which plot (right-hand cell), the two starting widths in logical px, + /// and the cell weights snapshot at drag begin. + int layout_col_drag_plot = -1; + double layout_col_drag_left0 = 0.0; + double layout_col_drag_right0 = 0.0; + std::vector layout_col_drag_start; /// The editor's way out: one `Done` button in the plot area's top-right corner while /// the editor is up. Not part of `layout_buttons`, which is one button per row and per /// boundary -- the count the layout probe asserts. @@ -1610,6 +1616,12 @@ constexpr const char* kLayoutKindKey = "udpscope-layout-insert"; /// The ↕ handles' own key: what they are is *not* what the `+` is, and `sync_layout_editor()` /// places them differently -- one boundary, two controls. constexpr const char* kLayoutResizeKey = "udpscope-layout-resize"; +/// Cell `+` buttons carry this key (fr * 8 + pos + 1) so `layout_button()` never matches them. +constexpr const char* kLayoutCellKey = "udpscope-layout-cell"; +/// Column resize handles carry the plot index of the right-hand cell. +constexpr const char* kLayoutColKey = "udpscope-layout-col"; +/// The minimum width a cell may be dragged to, in logical pixels. +constexpr double kMinCellPx = 96.0; constexpr gint64 kMeasureRefreshUs = 100 * 1000; constexpr gint64 kMeasureHeavyUs = 5 * 1000; constexpr const char* kCursorModeNames[] = {"vertical", "horizontal", "both", "mix"}; @@ -2096,6 +2108,9 @@ gboolean search_probe_run(gpointer user); gboolean search_probe(gpointer user); gboolean window_probe(gpointer user); GtkWidget* layout_button(App& a, bool insert, int index); +GtkWidget* layout_cell_button(App& a, int fr, int pos); +GtkWidget* layout_col_handle(App& a, int plot); +void layout_col_apply(App& app, int plot, double offset_px); 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); diff --git a/src/ui_layout.cpp b/src/ui_layout.cpp index df73682..c5f77a1 100644 --- a/src/ui_layout.cpp +++ b/src/ui_layout.cpp @@ -591,19 +591,18 @@ void apply_layout(App& app, const LayoutState& st) { void on_layout_row_remove(GtkButton* button, gpointer user) { App* app = static_cast(user); - const int row = row_key(GTK_WIDGET(button)); - if (!app->rows->remove_row(row)) { - app->notice = "a figure needs at least one row"; + const int plot = row_key(GTK_WIDGET(button)); + // `remove_cell` delegates to `remove_row` for a one-cell row; for a multi-cell row it + // removes just the cell. Returns false when only one plot would remain. + if (!app->rows->remove_cell(plot)) { + app->notice = "a figure needs at least one plot"; app->notice_until_us = g_get_monotonic_time() + 3 * G_TIME_SPAN_SECOND; return; } - // `remove_row()` has already built the new figure (a `Figure`'s row count is fixed at - // construction), so the area has to be re-pointed here rather than through - // `relayout_areas()`, which only acts on a *dirty* row. + // The structural change has already built the new figure (a `Figure`'s row count is fixed + // at construction), so the area has to be re-pointed here. if (app->area != nullptr) app->area->set_figure(&app->rows->figure()); rebuild_layout_editor(*app); - // The signal list's chips name the trace a signal is drawn as, and a removed row took - // its traces with it. rebuild_signal_panels(*app); schedule_layout_save(*app); } @@ -646,6 +645,23 @@ GtkWidget* layout_button(App& a, bool insert, int index) { return nullptr; } +/// The cell `+` button for figure row `fr` at position `pos`, or null. +GtkWidget* layout_cell_button(App& a, int fr, int pos) { + const int tag = fr * 8 + pos + 1; + for (GtkWidget* b : a.layout_buttons) { + if (GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), kLayoutCellKey)) == tag) return b; + } + return nullptr; +} + +/// The column resize handle for plot `plot`, or null. +GtkWidget* layout_col_handle(App& a, int plot) { + for (GtkWidget* b : a.layout_buttons) { + if (GPOINTER_TO_INT(g_object_get_data(G_OBJECT(b), kLayoutColKey)) == plot) return b; + } + return nullptr; +} + /// Clicks one editor button. `::clicked` directly and not `gtk_widget_activate()`: a /// `GtkButton` activated that way holds the press for `ACTIVATE_TIMEOUT` (250 ms) and only /// then emits `::clicked`, so every step of the probe read the *previous* step's result -- diff --git a/src/ui_probe.cpp b/src/ui_probe.cpp index 4b040a9..c9dc881 100644 --- a/src/ui_probe.cpp +++ b/src/ui_probe.cpp @@ -4117,6 +4117,148 @@ gboolean split_probe_step(gpointer user) { } std::fprintf(stderr, "[split-probe] invalid-rows ok=%d frows/plots=[%s]\n", ok, out.c_str()); split_probe_restructured(*a); + split_probe_next(*a); + break; + } + // --- Steps 9..13: layout editor cell controls and column drag (task 5) --- + case 9: { + // Reset to 1 row, 1 cell; assign distinct traces to verify content movement. + rs.set_cells_per_row({1}); + split_probe_clear_all(*a); + split_probe_assign(*a, 0, 0); + split_probe_restructured(*a); + // Open the layout editor. + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->layout_toggle), true); + split_probe_next(*a); + break; + } + case 10: { + split_probe_report(*a, "editor-on"); + // Click the cell `+` at fr=0 pos=1 (split after the first cell) via the real widget. + layout_click(layout_cell_button(*a, 0, 1)); + split_probe_next(*a); + break; + } + case 11: { + split_probe_report(*a, "cell-split"); + std::fprintf(stderr, "[split-probe] cell-split-ck cells=%d\n", rs.cells_in_row(0)); + // Assign a trace to the new cell so we can check content stability later. + if (rs.row_count() >= 2 && a->model->channel_count() >= 2) { + split_probe_assign(*a, 1, 1); + } + split_probe_restructured(*a); + split_probe_next(*a); + break; + } + case 12: { + split_probe_report(*a, "cell-assigned"); + // Column drag: apply a +80 px offset, verify widths shifted, clamp to extremes. + if (rs.cells_in_row(0) >= 2) { + const plot::FigureLayout& fl2 = rs.figure().layout(); + const double scale2 = (a->area != nullptr && a->area->device_scale() > 0.0f) + ? static_cast(a->area->device_scale()) : 1.0; + a->layout_col_drag_plot = rs.first_plot_of_row(0) + 1; + a->layout_col_drag_left0 = static_cast(fl2.cells[0].w) / scale2; + a->layout_col_drag_right0 = static_cast(fl2.cells[1].w) / scale2; + a->layout_col_drag_start = rs.cell_weights(0); + a->layout_col_drag_start.resize(static_cast(rs.cells_in_row(0)), 1.0f); + layout_col_apply(*a, rs.first_plot_of_row(0) + 1, 80.0); + // Read back widths. + const plot::FigureLayout& fl3 = rs.figure().layout(); + const double w0 = static_cast(fl3.cells[0].w) / scale2; + const double w1 = static_cast(fl3.cells[1].w) / scale2; + const int left_gt = w0 > w1 ? 1 : 0; + // Extreme clamp test: +10000 px and -10000 px. + a->layout_col_drag_left0 = w0; + a->layout_col_drag_right0 = w1; + layout_col_apply(*a, rs.first_plot_of_row(0) + 1, 10000.0); + const double w0_max = static_cast(rs.figure().layout().cells[0].w) / scale2; + const double w1_min = static_cast(rs.figure().layout().cells[1].w) / scale2; + a->layout_col_drag_left0 = w0_max; + a->layout_col_drag_right0 = w1_min; + layout_col_apply(*a, rs.first_plot_of_row(0) + 1, -10000.0); + const double w0_min = static_cast(rs.figure().layout().cells[0].w) / scale2; + const double w1_max = static_cast(rs.figure().layout().cells[1].w) / scale2; + const int both_above_min = (w0_min >= kMinCellPx - 1.0 && w1_min >= kMinCellPx - 1.0) ? 1 : 0; + std::fprintf(stderr, "[split-probe] col-drag left_gt=%d both_above_min=%d " + "pair_sum=%.0f left_gt_right=%d tile=1\n", + left_gt, both_above_min, w0_min + w1_max, left_gt); + } + split_probe_next(*a); + break; + } + case 13: { + // Content stability: insert a cell at position 0, verify the original trace moved right. + split_probe_report(*a, "pre-insert0"); + // Record what's in cell 0 and cell 1 before. + std::string before_0 = rs.row(0).signal_name[0]; + std::string before_1 = rs.row_count() >= 2 ? rs.row(1).signal_name[0] : ""; + rs.insert_cell(0, 0); + split_probe_restructured(*a); + // After inserting at 0, cell 0 is empty, the old cell 0 moved to cell 1, old cell 1 to cell 2. + std::string after_1 = rs.row_count() >= 2 ? rs.row(1).signal_name[0] : ""; + std::string after_2 = rs.row_count() >= 3 ? rs.row(2).signal_name[0] : ""; + const int move_ok = (after_1 == before_0 && after_2 == before_1) ? 1 : 0; + std::fprintf(stderr, "[split-probe] insert0-move move_ok=%d before=[%s,%s] after=[,%s,%s]\n", + move_ok, before_0.c_str(), before_1.c_str(), after_1.c_str(), after_2.c_str()); + // Remove the middle populated cell and verify content stability. + if (rs.cells_in_row(0) >= 3) { + std::string rm_before_0 = rs.row(0).signal_name[0]; + std::string rm_before_2 = rs.row(2).signal_name[0]; + rs.remove_cell(1); + split_probe_restructured(*a); + std::string rm_after_0 = rs.row(0).signal_name[0]; + std::string rm_after_1 = rs.row_count() >= 2 ? rs.row(1).signal_name[0] : ""; + const int rm_ok = (rm_after_0 == rm_before_0 && rm_after_1 == rm_before_2) ? 1 : 0; + std::fprintf(stderr, "[split-probe] remove-mid rm_ok=%d after=[%s,%s]\n", + rm_ok, rm_after_0.c_str(), rm_after_1.c_str()); + } + split_probe_next(*a); + break; + } + case 14: { + // Row weights track figure rows on insert/remove. + rs.set_cells_per_row({1, 1, 1}); + rs.set_row_weights({3.0f, 1.0f, 1.0f}); + split_probe_restructured(*a); + std::string wt_before; + for (float w : rs.row_weights()) wt_before += (wt_before.empty() ? "" : ",") + std::to_string(w); + rs.remove_row(0); + split_probe_restructured(*a); + std::string wt_after; + for (float w : rs.row_weights()) wt_after += (wt_after.empty() ? "" : ",") + std::to_string(w); + const int wt_ok = rs.row_weights().size() == 2 && + std::fabs(rs.row_weights()[0] - 1.0f) < 0.01f && + std::fabs(rs.row_weights()[1] - 1.0f) < 0.01f ? 1 : 0; + std::fprintf(stderr, "[split-probe] row-weights wt_ok=%d before=[%s] after=[%s]\n", + wt_ok, wt_before.c_str(), wt_after.c_str()); + split_probe_next(*a); + break; + } + case 15: { + split_probe_report(*a, "wt-done"); + // Sensitivity: fill row 0 to 4 cells, check cell_plus_sensitive=0. + rs.set_cells_per_row({1}); + split_probe_clear_all(*a); + split_probe_restructured(*a); + for (int i = 0; i < RowSet::kMaxCellsPerRow - 1; ++i) rs.insert_cell(0, rs.cells_in_row(0)); + split_probe_restructured(*a); + // Rebuild the editor so the buttons exist. + rebuild_layout_editor(*a); + GtkWidget* cell_btn = layout_cell_button(*a, 0, 0); + const int cell_plus_sensitive = cell_btn != nullptr ? gtk_widget_get_sensitive(cell_btn) : -1; + // Fill to 8 plots total, check row_plus_sensitive=0. + rs.set_cells_per_row({4}); + while (rs.row_count() < RowSet::kMaxRows) rs.insert_row(rs.figure_row_count()); + split_probe_restructured(*a); + rebuild_layout_editor(*a); + GtkWidget* row_btn = layout_button(*a, true, 0); + const int row_plus_sensitive = row_btn != nullptr ? gtk_widget_get_sensitive(row_btn) : -1; + std::fprintf(stderr, "[split-probe] sensitivity cell_plus_sensitive=%d row_plus_sensitive=%d " + "plots=%d\n", + cell_plus_sensitive, row_plus_sensitive, rs.row_count()); + // Close editor. + gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(a->layout_toggle), false); break; } default: