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:

   1 const PopupMenu = imports.ui.popupMenu;
   2 
   3 var MyMenu = class MyMenu extends PopupMenu.PopupMenu {
   4     constructor(...args) {
   5         super(args);
   6     }
   7 };

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.

   1 const GObject = imports.gi.GObject;
   2 const PopupMenu = imports.ui.popupMenu;
   3 
   4 var MyMenu = GObject.registerClass({
   5     GTypeName: 'NamespaceMyMenu'
   6 }, class MyMenu extends PopupMenu.PopupMenu {
   7     _init(...args) {
   8         super._init(args);
   9     }
  10 });

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

Projects/GnomeShell/Extensions/MigratingShellClasses (last edited 2020-03-06 06:07:13 by AndyHolmes)