(Bug?) In GTK4, widgets inside GtkExpanders do not receive destroy signals

TLDR: widgets in expanders (GtkExpanders) don’t receive destroy signals. Is this a bug or intended behaviour, and if it is the latter, why/what are the exact rules for when the destroy signal is not emitted?

As I understand it, when their parent is destroyed (for example a window being closed) all children should receive the destroy signal as they are freed. This can be useful for freeing data associated with the widget, and is behaviour depended upon by g_signal_connect_data (the callback for freeing the data is never emitted when the instance this function is called on is inside a GtkExpander)

Yet it seems that in GTK4, this does not function correctly when the child is inside an expander. Even when the parent of the expander is destroyed, the child of the expander does not ever see a destroy signal.

I have tried to create a minimal example to demonstrate this: it compiles with both GTK3 and GTK4
(cc -o main main.c $(pkg-config --cflags --libs gtk4), or swap out gtk4 for gtk+-3.0 to use GTK3) and uses preprocessing bits to make the very minor tweaks needed to make it compatible with both APIs.
What’s important, though, is that upon running and closing the window, with GTK3 it prints messages for both buttons being destroyed, yet with GTK4 only the second button which is not in an expander is caused to print a message by receiving the destroy signal.

#include <gtk/gtk.h>
#include <stdio.h>

#if GTK_MAJOR_VERSION == 3 // the following are just aliases, and resulting behaviour should be the same regardless of version of gtk
void gtk_expander_set_child (GtkExpander *parent, GtkWidget *child) { gtk_container_add(GTK_CONTAINER(parent), child); }
void gtk_window_set_child   (GtkWindow   *parent, GtkWidget *child) { gtk_container_add(GTK_CONTAINER(parent), child); }
void gtk_box_append         (GtkBox      *parent, GtkWidget *child) { gtk_container_add(GTK_CONTAINER(parent), child); }
#endif

static void btn1_destroyed(GtkButton *btn, gpointer user_data) { printf("Button 1 destroyed!\n"); }
static void btn2_destroyed(GtkButton *btn, gpointer user_data) { printf("Button 2 destroyed!\n"); }

static void activate(GApplication *app, gpointer user_data)
{
        GtkWidget *win = gtk_application_window_new(GTK_APPLICATION(app)); // create window
        gtk_window_set_default_size(GTK_WINDOW(win), 200, 200); // set default size

        GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10); // 10px padding
        gtk_window_set_child(GTK_WINDOW(win), box); // add box to window

        GtkWidget *btn1 = gtk_button_new_with_label("Button 1"); // create first button
        GtkWidget *expander = gtk_expander_new("Expander"); // create expander
        gtk_expander_set_child(GTK_EXPANDER(expander), btn1); // add first button to expander

        GtkWidget *btn2 = gtk_button_new_with_label("Button 2"); // create second button

        gtk_box_append(GTK_BOX(box), expander); // add expander to box
        gtk_box_append(GTK_BOX(box), btn2); // add second button to box

        g_signal_connect(btn1, "destroy", G_CALLBACK(btn1_destroyed), NULL); // for each button, add a callback to print a message when the destroy signal is received
        g_signal_connect(btn2, "destroy", G_CALLBACK(btn2_destroyed), NULL);

#if GTK_MAJOR_VERSION == 3 // whichever way of showing the window is used in this version of gtk
        gtk_widget_show_all(win);
#else
        gtk_widget_show(win);
#endif
}

int main(int argc, char *argv[]) // basic main function to set up application and activate it
{
        GtkApplication *app = gtk_application_new("com.test", G_APPLICATION_FLAGS_NONE);
        g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
        int status = g_application_run(G_APPLICATION(app), argc, argv);
        g_object_unref(app);
        return status;
}

This looks like a reference leak inside GtkExpander: we acquire a reference to the child if the expander is not expanded, and release it on expansion; but if we dispose the expander in its unexpanded state, we never remove the reference.

Looking at the history of GtkExpander, it’s always been there—likely just masked by the explicit gtk_widget_destroy().

Thanks for your rapid reply!

Does this mean it should be fixed in some future release of GTK4?

In the meantime I can find a workaround but it’d be great to know if I should expect this to work as I thought it would at some point in the future.

Yes, it will be fixed in the next release, as soon as I look into fixing it.

Edit: opened a merge request; testing is appreciated.

2 Likes

Firstly, thanks for looking into this, and so promptly.

Secondly, having just built and tested the latest build with the changes from your merge request, I’m sorry to say it doesn’t appear to have changed anything. I’m still getting the exact behaviour.

Also, I might just mention that your addition looks to only have an effect if the expander is not expanded - but I am seeing this behaviour of no destroy signal regardless of whether the expander is or is not expanded. (If I am misunderstanding your code, I do apologise.)

Indeed, there was an additional reference being held. The code leaked back in GTK3 as well, but we never noticed because of the implicit recursive destroy that GtkContainer performed when getting disposed. I’ve updated the merge request, and tested with your example: now both “destroy” handlers are called.

1 Like

That does seem to have fixed it. Many thanks!

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.