More on lists in GTK 4

The previous post introduced the overall list view framework that was introduced in the 3.98.5 release, and showed some examples. In this post, we’ll dive deeper into some technical areas, and look at some related topics.

Trees

One important feature of GtkTreeView is that it can display trees. That is what it is named for, after all. GtkListView puts the emphasis on plain lists, and makes handling those easier. In particular the GListModel api is much simpler than GtkTreeModel—which is why there have been comparatively few custom tree model implementations, but GTK 4 already has a whole zoo of list models.

But we still need to display trees, sometimes. There are some complexities around this, but we’ve figured out a way to do it.

Models

The first ingredient that we need is a model. Since GListModel represents a linear list of items, we have to work a bit harder to make it handle trees.

GtkTreeListModel does this by adding a way to expand items, by creating new child list models on demand. The items in a GtkTreeListModel are instances of GtkTreeListRow which wrap the actual items in your model, and there are functions such as gtk_tree_list_row_get_children() to get the items of the child model and gtk_tree_list_row_get_item() to get the original item. GtkTreeListRow has an :expanded property that is used to keep track of whether the children are currently shows or not.

At the core of  a tree list model is the GtkTreeListModelCreateModelFunc which takes an item from your list and returns a new list model containing items of the same type that should be the children of the given item in the tree.

Here is an example for a tree list model of GSettings objects. The function enumerates the child settings of a given GSettings object, and returns a new list model for them:

static GListModel *
create_settings_model (gpointer item,
                       gpointer unused)
{
  GSettings *settings = item;
  char **schemas;
  GListStore *result;
  guint i;

  schemas = g_settings_list_children (settings);

  if (schemas == NULL || schemas[0] == NULL)
    {
      g_free (schemas);
      return NULL;
    }

  result = g_list_store_new (G_TYPE_SETTINGS);
  for (i = 0; schemas[i] != NULL; i++)
    {
      GSettings *child = g_settings_get_child (settings, schemas[i]);
      g_list_store_append (result, child);
      g_object_unref (child);
    }

  g_strfreev (schemas);

  return G_LIST_MODEL (result);
}

Expanders

The next ingredient that we need is a widget that displays the expander arrow that users can click to control the :expanded property. This is provided by the GtkTreeExpander widget. Just as the GtkTreeListRow items wrap the underlying items in your model, you use GtkTreeExpander widgets to wrap the widgets used to display your items.

Here is how the tree expanders look in action, for our GSettings example:

The full example can be found here.

Sorting

One last topic to touch on before we leave trees is sorting. Lists often have more than one way of being sorted: a-z, z-a, ignoring case, etc, etc. Column views support this by letting you associate sorters with columns, which the user can then activate by clicking on the column headers. The API for this is

gtk_column_view_column_set_sorter (column, sorter)

When sorting trees, you typically want the sort order to apply to the items at a given level in the tree, but not across levels, since that would scramble the tree structure. GTK supports this with the GtkTreeListRowSorter, which wraps an existing sorter and makes it respect the tree structure:

sorter = gtk_column_view_get_sorter (view);
tree_sorter = gtk_tree_list_row_sorter_new (sorter);
sort_model = gtk_sort_list_model_new (tree_model,           
                                      tree_sorter);
gtk_column_view_set_model (view, sort_model);

In conclusion, trees have been de-emphasized a bit in the new list widgets, and they add quite a bit of complication to the machinery, but they are fully supported, in list views as well as in column views:

Combo boxes

One of the more problematic areas where cell renderers are used is our single-selection control: GtkComboBox.  This was never a great fit, in particular in combination with nested menus. So, we were eager to try the new list view machinery on a replacement for GtkComboBox.

From the design side, there has also been a longstanding wishlist item to do better for combo boxes, as can be seen in this mockup from 2015:

Five years later, we finally have a replacement widget. It is called GtkDropDown. The API of the new widget is as minimal as possible, almost all the work is done by list model and item factory machinery.  Basically, you create a dropdown with gtk_drop_down_new(), then you give it a factory and a model, and you are done.

Since most choices consist of simple strings, there is a convenience method that creates the model and factory for you, from a string array:

const char * const times[] = {
  "1 minute",
  "2 minutes",
  "5 minutes",
  "20 minutes",
  NULL
};

button = drop_down_new ();
gtk_drop_down_set_from_strings (GTK_DROP_DOWN (button), times);

This convenience API is very similar to GtkComboBoxText, and the GtkBuilder support is also very similar. You can specify a list of strings in the ui file like this:

<object class="GtkDropDown">
  <items>
    <item translatable="yes">Factory</item>
    <item translatable="yes">Home</item>
    <item translatable="yes">Subway</item>
  </items>
</object>

Here are a few GtkDropDowns in action:

Summary

To stress this point again: all of this is brandnew API, and we’d love to hear your feedback on what works well, what doesn’t, and what is missing.

Scalable lists in GTK 4

One of the last big missing pieces in GTK 4 is the infrastructure for new list and grid widgets. It has just been merged and is included in the 3.98.5 release. So it is time to take a closer look.

History: tree views and list boxes

Since ancient times (ie GTK 2), GtkTreeView has been the go-to data display widget in GTK. It uses a model-view pattern to keep the data separate from the display infrastructure. Over the years, it has grown a grid-display sibling (GtkIconView) and a selection cousin (GtkComboBox), using the same infrastructure (tree models and cell renderers).

Unfortunately, the approach taken for rendering in GtkTreeView with cell renderers left us with a schism: widgets use one set of vfuncs and technologies for size allocation and rendering, and cell renderers use another. One of the unfortunate consequences of this split is that it is very hard to do animations in tree views (since cell renderers don’t keep state). Another is that most of the advances of the GTK CSS rendering machinery are unavailable in cell renderers.

Therefore, we would really like to use widgets for displaying the data in lists. During the GTK 3 era, we have introduced a number of containers for this purpose: GtkListBox for lists, and GtkFlowBox for grids. They don’t use cell renderers, so the aforementioned limitations are not a concern. And they can even use list models to hold the data. But they generate a widget for each data item, and this severely limits their scalability. As a rule of thumb, GtkListBox can handle 1 000 items well, while GtkTreeView works fine for 100 000.

Overcoming the scalability limitations, while still using widgets for all rendering has been on our roadmap for a long time.

Scalability limits
Widget Scalability
GtkIconView 100
GtkListBox 1 000
GtkTreeView 100 000
GtkListView unlimited

New infrastructure

With list view family of widgets, we hope to  finally achieve that. The goal is to handle unlimited numbers of items well. If you’ve worked with things like the Android recycler view, you will recognize the basic ideas behind list views:

  • The data for items is provided in the form of a model (containing objects)
  • Widgets are created just for the viewable range of items
  • Widgets can be recycled by binding them to different items
  • Avoid iterating over all items in the model as much as possible, and just deal with the items in the visible range which are bound to widgets

Models

For the model side of our model-view architecture, we’ve moved away from GtkTreeModel, and are now using GListModel. There are several reasons for this. One is that we want the items to be objects with properties, so we can use property bindings.  Another one is that GtkTreeModel is not all that scalable in the first place (see e.g. this case of unintentional quadratic behavior).

GTK 4 comes with a rich assortment of GListModel implementations, from various ways to combine or modify existing models to filtering and sorting.  Filtering and sorting is supported by several GtkFilter and GtkSorter classes. Selections are also handled on the model side, with GtkSelectionModel and its subclasses.

Finally, there are concrete models for the kinds of data that we are dealing with: GtkDirectoryList for files, PangoFontMap for fonts.

APIs that have traditionally returns GLists of various items have been changed or supplemented with APIs returning a GListModel, for example gdk_display_get_monitors(), gtk_window_get_toplevels() and gtk_stack_get_pages().

Factories

Since we are talking about automatically creating widgets on demand, there will be factories involved. GtkListItemFactory is the object that is tasked with creating widgets for the items in the model.

There are different implementations of this factory. One of them, GtkBuilderListItemFactory, is using ui files as templates for the list item widgets.  Here is a typical example:

<interface>
  <template class="GtkListItem">
    <property name="child">
      <object class="GtkLabel">
        <property name="xalign">0</property>
        <property name="wrap">1</property>
        <property name="width-chars">50</property>
        <property name="max-width-chars">50</property>
        <binding name="label">
          <lookup name="summary" type="SettingsKey">
            <lookup name="item">GtkListItem</lookup>
          </lookup>
        </binding>
      </object>
    </property>
  </template>
</interface>

The full example can be found in gtk4-demo.

Another list item factory implementation, GtkSignalListItemFactory, takes callbacks to setup, teardown, bind and unbind widgets from the items.

static void
setup_listitem_cb (GtkListItemFactory *factory,
                   GtkListItem        *list_item)
{
  GtkWidget *image;

  image = gtk_image_new ();
  gtk_image_set_icon_size (GTK_IMAGE (image), GTK_ICON_SIZE_LARGE);
  gtk_list_item_set_child (list_item, image);
}

static void
bind_listitem_cb (GtkListItemFactory *factory,
                  GtkListItem *list_item)
{
  GtkWidget *image;
  GAppInfo *app_info;

  image = gtk_list_item_get_child (list_item);
  app_info = gtk_list_item_get_item (list_item);
  gtk_image_set_from_gicon (GTK_IMAGE (image),
                            g_app_info_get_icon (app_info));
}

static void
activate_cb (GtkListView  *list,
             guint         position,
             gpointer      unused)
{
  GListModel *model;
  GAppInfo *app_info;

  model = gtk_list_view_get_model (list);
  app_info = g_list_model_get_item (model, position);
  g_app_info_launch (app_info, NULL, NULL, NULL);
  g_object_unref (app_info);
}

...

factory = gtk_signal_list_item_factory_new ();
g_signal_connect (factory, "setup", setup_listitem_cb, NULL);
g_signal_connect (factory, "bind", bind_listitem_cb, NULL);

list = gtk_list_view_new_with_factory (factory);
g_signal_connect (list, "activate", activate_cb, NULL);

model = create_application_list ();
gtk_list_view_set_model (GTK_LIST_VIEW (list), model);
g_object_unref (model);

gtk_scrolled_window_set_child (GTK_SCROLLED_WINDOW (sw), list);

The full example can be found here.

Expressions

To bind the widgets that are created by the factory to the data in your items, we need a flexible mechanism for binding properties. GObject’s GBinding mechanism goes in the right direction, but is not flexible enough to handle situations where you may need to bind properties of sub-objects or widgets deep inside the hierarchy of your widget, and where the objects in questions may not even exist at the time you are setting up the binding.

To handle this, we’ve introduced GtkExpression as a more flexible binding system which can express things like:

label = this->item->value

where this is a GtkListItem, which has an item property (of type SettingsKey), whose value property we want to bind to the label property. Expressing the same in a GtkBuilder ui file looks a bit more unwieldy:

<binding name="label">
  <lookup name="value" type="SettingsKey">
    <lookup name="item">GtkListItem</lookup>
  </lookup>
</binding>

New widgets

GtkListView is a simple list, without columns or headers. An example where this kind of list is used is GtkFontChooser. One little way in which GtkListView breaks new ground is that it can be set up as a horizontal list, as well as the usual vertical orientation.

GtkGridView puts the widgets in a reflowing grid, much like GtkFlowBox or GtkIconView.

GtkColumnView is the equivalent of a full GtkTreeView, with multiple columns and headers, and features such as interactive resizing and reordering.  Just like a GtkTreeView has GtkTreeViewColumns, a GtkColumnView has a list of GtkColumnViewColumns. Each column has a factory that produces a cell for each item. The cells are then combined into the row for the item.

Examples

Many of the lists in complex GTK dialogs (although not all yet) have been replaced with the new list widgets. For example, the font chooser is now using a GtkListView, and most of the lists in the GTK inspector use GtkColumnView.

But gtk4-demo contains a number of examples for the new widgets. Here are a few:

The clocks examples shows the advantages of having the full flexibility of widget rendering available.

The colors example shows the a medium-size dataset rendered in  various ways.

The settings example shows that the column view more or less matches GtkTreeView, feature-wise.

Summary

This post gave an introduction to the new list widgets. There’s more that we haven’t touched on here, such as trees or the combo box replacement. One place to learn more about the new apis is the detailed introduction in the GTK documentation.

We’ve merged the listview infrastructure to master now. That doesn’t mean that it is finished. But we think it is ready for some wider use, and we hope to get feedback from you on what works, what doesn’t and what is missing.

And, to be clear, it also does not mean that we are removing treeviews and combo boxes from GTK 4—it is too late for that, and they are still used in many places inside GTK. That may be a GTK 5 goal.