Migrating GTK3 TreeStore to GTK4 ListStore and handling child rows

I can’t seem to get my head around how GTK4 GListStore and GtkTreeListModel come together to provide an equivalent solution to what GtkTreeStore and GtkTreeView provided, specifically when dealing with child rows. I’ve read through List Widget Overview and have been looking at how the demo for the ListView Settings was put together. I see where the GtkTreeExpander comes into play with the display (view) side but I’m missing how I append child items to other items on the GListStore.

The ListView Settings example is interesting but it looks like the list is populated with keys and the GtkTreeListModelCreateModelFunc defined does a real-time lookup of children and returns a new GListStore – effectively creating new lists instead of having one list. My C is a novice level so I may have missed what’s really going on there.

With GtkTreeStore, I would append a child to the store by including the iter of the parent. So, with that thought in mind, I expected to see an ability to append a child to a parent in some manner via the GtkTreeListModel and then it would manage the tagging of the row in some internal mechanism to indicate the relationship, flattening the model so that it could be stored in the GListStore. And then the GtkTreeListRow would recognize parent/child/grandchild relationships to provide proper organization for presentation. Effectively, the GtkTreeListModel being the interface between a tree and the storage of the tree. But GtkTreeListModel does not have append functions so it appears that GtkTreeListModel is intended to provide the interface for the view operation (and sort and filter). That leaves me with how to populate the GListStore with both parent and child rows, properly linked to support the view with expanders.

I did manage to successfully populate a GListStore with multiple GObjects that represent the different logical rows. For instance:

class SideBarGroupRow(GObject.GObject):
sortval: int
group: str
favorite: bool
‘’’ For Trees and Tree models, you need to create your own custom data objects based on Gobject ‘’’

def __init__(self, sortval: int, group: str, favorite: bool):
    super().__init__()
    self.sortval = sortval
    self.group = group
    self.favorite = favorite

def __repr__(self):
    return f'SideBarRow(sortval: {self.sortval} group: {self.group} favorite: {self.favorite})'

class SideBarAccountRow(GObject.GObject):
group: str
name: str
favorite: bool
isVisible: bool
tableName: str
itemID: int
acctType: str
‘’’ For Trees and Tree models, you need to create your own custom data objects based on Gobject ‘’’

def __init__(self, group: str, name: str, favorite: bool, isVisible: bool, tableName: str, itemID: int, acctType: str):
    super().__init__()
    self.group = group
    self.name = name
    self.favorite = favorite
    self.isVisible = isVisible
    self.tableName = tableName
    self.itemID = itemID
    self.acctType = acctType

def __repr__(self):
    return f'SideBarRow(group: {self.group} name: {self.name} favorite: {self.favorite} isVisible: {self.isVisible} tableName: {self.tableName} itemID: {self.itemID} acctType: {self.acctType})'

And then simple appends:

   sideBarList = Gio.ListStore()
    # Group/Category Row
   sideBarList.append(SideBarGroupRow(None, groupName, False)) 
   # Child Accounts
   sideBarList.append(SideBarAccountRow(acctName, None, True, True, 'account',
                                    acctId, acctType))

But with this approach, I haven’t seen how I would connect the GtkTreeListModel and its GtkTreeListModelCreateModelFunc to that list. I have a list of GObjects, but need a way to connect them into the TreeListModel for sorting, filtering, expander, selection, and view purposes. That’s telling me that I’m likely headed down the wrong path and am not understanding the intended pattern to be used to move away from a TreeStore and handle child rows.

Any and all thoughts, pointers, examples, links… are much appreciated.

Hi there!

I’m relatively new over here (indeed, this is my first reply :face_with_hand_over_mouth: ) and I’m also trying to understand how all this GTK4 stuff works concerning listviews/columnviews sorting/filtering, and of course, how to migrate GTK3 TreeViews.

While I’ve made some progress with listviews/columnviews, I haven’t got enough time to see how to migrate treeviews. Nevertheless, a couple of days ago, I discovered this script which I found messy but interesting (it uses a Gtk.TreeListModel with sorting/filtering):

treelistmodel.py

Not sure if it will be very useful for you (and for me), but just in case, I leave it here.

Good luck!

@t00m Thank you for the link. I had found that one too a number of weeks ago but lost it.

Digging through it:

  • Task2 class defines children as a list inside the class (row 767).
  • They populate the list of children (row 1646).
  • Finally, create the root model and do their append (rows 1649 - 1654).

The TreeListModel is then created pointing to the model func. That function (row 1377) inspects the item from the root model, retrieving any children out of the object and placing them on a new GListStore which gets returned.

So, I think the part that I was missing is that I need to maintain the relationships. In the GTG example, they used a list on the object. In the Settings demo, I believe they make a call to the GtkSettings modules to retrieve the children. Either way, this gives me a direction to explore.

Thanks for taking the time to put this example out there.

Don’t know if an example in C++ can help you, but there is one here:

Gtkmm-demos, files demowindow.h and demowindow.cc. (You might also want to look at demos.h.)

Even though this is a fairly simple example, it was not a trivial task to upgrade it
from TreeStore/TreeView to TreeListModel/ListView. It’s a static list without
sorting and filtering.

I’m missing how I append child items to other items on the GListStore.
…effectively creating new lists instead of having one list.

Yes, that’s what your code shall do. You create a GListModel with the top items in
the tree, including the expandable items (those having child items). This list does not
contain the child items. Then the GtkTreeListModel will call the GtkTreeListModelCreateModelFunc
for each item in the list. If the item has children, the GtkTreeListModelCreateModelFunc
shall return a new GListModel with the children.

Apart from having several GListModels, there are two peculiarities that took me
some time to understand:

  • The GObject returned by gtk_list_item_get_item() is not the GObject in the
    GListModels you create. It’s a GtkTreeListRow. gtk_tree_list_row_get_item()
    returns the GObject from the GListModel.

  • To get reasonable indentations in the ListView, all GtkListItem instances,
    not just the expandable ones, shall have a GtkTreeExpander child.

1 Like

@kjellahl Thanks for the example and the tips. Those are very helpful.

I’m getting strange Builder errors related to properties on the class that are there but Builder doesn’t seem to be able to find them.

(main.py:47877): Gtk-CRITICAL **: 10:44:25.466: Error building template for list item: .:0:0: Type SideBarRow does not have a property name name

(main.py:47877): Gtk-CRITICAL **: 10:44:25.466: Error building template for list item: .:0:0: Type SideBarRow does not have a property name favorite

My layout.ui file containing the XML:

<?xml version='1.0' encoding='UTF-8'?>
<interface>
    <object class="GtkApplicationWindow" id="baseWindow">
        <property name="default-height">600</property>
        <property name="default-width">800</property>
        <property name="hexpand">True</property>
        <property name="title">My Side List</property>
        <property name="vexpand">True</property>
        <property name="resizable">True</property>
        <child>
            <object class="GtkBox">
                <property name="orientation">vertical</property>
                <child>
                    <object class="GtkScrolledWindow">
                        <property name="hexpand">1</property>
                        <property name="vexpand">1</property>
                        <child>
                            <object class="GtkColumnView" id="columnview">
                                <child>
                                    <object class="GtkColumnViewColumn">
                                        <property name="title">Account Name</property>
                                        <property name="factory">
                                            <object class="GtkBuilderListItemFactory">
                                                <property name="bytes">
                                                    <![CDATA[
<?xml version="1.0" encoding="UTF-8"?>
  <interface>
    <template class="GtkListItem">
      <property name="child">
        <object class="GtkLabel">
          <property name="xalign">1</property>
          <binding name="label">
            <lookup name="name" type="SideBarRow">
              <lookup name="item">GtkListItem</lookup>
            </lookup>
          </binding>
        </object>
      </property>
    </template>
  </interface>
                                            ]]>
                                                </property>
                                            </object>
                                        </property>
                                    </object>
                                </child>
                                <child>
                                    <object class="GtkColumnViewColumn">
                                        <property name="title">Favorite</property>
                                        <property name="factory">
                                            <object class="GtkBuilderListItemFactory">
                                                <property name="bytes">
                                                    <![CDATA[
<?xml version="1.0" encoding="UTF-8"?>
  <interface>
    <template class="GtkListItem">
      <property name="child">
        <object class="GtkSwitch">
          <binding name="active">
            <lookup name="favorite" type="SideBarRow">
              <lookup name="item">GtkListItem</lookup>
            </lookup>
          </binding>
        </object>
      </property>
    </template>
  </interface>
                                            ]]>
                                                </property>
                                            </object>
                                        </property>
                                    </object>
                                </child>
                            </object>
                        </child>
                    </object>
                </child>
            </object>
        </child>
    </object>
</interface>

main.py that consumes the layout.ui

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

class SideBarRow(GObject.Object):
    __gtype_name__ = "SideBarRow"
    ''' For Trees and Tree models, you need to create your own custom data objects based on GObject '''

    def __init__(self, name: str, favorite: bool):
        super().__init__()
        self.__name = name
        self.favorite = favorite
        self.children = []
        print("SideBarRow name: " + self.name)
        # print("magic: " + str(dir(self)))

    @property
    def name(self):
        return self.__name

    @name.setter
    def name(self, value):
        self.__name = value

class MyApp(Gtk.Application):
    def __init__(self):
        Gtk.Application.__init__(self, application_id='com.example.myfinapp')
        super(MyApp, self).__init__()
        self.builder = Gtk.Builder()
        self.builder.add_from_file("layout.ui")
        self.connect('activate', self.on_activate)
        self.sideBarList = create_sidebar_model(None, None, None)
        # self.sideBarList = Gio.ListStore.new(SideBarRow)

    def on_activate(self, app):
        self.load_list()
        self.treeModel = Gtk.TreeListModel.new(self.sideBarList, False, True, create_sidebar_model, None, None)
        self.selection = Gtk.SingleSelection.new(self.treeModel)
        self.columnview = self.builder.get_object("columnview")
        self.columnview.set_model(self.selection)
        self.win = self.builder.get_object("baseWindow")
        self.add_window(self.win)
        self.win.present()

    def load_list(self):
        accounts = [
            {
                'acctName': 'Checking Account',
                'acctType': 'Banking',
                'isFavorite': True
            },
            {
                'acctName': 'Savings Account',
                'acctType': 'Banking',
                'isFavorite': True
            },
            {
                'acctName': 'My Mort',
                'acctType': 'Loan',
                'isFavorite': False
            }
        ]
        groupName = None
        for account in accounts:
            if not (str(account['acctType']) == groupName):
                if groupName == None:
                    toprow = SideBarRow(account['acctType'], False)
                else:
                    self.sideBarList.append(toprow)
                    toprow = SideBarRow(account['acctType'], False)
                groupName = str(account['acctType'])

            childrow = SideBarRow(account['acctName'],account['isFavorite'])
            toprow.children.append(childrow)

        self.sideBarList.append(toprow)
        for obj in self.sideBarList:
            print("ListStore Object: ", str(obj.name))

def create_sidebar_model(item, unused1, unused2):
    model = Gio.ListStore.new(SideBarRow)
    if item == None:
        return model
    else:
        if type(item) == Gtk.TreeListRow:
            item = item.get_item()

        if item.children:
            for child in item.children:
                model.append(child)
                print("Added Child Model: ", child.name)
            return model
        else:
            # print("No Child Model")
            # returning an empty model so future children will be displayed
            return model


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

I have been stepping though the gtk demo C example for the listview which includes column view. I get a list in a window but no text connected to labels and the switch widget. The column titles come through. So, it is progress, but I can’t seem to get the Builder to reference the properties/attributes of the objects on the liststore.

Maybe I’m making an assumption that the Builder will retrieve the object from the GtkTreeListRow object? I’ve been able to account for that in the GtkTreeListModelCreateModelFunc for use with retrieving the child objects. But I don’t see where in the C demo that it does this with the Builder.

Thoughts?

I resolved the problem with the Builder not finding the properties. While the class had the properties, the GObject didn’t have them defined. I adjusted my class definition to use the GObject decorator to give the GObject the properties:

class SideBarRow(GObject.Object):
    __gtype_name__ = "SideBarRow"
    ''' For Trees and Tree models, you need to create your own custom data objects based on GObject '''

    def __init__(self, name: str, favorite: bool = False):
        super().__init__()
        self.__name = name
        self.__favorite = favorite
        self.children = []
        print("SideBarRow name: " + self.name)
        # print("magic: " + str(dir(self)))

    @GObject.Property(type=str)
    def name(self):
        return self.__name

    @name.setter
    def name(self, value):
        self.__name = value

    @GObject.Property(type=bool, default=False)
    def favorite(self):
        return self.__favorite

    @favorite.setter
    def favorite(self, value):
        self.__favorite = value

The 2 key pieces so far seem to be the gtype_name to match the type name in the XML and setting up your object with the @GObject.Property decorator.

The builder still isn’t connecting the value of the name property with the GtkLabel in the template, nor the GtkSwitch’s active property with the favorite true/false.

Thanks for any observations anyone has.

I’m making some progress. I can get a Gtk.ListView to display a Gtk.TreeExpander with the UI file and bind the name property of the GObject to the Gtk.Label of the TreeExpander within the GtkListItem template.

However, applying the same technique to a Gtk.ColumnView allows me to bind the first column (name column) where the template includes a TreeExpander, but the second column (favorite column) that is purely a Label does not want to be bound to a different (or the same) property of the GObject (SideBarRow). I can make it work by introducing the TreeExpander into the second GtkColumnViewColumn but that’s not what I want. Ultimately I want a different Widget for the second column other than a Label but am trying to simplify it to get it working first.

Even if I remove/comment out the first GtkColumnViewColumn definition, the second one for favorites does not want to bind even if I try to bind it to the name property of the GObject that works with the TreeExpander. The only way I managed to get the Label working was by introducing the TreeExpander. It almost feels like Builder wants an additional Widget layer included but that’s inconsistent with the demos I’ve looked at.

Obviously I’m missing something. I’ve spent days comparing the xml against various demos like the ListView Settings (link in first message) without success. It feels close but I can’t get the binding via the Builder to function as it appears to do in the C examples for anything beyond the first column and then only when I include the GtkTreeExpander. Thoughts? Suggestions? Help?

Here’s the ui and python code.

layout.ui

<?xml version='1.0' encoding='UTF-8'?>
<interface>
    <object class="GtkApplicationWindow" id="baseWindow">
        <property name="default-height">600</property>
        <property name="default-width">800</property>
        <property name="hexpand">True</property>
        <property name="title">My Side List</property>
        <property name="vexpand">1</property>
        <property name="resizable">1</property>
        <child>
            <object class="GtkBox">
                <property name="orientation">vertical</property>
                <child>
                    <object class="GtkScrolledWindow" id="scrolledwin">
                        <property name="hexpand">1</property>
                        <property name="vexpand">1</property>
                        <child>
                            <object class="GtkColumnView" id="columnview">
                                <!--    Define the Account Name Column     -->
                                <child>
                                    <object class="GtkColumnViewColumn">
                                        <property name="title">Account Name</property>
                                        <property name="resizable">1</property>
                                        <property name="factory">
                                            <object class="GtkBuilderListItemFactory">
                                                <property name="bytes">
                                                    <![CDATA[
<?xml version="1.0" encoding="UTF-8"?>
  <interface>
    <template class="GtkListItem">
      <property name="child">
        <object class="GtkTreeExpander" id="expander">
            <property name="indent-for-icon">1</property>
            <binding name="list-row">
                <lookup name="item">GtkListItem</lookup>
            </binding>
            <property name="child">
                <object class="GtkLabel">
                  <property name="xalign">0</property>
                  <binding name="label">
                    <lookup name="name" type="SideBarRow">
                      <lookup name="item">expander</lookup>
                    </lookup>
                  </binding>
                </object>
            </property>
        </object>
      </property>
    </template>
  </interface>
                                            ]]>
                                                </property>
                                            </object>
                                        </property>
                                    </object>
                                </child>
                                <!--    Define the Favorite Column     -->
                                <child>
                                    <object class="GtkColumnViewColumn">
                                        <property name="title">Favorite</property>
                                        <property name="visible">True</property>
                                        <property name="factory">
                                            <object class="GtkBuilderListItemFactory">
                                                <property name="bytes">
                                                    <![CDATA[
<?xml version="1.0" encoding="UTF-8"?>
  <interface>
    <template class="GtkListItem">
      <property name="child">
        <object class="GtkLabel">
          <binding name="label">
            <lookup name="favorite" type="SideBarRow">
              <lookup name="item">GtkListItem</lookup>
            </lookup>
          </binding>
        </object>
      </property>
    </template>
  </interface>
                                            ]]>
                                                </property>
                                            </object>
                                        </property>
                                    </object>
                                </child>
                            </object>
                        </child>
                    </object>
                </child>
            </object>
        </child>
    </object>
</interface>

main.py

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

class SideBarRow(GObject.Object):
    ''' For Trees and Tree models, you need to create your own custom data objects based on GObject '''
    __gtype_name__ = "SideBarRow"

    def __init__(self, name: str, favorite: str):
    # def __init__(self, name: str, favorite: bool = False):
        super().__init__()
        self._name = name
        self._favorite = favorite
        self.children = []

    @GObject.Property(type=str)
    def name(self):
        return self._name

    @name.setter
    def name(self, value):
        self._name = value

    # @GObject.Property(type=bool, default=False)
    @GObject.Property(type=str)
    def favorite(self):
        return self._favorite

    @favorite.setter
    def favorite(self, value):
        self._favorite = value


class MyApp(Gtk.Application):
    def __init__(self):
        super().__init__(application_id='com.example.myfinapp')
        self.builder = Gtk.Builder()
        self.builder.add_from_file("layout.ui")
        self.connect('activate', self.on_activate)
        self.sideBarList = create_sidebar_model(None)

    def on_activate(self, app):
        self.load_list()
        self.treeModel = Gtk.TreeListModel.new(
            root=self.sideBarList,
            passthrough=False,
            autoexpand=True,
            create_func=create_sidebar_model)
        self.selection = Gtk.SingleSelection(model=self.treeModel)
        self.columnview = self.builder.get_object("columnview")
        self.columnview.set_model(self.selection)
        self.scrolledwin = self.builder.get_object("scrolledwin")
        self.scrolledwin.set_child(self.columnview)
        self.win = self.builder.get_object("baseWindow")
        self.add_window(self.win)
        self.win.present()

    def load_list(self):
        accounts = [
            {
                'acctName': 'Checking Account',
                'acctType': 'Banking',
                'isFavorite': 'True'
            },
            {
                'acctName': 'Savings Account',
                'acctType': 'Banking',
                'isFavorite': 'True'
            },
            {
                'acctName': 'My Mort',
                'acctType': 'Loan',
                'isFavorite': 'False'
            }
        ]
        groupName = None
        for account in accounts:
            if not (str(account['acctType']) == groupName):
                if groupName == None:
                    toprow = SideBarRow(account['acctType'], "False")
                else:
                    self.sideBarList.append(toprow)
                    toprow = SideBarRow(account['acctType'], "False")
                groupName = str(account['acctType'])

            childrow = SideBarRow(account['acctName'],account['isFavorite'])
            toprow.children.append(childrow)

        self.sideBarList.append(toprow)
        for obj in self.sideBarList:
            print("ListStore Object: ", str(obj.name), str(obj.favorite))

def create_sidebar_model(item):
    model = Gio.ListStore(item_type=SideBarRow)
    if item == None:
        return model
    else:
        if type(item) == Gtk.TreeListRow:
            item = item.get_item()

        if item.children:
            for child in item.children:
                model.append(child)
                print("Added Child Model: ", child.name, child.favorite)
            return model
        else:
            # print("No Child Model")
            return None
            # return model
            # Desire returning an empty model so future children will be displayed
            # consequence of returning an empty model is that the TreeExpander icon shows on the child
            # even if there currently are no children of the child and there is no way to suppress/hide
            # the expander icon.

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