Creating a File Tree With DirectoryList, TreeListModel and TreeExpanders

I’ve been trying to create a file tree for quite a while now, and while I’ve gotten close I just can’t seem to fully get it. Here’s my code currently:

pub fn load_folder_view(state: &mut State) {
    let path = state.current_folder_path.clone();
    let file = File::for_path(&path);
    let dir_list = DirectoryList::new(Some("standard::name"), Some(&file));
    let model = TreeListModel::new(dir_list, false, false, move |o| {
        let dir_str = o.downcast_ref::<FileInfo>().unwrap().name();
        let mut dir_path = PathBuf::new();
        dir_path.push(&path);
        dir_path.push(&dir_str);
        if dir_path.is_dir() {
            let dir_list_local = DirectoryList::new(None, Some(&File::for_path(dir_path)));
            Some(dir_list_local.into())
        } else {
            None
        }
    });
    let selection = SingleSelection::new(Some(model.model()));
    let factory = SignalListItemFactory::new();
    factory.connect_setup(move |_, list_item| {
        list_item.set_child(Some(
            &TreeExpander::builder().child(&Label::new(None)).build(),
        ));
    });
    factory.connect_bind(move |_, list_item| {
        let item = list_item.item().unwrap();
        let file_info = item.downcast_ref::<FileInfo>().unwrap();
        let tree = list_item.child().and_downcast::<TreeExpander>().unwrap();
        tree.set_list_row(model.row(list_item.position()).as_ref());
        tree.set_child(Some(&Label::new(Some(file_info.name().to_str().unwrap()))));
    });
    state.file_view.set_model(Some(&selection));
    state.file_view.set_factory(Some(&factory));
}

It almost works, but not quite- it can load the top-most folder, and the TreeExpander that represent a folder have are expandable, but they have no items within them , even when they should. Below is a example of this:

Does anyone know what I’m doing wrong?

Hi,

I think you don’t need the intermediate TreeListModel.
Directly pass dir_list to SingleSelection::new() instead.

Hi! That could work, however I need to set the list row of tree in factory.connect_bind, do you know what I should set it to?

Ah, no, wait, my bad, I looked at a wrong example…
Using TreeListModel is correct if you want an expandable tree.

It’s probably something wrong with set_list_row.
Can you try passing list_item.item() as parameter for tree.set_list_row()?
I can’t test right now, but can have a look later today.

No worries :]

Though, unfortunately that doesn’t work- as set_list_row needs a TreeListRow, and list_item.item() is a glib::Object.

TreeListRow is a subclass of glib::object, so it’s OK, you may just need some Rust equivalent of dynamic_cast<> in C++.

The bind signal uses glib::object as generic base type for its signature, but the actual type actually depends on the model:
When the model is a TreeListModel, the item will be a TreeListRow.
Other model types may yield other item types.

I’ve tried a few variations of that- and while it seems Rust (or at least GTK-RS) has dynamic_cast<>, it doesn’t seem to work here. I also tried downcasting and upcasting. Could it be an issue with the model?

I found the time to experiment a bit, here is what I do in Python:

import sys
import gi
gi.require_version('Gtk', '4.0')
from gi.repository import GLib, Gio, Gtk

FILE_ATTRS = f'{Gio.FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME},{Gio.FILE_ATTRIBUTE_STANDARD_TYPE}'


class MyApp(Gtk.Application):
    __gtype_name__ = __qualname__

    def __init__(self):
        super().__init__()
        self.connect('activate', self.on_activate)

    def on_activate(self, app):
        self.add_window(MyAppWindow())


class MyAppWindow(Gtk.ApplicationWindow):
    __gtype_name__ = __qualname__

    def __init__(self):
        super().__init__(default_width=400, default_height=600)
        root = Gtk.DirectoryList.new(FILE_ATTRS, Gio.File.new_for_path("/etc"))
        model = Gtk.TreeListModel.new(root, False, True, self.create_treemodel)
        selection = Gtk.SingleSelection(model=model, autoselect=False, can_unselect=True)
        factory = MyListItemFactory()
        listview = Gtk.ListView.new(selection, factory)
        scrollwin = Gtk.ScrolledWindow(has_frame=False, child=listview)
        self.set_child(scrollwin)
        self.present()

    def create_treemodel(self, item):
        fileinfo = item
        assert isinstance(fileinfo, Gio.FileInfo)
        if fileinfo.get_file_type() == Gio.FileType.DIRECTORY:
            return Gtk.DirectoryList.new(FILE_ATTRS, fileinfo.get_attribute_object('standard::file'))
        return None


class MyListItemFactory(Gtk.SignalListItemFactory):
    __gtype_name__ = __qualname__

    def __init__(self):
        super().__init__()
        self.connect('setup', self.on_listitem_setup)
        self.connect('bind', self.on_listitem_bind)

    def on_listitem_setup(self, factory, listitem):
        label = Gtk.Inscription(hexpand=True)
        expander = Gtk.TreeExpander(child=label)
        listitem.set_child(expander)

    def on_listitem_bind(self, factory, listitem):
        expander = listitem.get_child()
        treelistrow = listitem.get_item()
        assert isinstance(expander, Gtk.TreeExpander) and isinstance(treelistrow, Gtk.TreeListRow)
        treelistrow.set_expanded(False)
        # Bind GtkTreeExpander to GtkTreeListRow
        expander.set_list_row(treelistrow)
        label = expander.get_child()
        fileinfo = treelistrow.get_item()
        # Bind row contents
        name = GLib.path_get_basename(fileinfo.get_display_name())
        label.set_text(name)


if __name__ == '__main__':
    sys.exit(MyApp().run(sys.argv))

I hope it helps…

Thank you for taking the time to make this, I’ll see if I can translate what I need over to Rust :]

I have another example of a very similar thing here, in C++. It was supposed to primarily demonstrate peel rather than explain Gtk::TreeListModel etc, but I hope it can still be useful as a reference for you.

This in particular looks wrong. You’re extracting the basename from the FileInfo, then concatenating this to the base path using std-rs APIs (as opposed to GLib APIs), then querying whether or not there is a directory at this path, then reconstructing a new gio::File for that path.

For one thing, you’re checking for directory synchronously, on the main thread. This will cause lags and hangs if the file is not-super-instant to access. Also this is completely unnecessary work, since readdir() on Unix already returns, in d_type, what type of entry (whether a directory or not) this is, and gio::FileInfo already exposes this as a standard::type attribute (aka FileInfo::get_type()).

But also, this always uses the root directory’s path, doesn’t it? So as soon as you descend deeper into a nested directory, this will start constructing non-existent paths. This explains the effect you’re seeing:

it can load the top-most folder, and the TreeExpander that represent a folder have are expandable, but they have no items within them , even when they should.

Instead of doing this all:

  • Check .file_type() to determine whether it’s a directory or not;
  • Use the standard::file attribute to extract the gio::File object for this file, instead of extracting the path, concatenating things yourself (wrongly) and constructing your own gio::File.

Hope this makes sense :smiley: Please see my or @gwillems’s samples on how this can be done code-wise.

1 Like

I’ve updated the code with your suggestions, both examples were very helpful- tbh I can’t believe I didn’t think about creating a GFile from the GFileInfo- however unfortunately I’m still having the same issue, I might be missing something. Here is the updated code:

let dir_list = DirectoryList::new(
    Some("standard::*"),
    Some(&File::for_path(&state.current_folder_path)),
);
let model = TreeListModel::new(dir_list, false, false, move |o| {
    let file_info = o.downcast_ref::<FileInfo>().unwrap();
    if file_info.file_type() == FileType::Directory {
        let dir_list_local = DirectoryList::new(
            Some("standard::*"),
            Some(
                file_info
                    .attribute_object("standard::file")
                    .unwrap()
                    .dynamic_cast_ref::<File>()
                    .unwrap(),
            ),
        );
        Some(dir_list_local.into())
    } else {
        None
    }
});
let selection = SingleSelection::new(Some(model.model()));
let factory = SignalListItemFactory::new();
factory.connect_setup(move |_, list_item| {
    list_item.set_child(Some(
        &TreeExpander::builder().child(&Label::new(None)).build(),
    ));
});
factory.connect_bind(move |_, list_item| {
    let item = list_item.item().unwrap();
    let file_info = item.downcast_ref::<FileInfo>().unwrap();
    let tree = list_item.child().and_downcast::<TreeExpander>().unwrap();
    tree.set_list_row(model.row(list_item.position()).as_ref());
    tree.set_child(Some(&Label::new(Some(
        file_info.name().display().to_string().as_str(),
    ))));
});
state.file_view.set_model(Some(&selection));
state.file_view.set_factory(Some(&factory));

I got your example working, after some wrestling with gtk-rs :sweat_smile:

This here is the first issue: the call model.model() returns the root model that the TreeListModel was created with (i.e. dir_list for state.current_folder_path). You’re basically throwing away the rest of the TreeListModel. It’s no wonder that expanding doesn’t work :slightly_smiling_face:

You’re trying to compensate for it here by indexing into the original model…

…but this of course makes no sense, since the index is for the model you pass to the fiew_view, i.e. selection, which wraps model.model() in your version.

Then:

this is a (long-winded) way to call .name(); but if you read the note on FILE_ATTRIBUTE_STANDARD_NAME, you’ll see that you should be using .display_name() for the purposes of displaying the name an a UI.

This means you’re creating a new gtk::Label on each bind; rather than updating the existing one. The whole point of recycling is that you don’t do that, you simply update the text in the existing label. This is supposed to be much faster, especially if you switch from labels to inscriptions.

So I’ve done these changes:

// Create the SingleSelection wrapping TreeListModel
let selection = SingleSelection::new(Some(model));

and rewrote bind like so:

let tree_list_row = list_item.item().and_downcast::<TreeListRow>().unwrap();
let file_info = tree_list_row.item().and_downcast::<FileInfo>().unwrap();
let tree_expander = list_item.child().and_downcast::<TreeExpander>().unwrap();
tree_expander.set_list_row(Some(&tree_list_row));
let label = tree_expander.child().and_downcast::<Label>().unwrap();
label.set_text(&file_info.display_name());

as you can see, list_item.item() is a TreeListRow (in a TreeListModel, as opposed to the root model, where an item is a FileInfo), and to extract the FileInfo, we call .item() on that. We pass the row directly to the TreeExpander, so it can control (expand and unexpand) the correct row; and we extract the previously created (in setup) label, settings its text to the display name of the file.

Hope that helps :smiley:

HOLY SHIT IT WORKS!!! I cannot thank you enough- I have been trying to get this for weeks, it’s put the entire project on hold, but now I can finally move on. Thank you so much :] /gen

1 Like