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:
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.
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
:
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
:
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
:
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:
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.
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:
GObject.Object
andGio.ListModel
by inheriting fromGio.ListStore
SimpleInterface
by implementing its methods and propertiesComplexInterface
by implementing its methods and properties
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:
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');