diff --git a/src/ui.cpp b/src/ui.cpp index 104f11a..94eb173 100644 --- a/src/ui.cpp +++ b/src/ui.cpp @@ -3356,21 +3356,36 @@ int math_node_at(const MathDialog* d, double x, double y) { return -1; } -/// The port under the pointer: `-1` none, `-2` the selected node's output, otherwise the index of -/// the node whose *input* `which` was hit. -int math_port_at(const MathDialog* d, double x, double y, int& which) { - which = 0; - for (std::size_t i = 0; i < d->nodes.size(); ++i) { - const MathNode& n = d->nodes[i]; +/// A port's identity: `kPortOut` for a node's output, 0 or 1 for one of its inputs. +constexpr int kPortOut = -1; + +/// The port under a canvas position: the **node index** it belongs to, with `which` set to +/// `kPortOut` or the input number; `-1` when there is no port there. `tol` is the hit radius in +/// 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(d->nodes.size()) - 1; i >= 0; --i) { + const MathNode& n = d->nodes[static_cast(i)]; double px = 0.0; double py = 0.0; 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) { 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; - return static_cast(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) { MathDialog* d = static_cast(user); - int which = 0; - const int port = math_port_at(d, x, y, which); - if (port == -2) { + int which = kPortOut; + const int node = math_port_at(d, x, y, kPort, which); + if (node >= 0 && which == kPortOut) { // 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 = (node >= 0 && d->pending_out == node) ? -1 : node; + d->pending_out = d->pending_out == node ? -1 : node; gtk_widget_queue_draw(d->canvas); return; } - if (port >= 0) { - if (d->pending_out >= 0 && d->pending_out < port) { - MathNode& n = d->nodes[static_cast(port)]; + if (node >= 0) { + if (d->pending_out >= 0 && d->pending_out != node) { + MathNode& n = d->nodes[static_cast(node)]; (which == 0 ? n.in_a : n.in_b) = d->pending_out; d->pending_out = -1; - d->selected = port; + d->selected = node; math_dialog_sync(d); + gtk_widget_queue_draw(d->canvas); return; } - d->selected = port; + d->selected = node; math_dialog_sync(d); return; } - const int node = math_node_at(d, x, y); - if (node >= 0) { - d->selected = node; + const int body = math_node_at(d, x, y); + if (body >= 0) { + d->selected = body; d->pending_out = -1; math_dialog_sync(d); return; @@ -3959,7 +3974,14 @@ void open_math_dialog(App& app, int edit) { GtkWidget* actions = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 6); 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"); gtk_widget_add_css_class(apply, "suggested-action"); g_signal_connect(apply, "clicked", G_CALLBACK(on_math_apply), d); @@ -6682,6 +6704,41 @@ gboolean math_probe(gpointer user) { } else { std::fprintf(stderr, "[math-probe] reopened: FAILED (no dialog)\n"); } + if (a->math_dialog != nullptr) { + auto* h = static_cast( + 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 // 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.