Skip to content

List Models

List models are a simple interface for ordered lists of GObject instances. It has become the preferred method for populating list, grid and tree widgets. A default implementation is provided, optimized for linear iteration.

GTK4 also includes additional Gio.ListModel implementations that can filter, sort, flatten nested lists, and more. This guide focuses on the implementation and usage of Gio.ListModel, primarily as introductory material for list view widgets and others in GTK4.

See Also

Basic Implementation

NOTE

Gio.ListModel only works with types that inherit from GObject.Object, and will not accept boxed types such as GObject.TYPE_JSOBJECT.

Gio.ListModel defines a read-only interface, intended to be used from a single thread. This makes implementation very simple, requiring only three methods, and emitting one signal at the appropriate time.

  1. Gio.ListModel.get_item(position)

    This method must return the GObject.Object at position, or null if an invalid position is passed. This allows easily iterating the list, without checking the length.

  2. Gio.ListModel.get_item_type()

    This method must return a GType shared by all objects in the list. It may simply be GObject.Object, or any other common ancestor or GObject.Interface of the objects.

  3. Gio.ListModel.get_n_items()

    This method must return the number of items in the list. More importantly, when Gio.ListModel::items-changed is emitted, it must return the current value.

The ordered nature of the Gio.ListModel interface also makes it easy to add support for the JavaScript Iterator protocol. The best way to demonstrate the list model API is to wrap one around an Array and expose the elements:

js
import GObject from 'gi://GObject';
import Gio from 'gi://Gio';


var ArrayStore = GObject.registerClass({
    Implements: [Gio.ListModel],
}, class ArrayStore extends GObject.Object {
    /* A native Array as internal storage for the list model */
    #items = [];

    /*
     * Wrapping the internal iterable is an easy way to support `for..of` loops
     */
    *[Symbol.iterator]() {
        for (const item of this.#items)
            yield item;
    }

    /**
     * Gets the item at @position.
     *
     * If @position is greater than the number of items in the list, %null is
     * returned. %null is never returned for a position that is smaller than
     * the length of the list.
     *
     * @param {number} position - the position of the item to fetch
     * @returns {GObject.Object|null} - the object at @position
     */
    vfunc_get_item(position) {
        return this.#items[position] || null;
    }

    /**
     * Gets the item type of the list.
     *
     * All items in the model must of this type, or derived from it. If the
     * type itself is an interface, the items must implement that interface.
     *
     * @returns {GObject.GType} the type of object in the list
     */
    vfunc_get_item_type() {
        return GObject.Object;
    }

    /**
     * Gets the number of items in the list.
     *
     * Depending on the model implementation, calling this function may be
     * less efficient than iterating the list with increasing values for
     * position until `null` is returned.
     *
     * @returns {number} - a positive integer
     */
    vfunc_get_n_items() {
        return this.#items.length;
    }

    /*
     * NOTE: The methods below are not part of the GListModel interface.
     */

    /**
     * Insert an item in the list. If @position is greater than the number of
     * items in the list or less than `0` it will be appended to the end of the
     * list.
     *
     * @param {GObject.Object} item - the item to add
     * @param {number} [position] - the position to add the item
     */
    insertItem(item, position = -1) {
        // Type check the item
        if (!(item instanceof GObject.Object))
            throw TypeError(`Not a GObject: ${item.constructor.name}`);

        if (!GObject.type_is_a(item.constructor.$gtype, this.get_item_type()))
            throw TypeError(`Invalid type: ${item.constructor.$gtype.name}`);

        // Normalize the position
        if (position < 0 || position > this.#items.length)
            position = this.#items.length;

        // Insert the item, then emit Gio.ListModel::items-changed
        this.#items.splice(position, 0, item);
        this.items_changed(position, 0, 1);
    }

    /**
     * Remove the item at @position. If @position is outside the length of the
     * list, this function does nothing.
     *
     * @param {number} position - the position of the item to remove
     */
    removeItem(position) {
        // NOTE: The Gio.ListModel interface will ensure @position is an
        //       unsigned integer, but other methods must check explicitly.
        if (position < 0 || position >= this.#items.length)
            return;

        // Remove the item and emit Gio.ListModel::items-changed
        this.#items.splice(position, 1);
        this.items_changed(position, 1, 0);
    }
});

Basic Usage

GListStore

Gio.ListStore is an implementation of Gio.ListModel suitable for common use cases, with a fast-path for iterating the items sequentially. It's usually the best choice for generic usage, and implements the JavaScript Iterator protocol already.

js
import GObject from 'gi://GObject';
import Gio from 'gi://Gio';


const listStore = Gio.ListStore.new(GObject.TYPE_OBJECT);

listStore.connect('items-changed', (_list, position, removed, added) => {
    console.log(`${removed} items were removed, and ${added} added at ${position}`);
});

const listItems = [
    new GObject.Object(),
    new GObject.Object(),
    new GObject.Object(),
];


/*
 * Adding and removing items
 */
listStore.append(listItems[0]);
listStore.insert(1, listItems[1]);
listStore.splice(2, 0, [listItems[2]]);

listStore.remove(0);


/**
 * Example sort function.
 *
 * NOTE: This function must be deterministic to ensure a stable sort.
 *
 * @param {GObject.Object} object1 - a GObject
 * @param {GObject.Object} object2 - a GObject
 * @returns {number} `-1` if @object1 should be before @object2, `0` if
 *     equivalent, or `1` if @object1 should be after @object2.
 */
function sortFunc(object1, object2) {
    return object1 === object2 ? 0 : -1;
}

listStore.sort(sortFunc);

listStore.insert_sorted(new GObject.Object(), sortFunc);


/**
 * Example find function.
 *
 * @param {GObject.Object} object1 - a GObject
 * @param {GObject.Object} object2 - a GObject
 * @returns {boolean} %true if equivalent, %false otherwise
 */
function findFunc(object1, object2) {
    return object1 === object2;
}


let [found, position] = listStore.find(listItems[0]);

if (found)
    console.log('This item will not be found, because it was already removed');


[found, position] = listStore.find_with_equal_func(listItems[1], findFunc);

if (found) {
    console.log(`The item found at position ${position} will be removed`);
    listStore.remove(position);
}

Consuming List Models

Usually objects implementing Gio.ListModel are bound to widget with a convenience function, or wrapped in another model like Gtk.SelectionModel and used to populate a widget like Gtk.ListView.

However, it's helpful to understand how these widgets will typically use list models, when first using widgets that consume list models, to understand how these widgets will use the model internally.

js
import GLib from 'gi://GLib';
import Gio from 'gi://Gio';


/* This array will take the place of a list view, or other widget.
 *
 * Internally, many widgets like GtkListBox will operate in a very similar way,
 * connecting the `items-changed` signal to create and destroy widgets at the
 * correct position.
 */
const listWidget = [];


const listStore = Gio.ListStore.new(Gio.File);

listStore.connect('items-changed', (list, position, removed, added) => {
    console.log(`position: ${position}, removed: ${removed}, added: ${added}`);

    /* Items are added and removed from the same position, so the removals
     * must be handled first.
     *
     * NOTE: remember that the items have already changed in the model when this
     *       signal is emitted, so you can not query removed objects.
     */
    while (removed--)
        listWidget.splice(position, 1);

    /* Once the removals have been processed, the additions must be inserted
     * at the same position.
     */
    for (let i = 0; i < added; i++)
        listWidget.splice(position + i, 0, list.get_item(position + i));
});


/* Splicing the items will result in a single emission of `items-changed`, with
 * a callback signature of `position = 0, removed = 0, added = 3`.
 *
 * Sorting the items will result in a single emission of `items-changed`, with
 * a callback signature of `position = 0, removed = 3, added = 3`.
 */
listStore.splice(0, 0, [
    Gio.File.new_for_path('/'),
    Gio.File.new_for_path('/home'),
    Gio.File.new_for_path('/home/user'),
]);

listStore.sort((object1, object2) => {
    return object1.get_path().localeCompare(object2.get_path());
});


/* Inserting one at a time results in a three emissions of `items-changed`, with
 * a callback signature of `position = ?, removed = 0, added = 1`.
 *
 * WARNING: when using a sorted list model all items must be sorted, with the
 *          same sorting function, or the list behavior becomes undefined.
 */
const moreItems = [
    Gio.File.new_for_path('/home/user/Downloads'),
    Gio.File.new_for_path('/home/user/Downloads/TV'),
    Gio.File.new_for_path('/home/user/Downloads/TV/Teddy Ruxpin'),
];

for (const item of moreItems) {
    listStore.insert_sorted(item, (object1, object2) => {
        return object1.get_path().localeCompare(object2.get_path());
    });
}


/* We should now be in state where the number and order of items is the same,
 * both in the list model and the list consumer.
 */
if (listStore.n_items !== listWidget.length)
    throw Error('Should never be thrown');

for (let i = 0; i < listStore.n_items; i++) {
    if (listWidget[i] !== listStore.get_item(i))
        throw Error('Should never be thrown');
}

MIT Licensed | GJS, A GNOME Project