Skip to content

GObject Interfaces

In plain JavaScript interfaces are usually informal and simply fulfilled by the presence of certain methods and properties on an object. However, you may recognize interfaces from TypeScript, where an object's type includes information about its capabilities. GObject interfaces are a way of ensuring that objects passed to C code have the right capabilities.

For example, the Gio.Icon interface is implemented by Gio.FileIcon for file-based icons and Gio.ThemedIcon for themed icons. Instances of these classes, or a custom JavaScript implementation, can be passed to Gtk.Image.

Implementing Interfaces

Implementing an interface involves providing working implementations for class methods and properties defined by the interface.

Methods

Interfaces that require methods to be implemented must have the corresponding virtual function defined in the class. For example, the virtual function for the Gio.ListModel method get_item() is vfunc_get_item(). When a caller invokes Gio.ListModel.get_item() on an object it will defer to the virtual function of the implementation.

This is different from overriding a function in native JavaScript classes and interfaces, where the method should be overridden using the original member name.

Below is an example implementation of the Gio.ListModel interface which only requires implementing three methods:

js
const ArrayStore = GObject.registerClass({
    Implements: [Gio.ListModel],
}, class ArrayStore extends GObject.Object {
    #items = [];

    vfunc_get_item(position) {
        return this.#items[position] || null;
    }

    vfunc_get_item_type() {
        return GObject.Object;
    }

    vfunc_get_n_items() {
        return this.#items.length;
    }

    /**
     * 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);
    }
});

Properties

Interfaces that require properties to be implemented must have the GParamSpec overridden in the class registration, as well as the JavaScript getter and/or setter implemented.

Below is an example of implementing the GtkOrientable interface from GTK, which only requires implementing one property. The orientation property is a read-write property, so we implement both get and set functions and register it in the properties dictionary.

js
const OrientableObject = GObject.registerClass({
    Implements: [Gtk.Orientable],
    Properties: {
        'orientation': GObject.ParamSpec.override('orientation',
            Gtk.Orientable),
    },
}, class OrientableObject extends GObject.Object {
    get orientation() {
        if (this._orientation === undefined)
            this._orientation = Gtk.Orientation.HORIZONTAL;

        return this._orientation;
    }

    set orientation(value) {
        if (this.orientation === value)
            return;

        this._orientation = value;
        this.notify('orientation');
    }
});

Multiple Interfaces

It is also possible for a class to implement multiple interfaces. The example below is an incomplete example of a container widget implementing both Gtk.Orientable and Gio.ListModel:

js
const OrientableWidget = GObject.registerClass({
    Implements: [Gio.ListModel, Gtk.Orientable],
    Properties: {
        'orientation': GObject.ParamSpec.override('orientation',
            Gtk.Orientable),
    },
}, class OrientableWidget extends Gtk.Widget {
    constructor(params = {}) {
        super(params);

        this._children = [];
    }

    get orientation() {
        if (this._orientation === undefined)
            this._orientation = Gtk.Orientation.HORIZONTAL;

        return this._orientation;
    }

    set orientation(value) {
        if (this.orientation === value)
            return;

        this._orientation = value;
        this.notify('orientation');
    }

    vfunc_get_item_type() {
        return Gtk.Widget;
    }

    vfunc_get_item(position) {
        return this._children[position] || null;
    }

    vfunc_get_n_items() {
        return this._children.length;
    }
});

Defining Interfaces

TIP

GObject Interfaces exist to implement type safe multiple-inheritance in the C programming language, while JavaScript code should usually just use mix-ins.

Interfaces are defined in GJS by inheriting from GObject.Interface and providing the class definition property Requires. This field must include a base type that is GObject.Object or a subclass of GObject.Object.

The Requires field may also contain multiple other interfaces that are either implemented by the base type, or that the implementation is expected to. For example, Requires: [GObject.Object, Gio.Action] indicates that an implementation must provide methods, properties and emit signals from the Gio.Action interface, or be derived from a base type that does.

Defining Methods

Methods defined on an interface must be implemented, if the method throws the special error GObject.NotImplementedError(). Methods that do not throw this error are optional to implement.

Note that unlike GObject Interfaces defined by a C library, methods are overridden directly rather than by virtual function. For example, instead of overriding vfunc_requiredMethod(), you should override requiredMethod().

Defining Properties

Properties defined on an interface must always be implemented, using GObject.ParamSpec.override() in the Properties class definition property. The implementation should also provide get and set methods for the property, as indicated by the GObject Property Flags.

Defining Signals

Signals defined on an interface do not need to be implemented. Typically interface definitions will provide emitter methods, such as with Gio.ListModel.items_changed(), otherwise they can be emitted by calling GObject.Object.prototype.emit() on an instance of the implementation.

A Simple Interface

Below is a simple example of defining an interface that only requires GObject.Object:

js
const SimpleInterface = GObject.registerClass({
    GTypeName: 'SimpleInterface',
    Requires: [GObject.Object],
    Properties: {
        'simple-property': GObject.ParamSpec.boolean(
            'simple-property',
            'Simple property',
            'A property that must be implemented',
            GObject.ParamFlags.READABLE,
            true
        ),
    },
    Signals: {
        'simple-signal': {},
    },
}, class SimpleInterface extends GObject.Interface {
    /**
     * By convention interfaces provide methods for emitting their signals, but
     * you can always call `emit()` on the instance of an implementation.
     */
    emitSimple() {
        this.emit('simple-signal');
    }

    /**
     * Interfaces can define methods that MAY be implemented, by providing a
     * default implementation.
     */
    optionalMethod() {
        return true;
    }

    /**
     * Interfaces can define methods that MUST be implemented, by throwing the
     * special error `GObject.NotImplementedError()`.
     */
    requiredMethod() {
        throw new GObject.NotImplementedError();
    }
});

Note that unlike with interfaces defined by C libraries, we override methods like requiredMethod() directly, not vfunc_requiredMethod(). Below is a minimal implementation of SimpleInterface:

js
const SimpleImplementation = GObject.registerClass({
    Implements: [SimpleInterface],
    Properties: {
        'simple-property': GObject.ParamSpec.override('simple-property',
            SimpleInterface),
    },
}, class SimpleImplementation extends GObject.Object {
    get simple_property() {
        return true;
    }

    requiredMethod() {
        console.log('requiredMethod() implemented');
    }
});

Instances of the implementation can then be constructed like any class. The instanceof operator can be used to confirm the base class (i.e. GObject) and any interfaces it implements:

js
const simpleInstance = new SimpleImplementation();

if (simpleInstance instanceof GObject.Object)
    console.log('An instance of a GObject');

if (simpleInstance instanceof SimpleInterface)
    console.log('An instance implementing SimpleInterface');

if (!(simpleInstance instanceof Gio.ListModel))
    console.log('Not an implementation of a list model');

A Complex Interface

More complex interfaces can also be defined that depend on other interfaces, including those defined in GJS. ComplexInterface depends on Gio.ListModel and SimpleInterface, while adding a property and a method.

js
const ComplexInterface = GObject.registerClass({
    GTypeName: 'ComplexInterface',
    Requires: [Gio.ListModel, SimpleInterface],
    Properties: {
        'complex-property': GObject.ParamSpec.boolean(
            'complex-property',
            'Complex property',
            'A property that must be implemented',
            GObject.ParamFlags.READABLE,
            true
        ),
    },
}, class ComplexInterface extends GObject.Interface {
    complexMethod() {
        throw new GObject.NotImplementedError();
    }
});

An implementation of this interface must then meet the requirements of Gio.ListModel and SimpleInterface, which both require GObject.Object. The following implementation of ComplexInterface will meet the requirements of:

js
const ComplexImplementation = GObject.registerClass({
    Implements: [Gio.ListModel, SimpleInterface, ComplexInterface],
    Properties: {
        'complex-property': GObject.ParamSpec.override('complex-property',
            ComplexInterface),
        'simple-property': GObject.ParamSpec.override('simple-property',
            SimpleInterface),
    },
}, class ComplexImplementation extends Gio.ListStore {
    get complex_property() {
        return false;
    }

    get simple_property() {
        return true;
    }

    complexMethod() {
        console.log('complexMethod() implemented');
    }

    requiredMethod() {
        console.log('requiredMethod() implemented');
    }
});

By using instanceof, we can confirm both the inheritance and interface support of the implementation:

js
let complexInstance = new ComplexImplementation();

if (complexInstance instanceof GObject.Object &&
    complexInstance instanceof Gio.ListStore)
    console.log('An instance with chained inheritance');

if (complexInstance instanceof Gio.ListModel &&
    complexInstance instanceof SimpleInterface &&
    complexInstance instanceof ComplexInterface)
    console.log('An instance implementing three interfaces');

MIT Licensed | GJS, A GNOME Project