PyGTK4 ColumnView sorting

Hi,

I must have tried this a million ways. I’ve used openai and github copilot to solicit results. But i can’t seem to get column sorting working in ColumnView. I’ve looked at other solutions here also to no result. I’ve read some place that column sorting was broken.

Any feedback welcome

Some verions below.

daoneill@redhat:~/src/authy-gnupass-totp$ pip3 list | grep -i object
PyGObject 3.48.2
daoneill@redhat:~/src/authy-gnupass-totp$ sudo rpm -qa | grep ^gtk4
gtk4-4.14.3-1.fc40.x86_64
daoneill@redhat:~/src/authy-gnupass-totp$ gnome-shell --version
GNOME Shell 46.1

1 Like

Have you checked this topic: GTK4 - ColumnView Sorting - #3 by gtkuser?

sorting - How Having trouble adding a sort function for multiple columns when using the GTK 4 Columnview list model. C coding, a Linux/GNOME enviroment - Stack Overflow

Is in c , but shows the prinziple.

Best regards

@Holger @rafaelmardojai
Thank you.

In fact my code does work. If i put static data in the ListStore, and click the columns it does sort.

However since i’m updating the ListStore every second with random data the SortListModel doesn’t keep the column sorted.

How do i notify the SortListModel that the DataProperties of the objects in the ListStore have changed and to resort the column automatically ?

I found a solution, not sure if it correct, but after the model changes, you can have the SortListModel updated by getting the sorter and resetting it.

    def update_data(self):
        print("Updating data...")
        for i, item in enumerate(self.store):
            random_text = "".join(
                random.choices(string.ascii_uppercase + string.digits, k=5)
            )
            random_number = random.randint(5555, 9999)
            item.text = random_text
            item.number = random_number
            item.truefalse = random.choice([True, False])

        sorter = self.sort_model.get_sorter()
        self.sort_model.set_sorter(None)
        self.sort_model.set_sorter(sorter)
        # Return True to keep the timeout
        return True

i would have thought that the sortListModel would auto resort.
I’m sure there is a solution for this? Or im doing something wrong?

Use Gtk.Sorter.changed() to notify of changes in the sorter.

Full working copy for the next person, please close

import random
import string

import gi

gi.require_version("Gtk", "4.0")
from gi.repository import Gio, GLib, GObject, Gtk  # noqa


class DataObject(GObject.GObject):

    __gtype_name__ = "DataObject"

    text = GObject.Property(type=GObject.TYPE_STRING, default="")
    number = GObject.Property(type=GObject.TYPE_FLOAT, default=0)
    truefalse = GObject.Property(type=GObject.TYPE_BOOLEAN, default=False)

    def __init__(self, text, number, truefalse=False):

        super().__init__()

        self.text = text
        self.number = number
        self.truefalse = truefalse


class MyApp(Gtk.Application):
    def __init__(self):
        super().__init__(application_id="org.gtk.Example")
        self.column_view = Gtk.ColumnView()
        self.store = Gio.ListStore.new(DataObject)
        self.update_timer = None

    @staticmethod
    def to_str(bind, from_value):
        return str(from_value)

    def update_data(self):
        print("Updating data...")
        for i, item in enumerate(self.store):
            random_text = "".join(
                random.choices(string.ascii_uppercase + string.digits, k=5)
            )
            random_number = random.randint(5555, 9999)
            item.text = random_text
            item.number = random_number
            item.truefalse = random.choice([True, False])

        # Randomly remove 1 or more items if there are more than 5 items
        if len(self.store) > 5:
            remove_count = random.randint(1, len(self.store) - 5)
            for _ in range(remove_count):
                remove_index = random.randint(0, len(self.store) - 1)
                self.store.remove(remove_index)

        # Randomly add 1 or more items if there are less than 20 items
        if len(self.store) < 20:
            add_count = random.randint(1, 20 - len(self.store))
            for _ in range(add_count):
                random_text = "".join(
                    random.choices(string.ascii_uppercase + string.digits, k=5)
                )
                random_number = random.randint(5555, 9999)
                truefalse = random.choice([True, False])
                self.store.append(DataObject(random_text, random_number, truefalse))

        sorter = self.sort_model.get_sorter()
        sorter.changed(0)
        return True

    def init_columnview(self):
        properties = [prop.name for prop in DataObject.list_properties()]
        for x in range(10):
            self.store.append(DataObject("entry", 0.0))

        for i, property_name in enumerate(properties):
            factory = Gtk.SignalListItemFactory()
            factory.connect("setup", self.setup, property_name)
            factory.connect("bind", self.bind, property_name)
            column = Gtk.ColumnViewColumn.new(property_name, factory)
            
            # Create a Gtk.Expression for the property
            property_expression = Gtk.PropertyExpression.new(DataObject, None, property_name)
            
            # Create a Gtk.Sorter based on the property type
            property_type = DataObject.find_property(property_name).value_type.fundamental
            if property_type == GObject.TYPE_STRING:
                sorter = Gtk.StringSorter.new(property_expression)
            elif property_type == GObject.TYPE_FLOAT:
                sorter = Gtk.NumericSorter.new(property_expression)
            elif property_type == GObject.TYPE_BOOLEAN:
                sorter = Gtk.NumericSorter.new(property_expression)
            
            # Set the sorter on the column
            column.set_sorter(sorter)

            self.column_view.append_column(column)
            
        sorter = Gtk.ColumnView.get_sorter(self.column_view)
        self.sort_model = Gtk.SortListModel.new(self.store, sorter)
        self.selection = Gtk.SingleSelection.new(self.sort_model)
        self.selection.connect("selection-changed", self.on_selection_changed)
        self.column_view.set_model(self.selection)
        self.update_timer = GLib.timeout_add_seconds(1, self.update_data)

    def on_selection_changed(self, selection, position, item):
        item = selection.get_selected_item()
        if item is not None:
            print(f"Selected item: {item.text}, {item.number}, {item.truefalse}")
        else:
            print("No item selected")


    def setup(self, widget, item, property_name):
        def setup_when_idle():
            obj = item.get_item()
            property_type = obj.find_property(property_name).value_type
            if property_type == GObject.TYPE_BOOLEAN:
                widget_type = Gtk.CheckButton
            else:
                widget_type = Gtk.Label
            widget = widget_type()
            item.set_child(widget)

        GLib.idle_add(setup_when_idle)

    def bind(self, widget, item, property_name):
        def bind_when_idle():
            child = item.get_child()
            obj = item.get_item()
            if obj is not None:
                property_type = obj.find_property(property_name).value_type
                if property_type == GObject.TYPE_BOOLEAN:
                    widget_property = "active"
                    obj.bind_property(
                        property_name,
                        child,
                        widget_property,
                        GObject.BindingFlags.SYNC_CREATE,
                    )
                else:
                    widget_property = "label"
                    obj.bind_property(
                        property_name,
                        child,
                        widget_property,
                        GObject.BindingFlags.SYNC_CREATE,
                        self.to_str,
                    )

        GLib.idle_add(bind_when_idle)

    def do_activate(self):
        win = Gtk.ApplicationWindow(
            application=self,
            title="Gtk4 is Awesome !!!",
            default_height=400,
            default_width=400,
        )
        sw = Gtk.ScrolledWindow()

        self.init_columnview()

        sw.set_child(self.column_view)
        win.set_child(sw)
        win.present()


app = MyApp()
app.run(None)