Search code examples
listviewrustgtkgtk4

gtk4/rust failed assertion when removing EditableLabel from ListView


In the following gtk4 code, I'm trying to remove an EditableLabel from display by deleting the cooresponding StringObject . This is supposed to happen after editing is complete as specified by the connect_editing_notify callback.

If I run the program and exit editing mode using the return key then it works as expected: the label disappears. However, if I exit editing mode by selecting a different label then I get a repeating console print saying that an assertion has failed:

(viewdb:13773): Gtk-CRITICAL **: 17:35:26.754: gtk_widget_get_parent: assertion 'GTK_IS_WIDGET (widget)' failed

(viewdb:13773): Gtk-CRITICAL **: 17:35:26.754: gtk_widget_get_parent: assertion 'GTK_IS_WIDGET (widget)' failed

(viewdb:13773): Gtk-CRITICAL **: 17:35:26.754: gtk_widget_get_parent: assertion 'GTK_IS_WIDGET (widget)' failed

How can I cleanly delete the EditableLabel after editing ?

use gtk::glib::clone;
use gtk::prelude::*;

use gtk::{
    Application, ApplicationWindow, EditableLabel, ListView, SignalListItemFactory,
    SingleSelection, StringList, StringObject, Widget,
};

fn build_ui(app: &Application) {
    let model: StringList = vec!["One", "Two", "Three", "Four"].into_iter().collect();

    let selection_model = SingleSelection::new(Some(&model));

    let factory = SignalListItemFactory::new();
    factory.connect_setup(move |_, listitem| {
        let label = EditableLabel::builder().build();

        listitem.set_child(Some(&label));

        listitem
            .property_expression("item")
            .chain_property::<StringObject>("string")
            .bind(&label, "text", Widget::NONE);

        label.connect_editing_notify(
            clone!( @strong model, @strong listitem => move |lbl: &EditableLabel| {
                if !lbl.is_editing() {
                    let position = listitem.position();
                    model.remove(position); // remove model entry cooresponding to edit.
                }
            }),
        );
    });

    let view = ListView::new(Some(&selection_model), Some(&factory));

    let window = ApplicationWindow::builder()
        .application(app)
        .child(&view)
        .build();

    window.present();
}

fn main() {
    let app = Application::builder().build();
    app.connect_activate(build_ui);
    app.run();
}

Solution

  • Removing the item from its parent when it is still processing some events sometimes causes issues.

    The easy fix is to do the removing in a deferred function. In C that would be done with g_idle_add() that is a hassle to use correctly. But in Rust it is super-easy:

    gtk::glib::source::idle_add_local_once(
        clone!(@strong model => 
            move || model.remove(position)
        )
    );
    

    But note that since idle functions are run some unspecified time in the future, it is possible, although very unlikely, that the value of position (a plain index in the list) when the callback is run will refer to a different item.

    I didn't test it but I think it is wiser to keep the listitem itself into the callback:

    gtk::glib::source::idle_add_local_once(
        clone!(@strong model, @weak listitem => 
            move || {
                let position = listitem.position();
                if position != INVALID_LIST_POSITION {
                    model.remove(position)
                }
            }
        )
    );
    

    A weak clone will ensure that it will only be called if the item still exists.