Feature/source plugins #1
No files matched your search
@@ -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<float>{});
|
||||
// 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<std::size_t>(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<std::size_t>(at) < row_weights_.size()) {
|
||||
row_weights_.erase(row_weights_.begin() + at);
|
||||
}
|
||||
rebuild_all();
|
||||
return true;
|
||||
}
|
||||
|
||||
+198
-49
@@ -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<int>(fl.rows.size()) != rows) return;
|
||||
if (static_cast<int>(fl.rows.size()) != frows) return;
|
||||
if (static_cast<int>(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<double>(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<double>(fl.rows[static_cast<std::size_t>(idx)].y) / scale
|
||||
: (static_cast<double>(fl.rows.back().y) +
|
||||
static_cast<double>(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<std::size_t>(plot - 1)];
|
||||
const plot::Rect& cr = fl.cells[static_cast<std::size_t>(plot)];
|
||||
const double cx = (static_cast<double>(cl.right()) + static_cast<double>(cr.x)) / (2.0 * scale);
|
||||
const double cy = static_cast<double>(cr.y) / scale + static_cast<double>(cr.h) / (2.0 * scale);
|
||||
gtk_widget_set_margin_start(
|
||||
b, static_cast<int>(std::clamp(cx - half, 0.0,
|
||||
std::max(0.0, static_cast<double>(gtk_widget_get_width(app.area_widget)) - kLayoutButtonPx)) + 0.5));
|
||||
gtk_widget_set_margin_top(b, static_cast<int>(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<std::size_t>(fr)];
|
||||
const double row_cy = static_cast<double>(row_r.y) / scale +
|
||||
static_cast<double>(row_r.h) / (2.0 * scale);
|
||||
double cx;
|
||||
if (pos == 0) {
|
||||
cx = static_cast<double>(fl.cells[static_cast<std::size_t>(first)].x) / scale + kLayoutButtonPad;
|
||||
} else if (pos >= n) {
|
||||
cx = static_cast<double>(fl.cells[static_cast<std::size_t>(first + n - 1)].right()) / scale -
|
||||
kLayoutButtonPx - kLayoutButtonPad;
|
||||
} else {
|
||||
const double r_edge = static_cast<double>(fl.cells[static_cast<std::size_t>(first + pos - 1)].right()) / scale;
|
||||
const double l_edge = static_cast<double>(fl.cells[static_cast<std::size_t>(first + pos)].x) / scale;
|
||||
cx = (r_edge + l_edge) * 0.5 - half;
|
||||
}
|
||||
gtk_widget_set_margin_start(
|
||||
b, static_cast<int>(std::clamp(cx, 0.0,
|
||||
std::max(0.0, static_cast<double>(gtk_widget_get_width(app.area_widget)) - kLayoutButtonPx)) + 0.5));
|
||||
gtk_widget_set_margin_top(b, static_cast<int>(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<double>(fl.rows[static_cast<std::size_t>(idx)].y) / scale
|
||||
: (static_cast<double>(fl.rows.back().y) +
|
||||
static_cast<double>(fl.rows.back().h)) / scale;
|
||||
const double top = std::clamp(edge - half, 0.0, height - kLayoutButtonPx);
|
||||
gtk_widget_set_margin_top(b, static_cast<int>(top + 0.5));
|
||||
const double width = static_cast<double>(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<int>(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<double>(fl.rows[static_cast<std::size_t>(idx)].y) / scale
|
||||
: (static_cast<double>(fl.rows.back().y) +
|
||||
static_cast<double>(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<double>(fl.rows[static_cast<std::size_t>(idx)].y) / scale
|
||||
: (static_cast<double>(fl.rows.back().y) +
|
||||
static_cast<double>(fl.rows.back().h)) / scale;
|
||||
const double top = std::clamp(edge - half, 0.0, height - kLayoutButtonPx);
|
||||
gtk_widget_set_margin_top(b, static_cast<int>(top + 0.5));
|
||||
} else {
|
||||
const plot::Rect& r = fl.rows[static_cast<std::size_t>(idx)];
|
||||
const double top = std::clamp(static_cast<double>(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<std::size_t>(idx)];
|
||||
const double top = std::clamp(static_cast<double>(c.y) / scale + kLayoutButtonPad,
|
||||
0.0, height - kLayoutButtonPx);
|
||||
gtk_widget_set_margin_top(b, static_cast<int>(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<double>(p->layout().axes.x) / scale
|
||||
: static_cast<double>(r.x) / scale) +
|
||||
: static_cast<double>(c.x) / scale) +
|
||||
kLayoutButtonPad;
|
||||
gtk_widget_set_margin_start(b, static_cast<int>(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<std::size_t>(app.rows->row_count()), 1.0f);
|
||||
app.layout_drag_start.resize(static_cast<std::size_t>(app.rows->figure_row_count()), 1.0f);
|
||||
}
|
||||
|
||||
void on_layout_drag_update(GtkGestureDrag*, double, double offset_y, gpointer user) {
|
||||
App& app = *static_cast<App*>(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<std::size_t>(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<std::size_t>(frows)) return;
|
||||
const plot::FigureLayout& fl = app.rows->figure().layout();
|
||||
if (static_cast<int>(fl.rows.size()) != rows) return;
|
||||
if (static_cast<int>(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<double>(fl.rows[static_cast<std::size_t>(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<float> w = rs.cell_weights(fr);
|
||||
w.resize(static_cast<std::size_t>(rs.cells_in_row(fr)), 1.0f);
|
||||
const float sum = w[static_cast<std::size_t>(k - 1)] + w[static_cast<std::size_t>(k)];
|
||||
w[static_cast<std::size_t>(k - 1)] = static_cast<float>(sum * want_left / pair);
|
||||
w[static_cast<std::size_t>(k)] = sum - w[static_cast<std::size_t>(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<App*>(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<double>(fl.cells[static_cast<std::size_t>(plot - 1)].w) / scale;
|
||||
app.layout_col_drag_right0 = static_cast<double>(fl.cells[static_cast<std::size_t>(plot)].w) / scale;
|
||||
app.layout_col_drag_start = app.rows->cell_weights(fr);
|
||||
app.layout_col_drag_start.resize(static_cast<std::size_t>(app.rows->cells_in_row(fr)), 1.0f);
|
||||
}
|
||||
|
||||
void on_layout_col_update(GtkGestureDrag*, double offset_x, double, gpointer user) {
|
||||
App& app = *static_cast<App*>(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<App*>(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<App*>(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
|
||||
|
||||
@@ -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<float> 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<float> layout_col_drag_start;
|
||||
/// The editor's way out: one `Done` button in the plot area's top-right corner while
|
||||
/// the editor is up. Not part of `layout_buttons`, which is one button per row and per
|
||||
/// boundary -- the count the layout probe asserts.
|
||||
@@ -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);
|
||||
|
||||
+24
-8
@@ -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<App*>(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 --
|
||||
|
||||
@@ -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<double>(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<double>(fl2.cells[0].w) / scale2;
|
||||
a->layout_col_drag_right0 = static_cast<double>(fl2.cells[1].w) / scale2;
|
||||
a->layout_col_drag_start = rs.cell_weights(0);
|
||||
a->layout_col_drag_start.resize(static_cast<std::size_t>(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<double>(fl3.cells[0].w) / scale2;
|
||||
const double w1 = static_cast<double>(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<double>(rs.figure().layout().cells[0].w) / scale2;
|
||||
const double w1_min = static_cast<double>(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<double>(rs.figure().layout().cells[0].w) / scale2;
|
||||
const double w1_max = static_cast<double>(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:
|
||||
|
||||
Reference in new issue
Block a user