Recursive loop of callbacks in signal connect method of Gtk4 widgets

I am trying to do bi-directional binding of two textboxes which are GtkEntry widgets from Gtk4.jl. The two textboxes should have the same text. I am using signal connect handlers on change events to achieve that. But it crashes the Julia REPL.
Following is my code:

using Gtk4

win = GtkWindow("mirrored text boxes")
hbox = GtkBox(:h)
push!(win, hbox)

textbox1 = GtkEntry()
textbox2 = GtkEntry()
push!(hbox, textbox1)
push!(hbox, textbox2)

## try_1
id1 = signal_connect(textbox1, :changed) do widget
    set_gtk_property!(textbox2, :text, get_gtk_property(textbox1, :text))
end
id2 = signal_connect(textbox2, :changed) do widget
    #set_gtk_property!(textbox1, :text, get_gtk_property(textbox2, :text))
end

If I uncomment the middle line in id2 signal, and run the code, it crashes the Julia terminal.
I guess that it starts a recursive loop of callbacks. But even if I keep that line commented in id2, it still crashes the REPL. I do not understand WHY. I was able to capture the screenshot just before the crash, it reads:

GLib-GObject:ERROR:./gobject/gsignal.c:884;emission_pop: code should not be reached
Bail out!
GLib-GObject:ERROR:/gobject/gsignal.c:884:emission_pop: code should not be reached

[392321 signal (6): Abort trap: 6 in expression starting at none:0

I have also tried the following to stop the recursive loop of the callbacks. But it does not work and crashes the REPL.

## try_2
signal_connect(textbox1, :changed) do widget
    if get_gtk_property(textbox1, :text) != get_gtk_property(textbox2, :text)
        set_gtk_property!(textbox2, :text, get_gtk_property(textbox1, :text))
    end
end
signal_connect(textbox2, :changed) do widget
    if get_gtk_property(textbox2, :text) != get_gtk_property(textbox1, :text)
        set_gtk_property!(textbox1, :text, get_gtk_property(textbox2, :text))
    end
end

Finally, I was able to avoid the callbacks loop by using signal_handler_block and signal_handler_unblock.

## try_3
id1 = signal_connect(textbox1, :changed) do widget
        signal_handler_block(textbox2, id2)
        set_gtk_property!(textbox2, :text, get_gtk_property(textbox1, :text))
        signal_handler_unblock(textbox2, id2)
end

id2 = signal_connect(textbox2, :changed) do widget
        signal_handler_block(textbox1, id1)
        set_gtk_property!(textbox1, :text, get_gtk_property(textbox2, :text))
        signal_handler_unblock(textbox1, id1)
end

But I am still wondering why the firing of the id2 signal without any action in ## try_1 block crashed the REPL.

Please let me know if something is not clear and you need more information. Thank you in advance! I am using Julia v"1.9.3" in VS Code on a Mac device.

With a callback one cannot change the UI. Thus one needs to run the code at a later stage, which can be done by the @idle_add macro. So change to:

id1 = signal_connect(textbox1, :changed) do widget
    @idle_add set_gtk_property!(textbox2, :text, get_gtk_property(textbox1, :text))
end
id2 = signal_connect(textbox2, :changed) do widget
    @idle_add set_gtk_property!(textbox1, :text, get_gtk_property(textbox2, :text))
end

But this will not fix the recursion issue. What I usually do in that situation is to define some global

const changingUIParameter = Ref(false)

which I then check like this:

id1 = signal_connect(textbox1, :changed) do widget
    if !changingUIParameter[]
      changingUIParameter[] = true
      @idle_add begin
          set_gtk_property!(textbox2, :text, get_gtk_property(textbox1, :text))
          changingUIParameter[] = false
      end
    end
end
id2 = signal_connect(textbox2, :changed) do widget
    if !changingUIParameter[]
      changingUIParameter[] = true
      @idle_add begin
          set_gtk_property!(textbox1, :text, get_gtk_property(textbox2, :text))
          changingUIParameter[] = false
      end
    end
end
1 Like