Migrating Shell Classes
In the past, limitations in GJS prevented proper GObject inheritance and either Lang.Class or a delegate pattern was used where a GObject was a property of a native JavaScript class. As GJS has improved, many classes have been refactored as proper GObject classes, which changes the way they are subclassed.
This is a short guide to migrating GNOME Shell objects from native JavaScript classes to GObject classes.
Native vs GObject
The primary differences to notice here are:
Using the GObject.registerClass() function
The native constructor() function vs _init()
The Signals module used to add GObject-like signals to a native class.
Native Classes
Native JavaScript classes in GNOME Shell generally look something like below.
1 const Signals = imports.signals;
2
3
4 var MyNativeClass = class MyNativeClass {
5
6 constructor(someConstructValue = null) {
7 this._string_prop = someConstructValue;
8 }
9
10 get string_prop() {
11 return this._string_prop;
12 }
13
14 set string_prop(value) {
15 this._string_prop = value;
16 }
17
18 someFunction() {
19 log(`string-prop: ${this.string_prop}`);
20 }
21
22 emitSomeSignal(arg1, arg2) {
23 // Like native properties, there is no need to pre-declare anything
24 // about signals when using the Signals module. They are simply
25 // connected, disconnected and emitted.
26 this.emit('some-signal', arg1, arg2);
27 }
28 };
29 Signals.addSignalMethods(MyNativeClass.prototype);
30
31
32 // Instantiate with the new keyword
33 let obj = new MyNativeClass('some string');
34
35 // The native signals module operates somewhat differently than GObject
36 let sigId = obj.connect('some-signal', (sourceObj, arg1, arg2) => {
37 // Here `sourceObj` is the same instance as `obj`, just as with true GObject
38 // signals.
39 //
40 // Unlike GObject signals, `this` will always be the `global` object even if
41 // you use `Function.prototype.bind()`
42
43 // Disconnecting signals
44 sourceObj.disconnect(sigId);
45 });
GObject Classes
Classes that derive from GObject.Object are a little more verbose, but also more powerful. Below is an example of how the same class might be rewritten as a GObject class.
Note that this example is meant to demonstrate how GObject classes are declared, not to represent a typical class in GNOME Shell.
1 const GObject = imports.gi.GObject;
2
3
4 var MyGObjectClass = GObject.registerClass({
5 // If the GTypeName is omitted then GJS will generate a unique type name
6 // for you. Otherwise you should ensure to avoid any possible collisions
7 // with other types (eg. GtkLabel, StLabel, ExtensionNameLabel).
8 GTypeName: 'MyGObjectClass',
9 Properties: {
10 // GParamSpec objects describe the property
11 'string-prop': GObject.ParamSpec.string(
12 // The accessor name should be the same as the key above. GJS will
13 // automatically adjust these so you can access properties as
14 // `obj.string_prop`, however you should still use `string-prop`
15 // anywhere a String is taken.
16 'string-prop',
17
18 // The property name and description don't serve much purpose in GJS
19 // but can still provide some inline documentation.
20 'String Property',
21 'A property holding a string',
22
23 // Flags can control whether a string is readable, writable or
24 // able to be changed after construction. These are documented in
25 // the GObject API documentation.
26 GObject.ParamFlags.READWRITE,
27
28 // Usually this is the default value, however you must implement
29 // this manually in the getter (see below).
30 null
31 ),
32 },
33 Signals: {
34 // Signal specs are simply Objects in GJS. Any of these properties can
35 // be left out if not needed.
36 'some-signal': {
37 // Flags can affect the order in which signal handlers are invoked.
38 // This are documented in the GObject API documentation.
39 flags: GObject.SignalFlags.RUN_FIRST,
40
41 // An array of argument types. Be aware that GObject.TYPE_OBJECT is
42 // a GObject, not a JavaScript Object (eg. `{}`)
43 param_types: [GObject.TYPE_INT, GObject.TYPE_STRING],
44
45 // In some rare cases, signals have return values which should be
46 // return by the callback. This is almost always a boolean used to
47 // stop other callbacks from being invoked.
48 return_type: GObject.TYPE_NONE,
49
50 // Accumulators are very rare. See the GObject API documentation if
51 // you need to use these.
52 accumulator: GObject.AccumulatorType.NONE
53 },
54 }
55 }, class MyGObjectClass extends GObject.Object {
56
57 // In GJS the native `constructor()` function is used to bootstrap the
58 // GObject, so you must override `_init()` instead.
59 _init(params = {}) {
60 // Depending on the parent class, GObjects may take a dictionary of
61 // construct properties.
62 super._init(params);
63 }
64
65 get string_prop() {
66 // Implementing a default value
67 if (this._string_prop === undefined)
68 this._string_prop = null;
69
70 return this._string_prop;
71 }
72
73 set string_prop(value) {
74 if (typeof value !== 'string' && value !== null)
75 throw ValueError(`Expected "string" got "${typeof value}"`);
76
77 // Checking if the value has actually changed to avoid a signal emission
78 if (this.string_prop !== value) {
79 this._string_prop = value;
80
81 // GObjects usually emit GObject::notify when properties change,
82 // although there are exceptions.
83 this.notify('string-prop');
84 }
85 }
86
87 someFunction() {
88 log(`string-prop: ${this.string_prop}`);
89 }
90
91 emitSomeSignal(arg1, arg2) {
92 if (typeof arg1 === 'number' && typeof arg2 === 'string')
93 this.emit('some-signal', arg1, arg2);
94 }
95 });
96
97
98 // In our example class, the dictionary of values will be passed to the setter
99 // for each property
100 let gobj = new MyGObjectClass({
101 'some-prop': 'some string'
102 });
103
104 // Signals
105 let gSigId = gobj.connect('some-signal', (sourceObj, arg1, arg2) => {
106 log(`some-signal emitted: ${arg1}. ${arg2}`);
107
108 // Note that unlike the Signals module, GObject callbacks will respect the
109 // `this` scope if connected with `Function.prototype.bind()`.
110
111 // Always remember to disconnect your signals
112 sourceObj.disconnect(gSigId);
113 });
114
115 // Watching for propery changes is just another signal to connect to. The
116 // property name is the "detail" (eg. `::some-prop`).
117 let gPropId = gobj.connect('notify::some-prop', (sourceObj, pspec) => {
118 log(`${pspec.name} changed to "${sourceObj.some_prop}"`);
119
120 sourceObj.disconnect(gPropId);
121 });
Migrating from Native to GObject
As GJS has improved, many public classes in GNOME Shell have been refactored as GObject classes. Previously you may have subclassed a native class like below:
If the class was refactored to be a GObject class, you need to adjust your declaration accordingly.
In most cases this just means wrapping the same class in GObject.registerClass() and adjusting the constructor function to use _init(). You may also need to take note of changes to signals (eg. something-changed became notify::something) or other GObject-specific behaviour.
In the best case scenario, you may be able to support multiple versions of GNOME Shell by dynamically wrapping your classes, depending on the version detected at runtime:
1 const GObject = imports.gi.GObject;
2
3 const Config = imports.misc.config;
4 const PanelMenu = imports.ui.panelMenu;
5
6 // Outputs a string like : "3.30.0"
7 log(Config.PACKAGE_VERSION);
8
9 // Getting the minor version as an integer (probably the most relevant)
10 let shellMinorVersion = parseInt(Config.PACKAGE_VERSION.split('.')[1]);
11
12 // A PanelMenu.Button subclass
13 var ExampleIndicator = class ExampleIndicator extends PanelMenu.Button {
14 _init(constructor_args) {
15 // Chaining up to the super-class
16 super._init(...constructor_args);
17 }
18 }
19
20 // Re-wrapping our subclass in `GObject.registerClass()`
21 if (shellMinorVersion > 30) {
22 ExampleIndicator = GObject.registerClass(
23 {GTypeName: 'ExampleIndicator'},
24 ExampleIndicator
25 );
26 }
Getting Help
StackOverflow using the gnome-shell-extensions and/or gjs tags
- #gnome-shell on irc.gnome.org
- #javascript on irc.gnome.org
Ask on discourse.gnome.org