Search code examples
pythongtkgtk3pygobject

Pygobjects RenderToggle in filtered model selects wrong column


I'm trying to add a toggle field in an filtered model for pygobjects. I used the code from the tutorial and enhanced to add toggle renderer, but as soon as I click in a filtered list the toggles are rendered wrong.

Assume you have a unfiltered list:

###########################
# 0|test|a|toggle=False
# 1|test|b|toggle=False
# 2|test|a|toggle=False
# 3|test|a|toggle=False
##########################

When you now apply a filter, saying you only want to see items of b you get:

# 1|test|b|toggle=False

If you now click on that column, the column from the unfiltered list is "toggled", as the path is 0, but should be 1:

# 0|test|a|toggle=True

But what I want is, of course:

# 1|test|b|toggle=True

So how do I do that?

Here is my code, mostly copied from the Tutorial at http://python-gtk-3-tutorial.readthedocs.org/en/latest/treeview.html#filtering :

from gi.repository import Gtk

#list of tuples for each software, containing the software name, initial release, and main programming languages used
software_list = [("Firefox", 2002,  "C++", False),
                 ("Eclipse", 2004, "Java", False),
                 ("Pitivi", 2004, "Python", False),
                 ("Netbeans", 1996, "Java", False),
                 ("Chrome", 2008, "C++", False),
                 ("Filezilla", 2001, "C++", False),
                 ("Bazaar", 2005, "Python", False),
                 ("Git", 2005, "C", False),
                 ("Linux Kernel", 1991, "C", False),
                 ("GCC", 1987, "C", False),
                 ("Frostwire", 2004, "Java", False)]

class TreeViewFilterWindow(Gtk.Window):

    def __init__(self):
        Gtk.Window.__init__(self, title="Treeview Filter Demo")
        self.set_border_width(10)

        #Setting up the self.grid in which the elements are to be positionned
        self.grid = Gtk.Grid()
        self.grid.set_column_homogeneous(True)
        self.grid.set_row_homogeneous(True)
        self.add(self.grid)

        #Creating the ListStore model
        self.software_liststore = Gtk.ListStore(str, int, str, bool)
        for software_ref in software_list:
            self.software_liststore.append(list(software_ref))
        self.current_filter_language = None

        #Creating the filter, feeding it with the liststore model
        self.language_filter = self.software_liststore.filter_new()
        #setting the filter function, note that we're not using the
        self.language_filter.set_visible_func(self.language_filter_func)

        #creating the treeview, making it use the filter as a model, and adding the columns
        self.treeview = Gtk.TreeView.new_with_model(self.language_filter)
        for i, column_title in enumerate(["Software", "Release Year", "Programming Language"]):
            renderer = Gtk.CellRendererText()
            column = Gtk.TreeViewColumn(column_title, renderer, text=i)
            self.treeview.append_column(column)
        renderer_toggle = Gtk.CellRendererToggle()
        renderer_toggle.connect("toggled", self.on_cell_toggled)
        column_toggle = Gtk.TreeViewColumn("toggle", renderer_toggle, active=3)
        self.treeview.append_column(column_toggle)

        #creating buttons to filter by programming language, and setting up their events
        self.buttons = list()
        for prog_language in ["Java", "C", "C++", "Python", "None"]:
            button = Gtk.Button(prog_language)
            self.buttons.append(button)
            button.connect("clicked", self.on_selection_button_clicked)

        #setting up the layout, putting the treeview in a scrollwindow, and the buttons in a row
        self.scrollable_treelist = Gtk.ScrolledWindow()
        self.scrollable_treelist.set_vexpand(True)
        self.grid.attach(self.scrollable_treelist, 0, 0, 8, 10)
        self.grid.attach_next_to(self.buttons[0], self.scrollable_treelist, Gtk.PositionType.BOTTOM, 1, 1)
        for i, button in enumerate(self.buttons[1:]):
            self.grid.attach_next_to(button, self.buttons[i], Gtk.PositionType.RIGHT, 1, 1)
        self.scrollable_treelist.add(self.treeview)

        self.show_all()
    def on_cell_toggled(self, widget, path):
        self.software_liststore[path][3] = not self.software_liststore[path][3]

    def language_filter_func(self, model, iter, data):
        """Tests if the language in the row is the one in the filter"""
        if self.current_filter_language is None or self.current_filter_language == "None":
            return True
        else:
            return model[iter][2] == self.current_filter_language

    def on_selection_button_clicked(self, widget):
        """Called on any of the button clicks"""
        #we set the current language filter to the button's label
        self.current_filter_language = widget.get_label()
        print("%s language selected!" % self.current_filter_language)
        #we update the filter, which updates in turn the view
        self.language_filter.refilter()


win = TreeViewFilterWindow()
win.connect("delete-event", Gtk.main_quit)
win.show_all()
Gtk.main()

Solution

  • The path in on_cell_toggled() is the path in the filtered model since that's the model applied to your GtkTreeView. You will need to convert it to the path in the unfiltered model before you can change that. To do that, use the convert_path_to_child_path() method of the filtered model.

    If you add sorting later, you will need to do the same with your sorted model, and with both a sorted model and a filtered model, you will need to call both methods. Regardless of how many layers you put on top of your raw model, your renderer's event handler will always give you a path to the layer that your GtkTreeView knows about.

    The same rules apply to manipulating tree view selections as well.