Using GtkTreeModelFilter properly

I am trying to implement a filter for a GtkTreeModel using a GtkTreeModelFilter, however, it doesn’t seem to be working as I would expect.

In the example program below, setting any filter will present an empty the GtkTreeView if the filter applies to nested elements. What I mean by that is if the filter matches on filenames, those rows are correctly returned as visible. However, the GtkTreeModelFilter does not automatically mark all parents as visible. This results in an empty TreeView.

On the other hand, if the filter “.*etc”, then it will display some matches because the top level row is marked as visible.

If I am reading the documentation for GtkTreeModelFilter correct, the toggling of parent row visibility is supposed to happen automatically:

Determining the visibility state of a given node based on the state of its child nodes is a frequently occurring use case. Therefore, GtkTreeModelFilter explicitly supports this. For example, when a node does not have any children, you might not want the node to be visible. As soon as the first row is added to the node’s child level (or the last row removed), the node’s visibility should be updated.

Am I doing something wrong in my example or am I misreading the documentation?

Thank you.

#!/usr/bin/python3
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
import os
import re


class MainWindow(Gtk.Window):
    def __init__(self):
        self.regexp = None

        Gtk.Window.__init__(self, title="TreeView Filter Test")
        self.grid = Gtk.Grid()
        self.add(self.grid)

        self.tree_model = Gtk.TreeStore(str)

        self.populate_tree("/etc")

        self.filter_model = self.tree_model.filter_new()
        self.filter_model.set_visible_func(self.apply_filter)

        self.tree_view = Gtk.TreeView.new_with_model(self.filter_model)
        renderer = Gtk.CellRendererText()
        column = Gtk.TreeViewColumn("Filename", renderer, text=0)
        self.tree_view.append_column(column)
        self.tree_view.expand_all()

        self.scrolled_window = Gtk.ScrolledWindow()
        self.scrolled_window.set_vexpand(True)
        self.scrolled_window.set_hexpand(True)
        self.scrolled_window.add(self.tree_view)

        self.grid.attach(self.scrolled_window, 0, 0, 2, 1)

        self.filter_entry = Gtk.Entry()
        self.filter_entry.set_hexpand(True)
        self.grid.attach(self.filter_entry, 0, 1, 1, 1)

        self.filter_button = Gtk.Button("Filter")
        self.filter_button.connect("clicked", self.set_filter)
        self.grid.attach(self.filter_button, 1, 1, 1, 1)

        self.show_all()

    def populate_tree(self, pathname, parent=None):
        tree_iter = self.tree_model.append(parent, [os.path.basename(pathname)])
        try: entries = os.listdir(pathname)
        except OSError: return
        for entry in entries:
            if os.path.isdir(os.path.join(pathname, entry)):
                self.populate_tree(os.path.join(pathname, entry), tree_iter)
            elif os.path.isfile(os.path.join(pathname, entry)):
                self.tree_model.append(tree_iter, [entry])

    def set_filter(self, button):
        self.filter = self.filter_entry.get_text()
        if not self.filter:
            self.regexp = None
        else:
            self.regexp = re.compile(self.filter)
        self.filter_model.refilter()

    def apply_filter(self, model, iter, data):
        visible = True
        if self.regexp:
            if not self.regexp.search(model[iter][0]):
                visible = False
        print("%s -> %u" % (model[iter][0], visible))
        return visible

win = MainWindow()
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()

The problem is in your regexp, which I don’t understand. For example this works:

def apply_filter(self, model, iter, data):
    visible = True
    if self.filter not in model[iter][0]:
         visible = False
    print("%s -> %u" % (model[iter][0], visible))
    return visible

A more powerful search, think ‘Google’, could be implemented like this:

def set_filter(self, button):
    self.filter = self.filter_entry.get_text()
    self.advanced_filter = self.filter.split(" ")
    self.filter_model.refilter()

def apply_filter(self, model, iter, data):
    for word in self.advanced_filter:
        if word not in model[iter][0]:
            return False
    return True

This splits the string into words and searches for each word separately.

Thank you for taking the time to reply.

Firstly, your change does not actually work. On my system, I have the following files:

/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-12-i386
/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-21-s390x
/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-15-ppc
/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-12-ppc
/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-8-ppc64
/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-21-ppc64le
/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-27-ppc64le
/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-iot-ppc64le
/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-29-armhfp
/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-23-s390x
/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-17-i386
/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-13-mips
/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-19-secondary
/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-22-aarch64
/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-25-aarch64
/etc/pki/rpm-gpg/RPM-GPG-KEY-rpmfusion-nonfree-fedora-31
/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-18-ppc
/etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-10-i386
...

When the the model is initially filled in, I see the following output from the filter:

etc -> 1
pki -> 1
rpm-gpg -> 1
etc -> 1
pki -> 1
RPM-GPG-KEY-fedora-12-i386 -> 1
etc -> 1
pki -> 1
RPM-GPG-KEY-fedora-21-s390x -> 1
etc -> 1
pki -> 1
RPM-GPG-KEY-fedora-15-ppc -> 1
etc -> 1
pki -> 1
RPM-GPG-KEY-fedora-12-ppc -> 1
etc -> 1
pki -> 1
RPM-GPG-KEY-fedora-8-ppc64 -> 1
etc -> 1
pki -> 1
RPM-GPG-KEY-fedora-21-ppc64le -> 1
etc -> 1
pki -> 1
RPM-GPG-KEY-fedora-27-ppc64le -> 1
etc -> 1
pki -> 1

However, when I set a filter of “RPM-GPG” (this is with your change, by the way), the output is this:

rpm-gpg -> 0
RPM-GPG-KEY-fedora-12-i386 -> 1
etc -> 0
pki -> 0
RPM-GPG-KEY-fedora-21-s390x -> 1
etc -> 0
pki -> 0
RPM-GPG-KEY-fedora-15-ppc -> 1
etc -> 0
pki -> 0
RPM-GPG-KEY-fedora-12-ppc -> 1
etc -> 0
pki -> 0
RPM-GPG-KEY-fedora-8-ppc64 -> 1
etc -> 0
pki -> 0
RPM-GPG-KEY-fedora-21-ppc64le -> 1
etc -> 0
pki -> 0
RPM-GPG-KEY-fedora-27-ppc64le -> 1
etc -> 0
pki -> 0

and the TreeView is empty.

To me, it seems clear that the parent nodes are not changed from non-visible to visible but I am not sure why. It seems that the immediate parent to any RPM-GPG child (rpm-gpg) is not changed to visible, which ends up leaving its parents invisible, as well.

Secondly, it doesn’t make sense why your change would work. The method by which visible is set should be irrelevant. All that matters is the return value of the function.

Thank you.

My bad, my mind must have been somewhere else. Your regexp does work.

You should read https://stackoverflow.com/questions/3564969/how-to-setup-a-gtk-treemodelfilter-that-filters-an-underlying-treestore.
Also read https://stackoverflow.com/questions/56029759/how-to-filter-a-gtk-tree-view-that-uses-a-treestore-and-not-a-liststore.

The second link contains an example that basically goes through the TreeStore backwards for every visible row and makes all of the parents visisble as well.

I quickly arrived at the same solution but that seems like important functionality that is missing from the stock GtkTreeModelFilter. Especially, considering the text that I quoted in my initial post. That text does seem to indicate that the base implementation does take parent visibility into consideration.

Let me put it in different words, “parent visibility” does not apply to a GtkListStore, so it makes not sense when using a GtkListStore. So, it has to be with respect to a GtkTreeStore.

Thank you.

You may also want to read https://gitlab.gnome.org/GNOME/gtk/issues/1595 where this feature/unfeature is discussed.

Thank you for digging this thread out. However, it’s does not discuss the issue that I am talking about. It does not mention parent visibility settings at all.

Even if the intent is for users of GtkTreeModelFilter to manage parent node visibility, it’s annoying because it means that the GtkTreeModelFilter would have to use a visibility column regardless of how the actual filtering is done. In other words, it prevents users of GtkTreeModelFilter to use only a visibility function.

I think I’ll file a bug and see how that goes.

That’s the problem. This question has come up many times, with different opinions and suggestions thrown about, usually ending with bad feelings all around. There are some more links and documentation that you should read before filing a bug report, because otherwise your bug report will be ignored/deleted. I would have to dig the various discussions up.

For instance, this same GtkTreeModelFilter is used on a GtkEntryCompletion and you cannot display a popup treestore in a tree fashion without loosing focus of the entry, defeating the purpose of the EntryCompletion.

So the Gtk devs would have to agree on a pattern that is only going to please a certain amount of people, and being this would ‘break’ Gtk3, it is not going to happen, as Gtk3 is now API/ABI stable. The best solution would be to look at implementing this in Gtk4, and for that to happen, there needs to be a concensus on how this is going to be implemented.

I can assure you for every proposal you come up with, you are going to break someone else’s code. I am not trying to be snarky here, I am just trying to warn you that what appears simple, isn’t. I found that out myself, after I dug into the code to make this work (after posting a question similar to yours).

Thus, most people are creating a workaround (like in the links I provided) to create their own version of the filter, that does specifically what they need.

Than, at the very least, the documentation has to be updated. Currently, it implies that it should work the way I am think it should work - the GtkTreeModelFilter should automatically handle parent node visibility. However, that is not the case.

So, it either a bug or a design. Either way something has to change. If the former, the code has to change. If the latter, the documentation. At present, it’s confusing at best.

Sigh… Now maybe if I could digest the documentation…

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