fix: a port hit names its own node, so the first click of a wire lands
\`math_port_at()\` returned a sentinel for an output hit and the caller recovered the node with a box test -- but the output square straddles the box's right edge, so every pixel past it disarmed the gesture silently. That is the whole of "edges cannot be added". Cancel also closed with \`gtk_window_destroy()\`, which emits no \`close-request\`, so the dialog pointer was left dangling and no channel could be edited twice. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
1 parent
0866b744ad
commit
84f310ee43
1 file changed
+85
-28
+85
-28
@@ -3356,21 +3356,36 @@ int math_node_at(const MathDialog* d, double x, double y) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The port under the pointer: `-1` none, `-2` the selected node's output, otherwise the index of
|
/// A port's identity: `kPortOut` for a node's output, 0 or 1 for one of its inputs.
|
||||||
/// the node whose *input* `which` was hit.
|
constexpr int kPortOut = -1;
|
||||||
int math_port_at(const MathDialog* d, double x, double y, int& which) {
|
|
||||||
which = 0;
|
/// The port under a canvas position: the **node index** it belongs to, with `which` set to
|
||||||
for (std::size_t i = 0; i < d->nodes.size(); ++i) {
|
/// `kPortOut` or the input number; `-1` when there is no port there. `tol` is the hit radius in
|
||||||
const MathNode& n = d->nodes[i];
|
/// the same units as `x`/`y`.
|
||||||
|
///
|
||||||
|
/// It returns the node rather than a sentinel because the output square is drawn *straddling* the
|
||||||
|
/// box's right edge (centred on `n.x + kNodeW`, half-width `kPort * 0.5`) and hit with a radius of
|
||||||
|
/// `kPort` -- so a caller that recovered the node with `math_node_at()`, which matches only inside
|
||||||
|
/// the box, got `-1` for every pixel past the edge. That was a silent disarm of the first click of
|
||||||
|
/// a wiring gesture, and it is the whole of "edges cannot be added".
|
||||||
|
///
|
||||||
|
/// Topmost-first, like `math_node_at()`: when two boxes overlap, the one drawn on top owns the port.
|
||||||
|
int math_port_at(const MathDialog* d, double x, double y, double tol, int& which) {
|
||||||
|
which = kPortOut;
|
||||||
|
for (int i = static_cast<int>(d->nodes.size()) - 1; i >= 0; --i) {
|
||||||
|
const MathNode& n = d->nodes[static_cast<std::size_t>(i)];
|
||||||
double px = 0.0;
|
double px = 0.0;
|
||||||
double py = 0.0;
|
double py = 0.0;
|
||||||
math_output_pos(n, px, py);
|
math_output_pos(n, px, py);
|
||||||
if (std::fabs(x - px) <= kPort && std::fabs(y - py) <= kPort) return -2;
|
if (std::fabs(x - px) <= tol && std::fabs(y - py) <= tol) {
|
||||||
|
which = kPortOut;
|
||||||
|
return i;
|
||||||
|
}
|
||||||
for (int k = 0; k < math_op_arity(n.op); ++k) {
|
for (int k = 0; k < math_op_arity(n.op); ++k) {
|
||||||
math_input_pos(n, k, px, py);
|
math_input_pos(n, k, px, py);
|
||||||
if (std::fabs(x - px) <= kPort && std::fabs(y - py) <= kPort) {
|
if (std::fabs(x - px) <= tol && std::fabs(y - py) <= tol) {
|
||||||
which = k;
|
which = k;
|
||||||
return static_cast<int>(i);
|
return i;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3454,31 +3469,31 @@ void math_canvas_draw(GtkDrawingArea*, cairo_t* cr, int width, int height, gpoin
|
|||||||
|
|
||||||
void math_canvas_pressed(GtkGestureClick*, int, double x, double y, gpointer user) {
|
void math_canvas_pressed(GtkGestureClick*, int, double x, double y, gpointer user) {
|
||||||
MathDialog* d = static_cast<MathDialog*>(user);
|
MathDialog* d = static_cast<MathDialog*>(user);
|
||||||
int which = 0;
|
int which = kPortOut;
|
||||||
const int port = math_port_at(d, x, y, which);
|
const int node = math_port_at(d, x, y, kPort, which);
|
||||||
if (port == -2) {
|
if (node >= 0 && which == kPortOut) {
|
||||||
// An output square: arm it. Clicking it again disarms, so a mis-click needs no escape.
|
// An output square: arm it. Clicking it again disarms, so a mis-click needs no escape.
|
||||||
const int node = math_node_at(d, x, y);
|
d->pending_out = d->pending_out == node ? -1 : node;
|
||||||
d->pending_out = (node >= 0 && d->pending_out == node) ? -1 : node;
|
|
||||||
gtk_widget_queue_draw(d->canvas);
|
gtk_widget_queue_draw(d->canvas);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (port >= 0) {
|
if (node >= 0) {
|
||||||
if (d->pending_out >= 0 && d->pending_out < port) {
|
if (d->pending_out >= 0 && d->pending_out != node) {
|
||||||
MathNode& n = d->nodes[static_cast<std::size_t>(port)];
|
MathNode& n = d->nodes[static_cast<std::size_t>(node)];
|
||||||
(which == 0 ? n.in_a : n.in_b) = d->pending_out;
|
(which == 0 ? n.in_a : n.in_b) = d->pending_out;
|
||||||
d->pending_out = -1;
|
d->pending_out = -1;
|
||||||
d->selected = port;
|
|
||||||
math_dialog_sync(d);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
d->selected = port;
|
|
||||||
math_dialog_sync(d);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const int node = math_node_at(d, x, y);
|
|
||||||
if (node >= 0) {
|
|
||||||
d->selected = node;
|
d->selected = node;
|
||||||
|
math_dialog_sync(d);
|
||||||
|
gtk_widget_queue_draw(d->canvas);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
d->selected = node;
|
||||||
|
math_dialog_sync(d);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const int body = math_node_at(d, x, y);
|
||||||
|
if (body >= 0) {
|
||||||
|
d->selected = body;
|
||||||
d->pending_out = -1;
|
d->pending_out = -1;
|
||||||
math_dialog_sync(d);
|
math_dialog_sync(d);
|
||||||
return;
|
return;
|
||||||
@@ -3959,7 +3974,14 @@ void open_math_dialog(App& app, int edit) {
|
|||||||
|
|
||||||
GtkWidget* actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
GtkWidget* actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6);
|
||||||
GtkWidget* cancel = gtk_button_new_with_label("Cancel");
|
GtkWidget* cancel = gtk_button_new_with_label("Cancel");
|
||||||
g_signal_connect_swapped(cancel, "clicked", G_CALLBACK(gtk_window_destroy), window);
|
// Named so the headless probe can click the *button* rather than call its handler: the defect
|
||||||
|
// this guards was in the connection, which a direct call cannot see.
|
||||||
|
g_object_set_data(G_OBJECT(window), "udpscope-math-cancel", cancel);
|
||||||
|
// `gtk_window_close()`, never `gtk_window_destroy()`: only a close emits `close-request`, which
|
||||||
|
// is what clears `app.math_dialog`. A destroy left that pointer at a dead widget, and every
|
||||||
|
// later open then took the "already open" branch and presented it -- "I cannot edit a math
|
||||||
|
// channel again". The same trap the other three windows in this file document.
|
||||||
|
g_signal_connect_swapped(cancel, "clicked", G_CALLBACK(gtk_window_close), window);
|
||||||
GtkWidget* apply = gtk_button_new_with_label(edit >= 0 ? "Save" : "Add");
|
GtkWidget* apply = gtk_button_new_with_label(edit >= 0 ? "Save" : "Add");
|
||||||
gtk_widget_add_css_class(apply, "suggested-action");
|
gtk_widget_add_css_class(apply, "suggested-action");
|
||||||
g_signal_connect(apply, "clicked", G_CALLBACK(on_math_apply), d);
|
g_signal_connect(apply, "clicked", G_CALLBACK(on_math_apply), d);
|
||||||
@@ -6682,6 +6704,41 @@ gboolean math_probe(gpointer user) {
|
|||||||
} else {
|
} else {
|
||||||
std::fprintf(stderr, "[math-probe] reopened: FAILED (no dialog)\n");
|
std::fprintf(stderr, "[math-probe] reopened: FAILED (no dialog)\n");
|
||||||
}
|
}
|
||||||
|
if (a->math_dialog != nullptr) {
|
||||||
|
auto* h = static_cast<MathDialog*>(
|
||||||
|
g_object_get_data(G_OBJECT(a->math_dialog), "udpscope-math-dialog"));
|
||||||
|
if (h != nullptr && !h->nodes.empty()) {
|
||||||
|
// The output square straddles the box's right edge: it is drawn centred on
|
||||||
|
// `n.x + kNodeW` with a 5 px half-width and hit with a 10 px radius. A hit test that
|
||||||
|
// recovers the node by a box test therefore misses every pixel past the edge, which
|
||||||
|
// is a *silent disarm* of the first click of a wiring gesture.
|
||||||
|
double ox = 0.0;
|
||||||
|
double oy = 0.0;
|
||||||
|
math_output_pos(h->nodes[0], ox, oy);
|
||||||
|
int w_in = 0;
|
||||||
|
const int n_in = math_port_at(h, ox - 2.0, oy, kPort, w_in);
|
||||||
|
int w_out = 0;
|
||||||
|
const int n_out = math_port_at(h, ox + 4.0, oy, kPort, w_out);
|
||||||
|
std::fprintf(stderr,
|
||||||
|
"[math-probe] hit: inside node=%d which=%d, past-edge node=%d which=%d "
|
||||||
|
"(want 0 %d 0 %d)\n",
|
||||||
|
n_in, w_in, n_out, w_out, kPortOut, kPortOut);
|
||||||
|
}
|
||||||
|
// Cancel must clear the pointer exactly as the close above does: it is the *button* that is
|
||||||
|
// clicked here, not the window, because the defect was in what the button was connected to.
|
||||||
|
if (a->math_dialog != nullptr) {
|
||||||
|
GtkWidget* cancel_button = GTK_WIDGET(
|
||||||
|
g_object_get_data(G_OBJECT(a->math_dialog), "udpscope-math-cancel"));
|
||||||
|
if (cancel_button != nullptr) {
|
||||||
|
g_signal_emit_by_name(cancel_button, "clicked");
|
||||||
|
std::fprintf(stderr, "[math-probe] cancel: dialog=%s (want cleared)\n",
|
||||||
|
a->math_dialog == nullptr ? "cleared" : "STILL SET");
|
||||||
|
open_math_dialog(*a, 0);
|
||||||
|
} else {
|
||||||
|
std::fprintf(stderr, "[math-probe] cancel: NO BUTTON (the data key is not set)\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// The editor's two reported failures, driven directly. A drag reports the *total* offset from
|
// The editor's two reported failures, driven directly. A drag reports the *total* offset from
|
||||||
// where the gesture began, so replaying a real sequence of them must land the box at start+last,
|
// where the gesture began, so replaying a real sequence of them must land the box at start+last,
|
||||||
// not at the sum of the deltas; and a numeric field must hold a fraction.
|
// not at the sum of the deltas; and a numeric field must hold a fraction.
|
||||||
|
|||||||
Reference in new issue
Block a user