[gjs] GObject: Adapt GObject class framework to ES6



commit 301700808b462b0d366c1fd54aa1f8b2d03ca722
Author: Philip Chimento <philip endlessm com>
Date:   Thu Aug 3 02:20:32 2017 +0100

    GObject: Adapt GObject class framework to ES6
    
    This moves our GObject class framework to use ES6 classes. GObject.Object
    and GObject.Interface gain static _classInit() methods which register
    GTypes and perform other such magic that used to be performed in the
    metaclasses. When defining the class you must call
    GObject.registerClass() on the class object, with an optional metadata
    object as the first argument. (The metadata object works exactly like the
    meta properties from Lang.Class, except that Name and Extends are not
    present.)
    
    Old:
    
        var MyClass = new Lang.Class({
            Name: 'MyClass',
            Extends: GObject.Object,
            Signals: { 'event': {} },
            _init(props={}) {
                this._private = [];
                this.parent(props);
            },
        });
    
    New:
    
        var MyClass = GObject.registerClass({
            Signals: { 'event': {} },
        }, class MyClass extends GObject.Object {
            _init(props={}) {
                this._private = [];
                super._init(props);
            }
        });
    
    It is forward compatible with the following syntax requiring decorators
    and class fields, which are not in the JS standard yet:
    
        @GObject.registerClass
        class MyClass extends GObject.Object {
            static [GObject.signals] = { 'event': {} }
            _init(props={}) {
                this._private = [];
                super._init(props);
            }
        }
    
    We also now add a [Symbol.hasInstance]() method to interfaces so that
    instanceof will finally work for interfaces.
    
    One limitation is that GObject ES6 classes can't have constructor()
    methods, they must do any setup in an _init() method.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=785652

 Makefile-test.am                           |    2 +
 gi/interface.cpp                           |   36 +++-
 installed-tests/js/testGObjectClass.js     |  337 ++++++++++++++++++++++++++++
 installed-tests/js/testGObjectInterface.js |  266 ++++++++++++++++++++++
 modules/overrides/GObject.js               |  238 ++++++++++++++++++++
 5 files changed, 878 insertions(+), 1 deletions(-)
---
diff --git a/Makefile-test.am b/Makefile-test.am
index a679e03..9cbdbe8 100644
--- a/Makefile-test.am
+++ b/Makefile-test.am
@@ -226,6 +226,8 @@ common_jstests_files =                                              \
        installed-tests/js/testGettext.js                       \
        installed-tests/js/testGIMarshalling.js                 \
        installed-tests/js/testGLib.js                          \
+       installed-tests/js/testGObjectClass.js                  \
+       installed-tests/js/testGObjectInterface.js              \
        installed-tests/js/testGTypeClass.js                    \
        installed-tests/js/testGio.js                           \
        installed-tests/js/testImporter.js                      \
diff --git a/gi/interface.cpp b/gi/interface.cpp
index 361f5bc..44ef12b 100644
--- a/gi/interface.cpp
+++ b/gi/interface.cpp
@@ -27,6 +27,7 @@
 #include "function.h"
 #include "gtype.h"
 #include "interface.h"
+#include "object.h"
 #include "repo.h"
 #include "gjs/jsapi-class.h"
 #include "gjs/jsapi-wrapper.h"
@@ -157,6 +158,34 @@ interface_resolve(JSContext       *context,
     return true;
 }
 
+static bool
+interface_has_instance_func(JSContext *cx,
+                            unsigned   argc,
+                            JS::Value *vp)
+{
+    JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+    /* This method is not called directly, so no need for error messages */
+
+    JS::RootedValue interface(cx, args.computeThis(cx));
+    g_assert(interface.isObject());
+    JS::RootedObject interface_constructor(cx, &interface.toObject());
+    JS::RootedObject interface_proto(cx);
+    gjs_object_require_property(cx, interface_constructor,
+                                "interface constructor",
+                                GJS_STRING_PROTOTYPE, &interface_proto);
+
+    Interface *priv;
+    if (!priv_from_js_with_typecheck(cx, interface_proto, &priv))
+        return false;
+
+    g_assert(args.length() == 1);
+    g_assert(args[0].isObject());
+    JS::RootedObject instance(cx, &args[0].toObject());
+    bool isinstance = gjs_typecheck_object(cx, instance, priv->gtype, false);
+    args.rval().setBoolean(isinstance);
+    return true;
+}
+
 static const struct JSClassOps gjs_interface_class_ops = {
     NULL,  /* addProperty */
     NULL,  /* deleteProperty */
@@ -182,6 +211,11 @@ JSFunctionSpec gjs_interface_proto_funcs[] = {
     JS_FS_END
 };
 
+JSFunctionSpec gjs_interface_static_funcs[] = {
+    JS_SYM_FN(hasInstance, interface_has_instance_func, 1, 0),
+    JS_FS_END
+};
+
 bool
 gjs_define_interface_class(JSContext              *context,
                            JS::HandleObject        in_object,
@@ -208,7 +242,7 @@ gjs_define_interface_class(JSContext              *context,
                                 /* props of constructor, MyConstructor.myprop */
                                 NULL,
                                 /* funcs of constructor, MyConstructor.myfunc() */
-                                NULL,
+                                gjs_interface_static_funcs,
                                 &prototype,
                                 constructor)) {
         g_error("Can't init class %s", constructor_name);
diff --git a/installed-tests/js/testGObjectClass.js b/installed-tests/js/testGObjectClass.js
new file mode 100644
index 0000000..44f2bd3
--- /dev/null
+++ b/installed-tests/js/testGObjectClass.js
@@ -0,0 +1,337 @@
+// -*- mode: js; indent-tabs-mode: nil -*-
+imports.gi.versions.Gtk = '3.0';
+
+const Gio = imports.gi.Gio;
+const GObject = imports.gi.GObject;
+const Gtk = imports.gi.Gtk;
+
+const MyObject = GObject.registerClass({
+    Properties: {
+        'readwrite': GObject.ParamSpec.string('readwrite', 'ParamReadwrite',
+            'A read write parameter', GObject.ParamFlags.READWRITE, ''),
+        'readonly': GObject.ParamSpec.string('readonly', 'ParamReadonly',
+            'A readonly parameter', GObject.ParamFlags.READABLE, ''),
+        'construct': GObject.ParamSpec.string('construct', 'ParamConstructOnly',
+            'A readwrite construct-only parameter',
+            GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
+            'default'),
+    },
+    Signals: {
+        'empty': {},
+        'minimal': { param_types: [ GObject.TYPE_INT, GObject.TYPE_INT ] },
+        'full': {
+            flags: GObject.SignalFlags.RUN_LAST,
+            accumulator: GObject.AccumulatorType.FIRST_WINS,
+            return_type: GObject.TYPE_INT,
+            param_types: [],
+        },
+        'run-last': { flags: GObject.SignalFlags.RUN_LAST },
+        'detailed': {
+            flags: GObject.SignalFlags.RUN_FIRST | GObject.SignalFlags.DETAILED,
+            param_types: [ GObject.TYPE_STRING ],
+        },
+    },
+}, class MyObject extends GObject.Object {
+    get readwrite() {
+        if (typeof this._readwrite === 'undefined')
+            return 'foo';
+        return this._readwrite;
+    }
+
+    set readwrite(val) {
+        if (val == 'ignore')
+            return;
+
+        this._readwrite = val;
+    }
+
+    get readonly() {
+        if (typeof this._readonly === 'undefined')
+            return 'bar';
+        return this._readonly;
+    }
+
+    set readonly(val) {
+        // this should never be called
+        void val;
+        this._readonly = 'bogus';
+    }
+
+    get construct() {
+        if (typeof this._constructProp === 'undefined')
+            return null;
+        return this._constructProp;
+    }
+
+    set construct(val) {
+        // this should be called at most once
+        if (this._constructCalled)
+            throw Error('Construct-Only property set more than once');
+
+        this._constructProp = val;
+        this._constructCalled = true;
+    }
+
+    notify_prop() {
+        this._readonly = 'changed';
+
+        this.notify('readonly');
+    }
+
+    emit_empty() {
+        this.emit('empty');
+    }
+
+    emit_minimal(one, two) {
+        this.emit('minimal', one, two);
+    }
+
+    emit_full() {
+        return this.emit('full');
+    }
+
+    emit_detailed() {
+        this.emit('detailed::one');
+        this.emit('detailed::two');
+    }
+
+    emit_run_last(callback) {
+        this._run_last_callback = callback;
+        this.emit('run-last');
+    }
+
+    on_run_last() {
+        this._run_last_callback();
+    }
+
+    on_empty() {
+        this.empty_called = true;
+    }
+
+    on_full() {
+        this.full_default_handler_called = true;
+        return 79;
+    }
+});
+
+const MyApplication = GObject.registerClass({
+    Signals: { 'custom': { param_types: [ GObject.TYPE_INT ] } },
+}, class MyApplication extends Gio.Application {
+    emit_custom(n) {
+        this.emit('custom', n);
+    }
+});
+
+const MyInitable = GObject.registerClass({
+    Implements: [ Gio.Initable ],
+}, class MyInitable extends GObject.Object {
+    vfunc_init(cancellable) {
+        if (!(cancellable instanceof Gio.Cancellable))
+            throw 'Bad argument';
+
+        this.inited = true;
+    }
+});
+
+const Derived = GObject.registerClass(class Derived extends MyObject {
+    _init() {
+        super._init({ readwrite: 'yes' });
+    }
+});
+
+const MyCustomInit = GObject.registerClass(class MyCustomInit extends GObject.Object {
+    _instance_init() {
+        this.foo = true;
+    }
+});
+
+describe('GObject class with decorator', function () {
+    let myInstance;
+    beforeEach(function () {
+        myInstance = new MyObject();
+    });
+
+    it('throws an error when not used with a GObject-derived class', function () {
+        class Foo {}
+        expect (() => GObject.registerClass(class Bar extends Foo {})).toThrow();
+    });
+
+    it('constructs with default values for properties', function () {
+        expect(myInstance.readwrite).toEqual('foo');
+        expect(myInstance.readonly).toEqual('bar');
+        expect(myInstance.construct).toEqual('default');
+    });
+
+    it('constructs with a hash of property values', function () {
+        let myInstance2 = new MyObject({ readwrite: 'baz', construct: 'asdf' });
+        expect(myInstance2.readwrite).toEqual('baz');
+        expect(myInstance2.readonly).toEqual('bar');
+        expect(myInstance2.construct).toEqual('asdf');
+    });
+
+    const ui = `<interface>
+                  <object class="Gjs_MyObject" id="MyObject">
+                    <property name="readwrite">baz</property>
+                    <property name="construct">quz</property>
+                  </object>
+                </interface>`;
+
+    it('constructs with property values from Gtk.Builder', function () {
+        let builder = Gtk.Builder.new_from_string(ui, -1);
+        let myInstance3 = builder.get_object('MyObject');
+        expect(myInstance3.readwrite).toEqual('baz');
+        expect(myInstance3.readonly).toEqual('bar');
+        expect(myInstance3.construct).toEqual('quz');
+    });
+
+    it('has a name', function () {
+        expect(MyObject.name).toEqual('MyObject');
+    });
+
+    // the following would (should) cause a CRITICAL:
+    // myInstance.readonly = 'val';
+    // myInstance.construct = 'val';
+
+    it('has a notify signal', function () {
+        let notifySpy = jasmine.createSpy('notifySpy');
+        myInstance.connect('notify::readonly', notifySpy);
+
+        myInstance.notify_prop();
+        myInstance.notify_prop();
+
+        expect(notifySpy).toHaveBeenCalledTimes(2);
+    });
+
+    it('can define its own signals', function () {
+        let emptySpy = jasmine.createSpy('emptySpy');
+        myInstance.connect('empty', emptySpy);
+        myInstance.emit_empty();
+
+        expect(emptySpy).toHaveBeenCalled();
+        expect(myInstance.empty_called).toBeTruthy();
+    });
+
+    it('passes emitted arguments to signal handlers', function () {
+        let minimalSpy = jasmine.createSpy('minimalSpy');
+        myInstance.connect('minimal', minimalSpy);
+        myInstance.emit_minimal(7, 5);
+
+        expect(minimalSpy).toHaveBeenCalledWith(myInstance, 7, 5);
+    });
+
+    it('can return values from signals', function () {
+        let fullSpy = jasmine.createSpy('fullSpy').and.returnValue(42);
+        myInstance.connect('full', fullSpy);
+        let result = myInstance.emit_full();
+
+        expect(fullSpy).toHaveBeenCalled();
+        expect(result).toEqual(42);
+    });
+
+    it('does not call first-wins signal handlers after one returns a value', function () {
+        let neverCalledSpy = jasmine.createSpy('neverCalledSpy');
+        myInstance.connect('full', () => 42);
+        myInstance.connect('full', neverCalledSpy);
+        myInstance.emit_full();
+
+        expect(neverCalledSpy).not.toHaveBeenCalled();
+        expect(myInstance.full_default_handler_called).toBeFalsy();
+    });
+
+    it('gets the return value of the default handler', function () {
+        let result = myInstance.emit_full();
+
+        expect(myInstance.full_default_handler_called).toBeTruthy();
+        expect(result).toEqual(79);
+    });
+
+    it('calls run-last default handler last', function () {
+        let stack = [ ];
+        let runLastSpy = jasmine.createSpy('runLastSpy')
+            .and.callFake(() => { stack.push(1); });
+        myInstance.connect('run-last', runLastSpy);
+        myInstance.emit_run_last(() => { stack.push(2); });
+
+        expect(stack).toEqual([1, 2]);
+    });
+
+    it("can inherit from something that's not GObject.Object", function () {
+        // ...and still get all the goodies of GObject.Class
+        let instance = new MyApplication({ application_id: 'org.gjs.Application' });
+        let customSpy = jasmine.createSpy('customSpy');
+        instance.connect('custom', customSpy);
+
+        instance.emit_custom(73);
+        expect(customSpy).toHaveBeenCalledWith(instance, 73);
+    });
+
+    it('can implement an interface', function () {
+        let instance = new MyInitable();
+        expect(instance instanceof Gio.Initable).toBeTruthy();
+        expect(instance instanceof Gio.AsyncInitable).toBeFalsy();
+
+        // Old syntax, backwards compatible
+        expect(instance.constructor.implements(Gio.Initable)).toBeTruthy();
+        expect(instance.constructor.implements(Gio.AsyncInitable)).toBeFalsy();
+    });
+
+    it('can implement interface vfuncs', function () {
+        let instance = new MyInitable();
+        expect(instance.inited).toBeFalsy();
+
+        instance.init(new Gio.Cancellable());
+        expect(instance.inited).toBeTruthy();
+    });
+
+    it('can be a subclass', function () {
+        let derived = new Derived();
+
+        expect(derived instanceof Derived).toBeTruthy();
+        expect(derived instanceof MyObject).toBeTruthy();
+
+        expect(derived.readwrite).toEqual('yes');
+    });
+
+    it('calls its _instance_init() function while chaining up in constructor', function () {
+        let instance = new MyCustomInit();
+        expect(instance.foo).toBeTruthy();
+    });
+
+    it('can have an interface-valued property', function () {
+        const InterfacePropObject = GObject.registerClass({
+            Properties: {
+                'file': GObject.ParamSpec.object('file', 'File', 'File',
+                    GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
+                    Gio.File.$gtype)
+            },
+        }, class InterfacePropObject extends GObject.Object {});
+        let file = Gio.File.new_for_path('dummy');
+        expect(() => new InterfacePropObject({ file: file })).not.toThrow();
+    });
+
+    it('can override a property from the parent class', function () {
+        const OverrideObject = GObject.registerClass({
+            Properties: {
+                'readwrite': GObject.ParamSpec.override('readwrite', MyObject),
+            },
+        }, class OverrideObject extends MyObject {
+            get readwrite() {
+                return this._subclass_readwrite;
+            }
+            set readwrite(val) {
+                this._subclass_readwrite = 'subclass' + val;
+            }
+        });
+        let obj = new OverrideObject();
+        obj.readwrite = 'foo';
+        expect(obj.readwrite).toEqual('subclassfoo');
+    });
+
+    it('cannot override a non-existent property', function () {
+        expect(() => GObject.registerClass({
+            Properties: {
+                'nonexistent': GObject.ParamSpec.override('nonexistent', GObject.Object),
+            },
+        }, class BadOverride extends GObject.Object {})).toThrow();
+    });
+});
diff --git a/installed-tests/js/testGObjectInterface.js b/installed-tests/js/testGObjectInterface.js
new file mode 100644
index 0000000..9eab974
--- /dev/null
+++ b/installed-tests/js/testGObjectInterface.js
@@ -0,0 +1,266 @@
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
+const Mainloop = imports.mainloop;
+
+const AGObjectInterface = GObject.registerClass({
+    GTypeName: 'ArbitraryGTypeName',
+    Requires: [ GObject.Object ],
+    Properties: {
+        'interface-prop': GObject.ParamSpec.string('interface-prop',
+            'Interface property', 'Must be overridden in implementation',
+            GObject.ParamFlags.READABLE,
+            'foobar')
+    },
+    Signals: {
+        'interface-signal': {}
+    },
+}, class AGObjectInterface extends GObject.Interface {
+    requiredG() {
+        throw new GObject.NotImplementedError();
+    }
+
+    optionalG() {
+        return 'AGObjectInterface.optionalG()';
+    }
+});
+
+const InterfaceRequiringGObjectInterface = GObject.registerClass({
+    Requires: [ AGObjectInterface ],
+}, class InterfaceRequiringGObjectInterface extends GObject.Interface {
+    optionalG() {
+        return 'InterfaceRequiringGObjectInterface.optionalG()\n' +
+            AGObjectInterface.optionalG(this);
+    }
+});
+
+const GObjectImplementingGObjectInterface = GObject.registerClass({
+    Implements: [ AGObjectInterface ],
+    Properties: {
+        'interface-prop': GObject.ParamSpec.override('interface-prop',
+            AGObjectInterface),
+        'class-prop': GObject.ParamSpec.string('class-prop', 'Class property',
+            'A property that is not on the interface',
+            GObject.ParamFlags.READABLE, 'meh')
+    },
+    Signals: {
+        'class-signal': {},
+    },
+}, class GObjectImplementingGObjectInterface extends GObject.Object {
+    get interface_prop() {
+        return 'foobar';
+    }
+
+    get class_prop() {
+        return 'meh';
+    }
+
+    requiredG() {}
+    optionalG() {
+        return AGObjectInterface.optionalG(this);
+    }
+});
+
+const MinimalImplementationOfAGObjectInterface = GObject.registerClass({
+    Implements: [ AGObjectInterface ],
+    Properties: {
+        'interface-prop': GObject.ParamSpec.override('interface-prop',
+            AGObjectInterface)
+    },
+}, class MinimalImplementationOfAGObjectInterface extends GObject.Object {
+    requiredG() {}
+});
+
+const ImplementationOfTwoInterfaces = GObject.registerClass({
+    Implements: [ AGObjectInterface, InterfaceRequiringGObjectInterface ],
+    Properties: {
+        'interface-prop': GObject.ParamSpec.override('interface-prop',
+            AGObjectInterface)
+    },
+}, class ImplementationOfTwoInterfaces extends GObject.Object {
+    requiredG() {}
+    optionalG() {
+        return InterfaceRequiringGObjectInterface.optionalG(this);
+    }
+});
+
+describe('GObject interface', function () {
+    it('cannot be instantiated', function () {
+        expect(() => new AGObjectInterface()).toThrow();
+    });
+
+    it('has a name', function () {
+        expect(AGObjectInterface.name).toEqual('AGObjectInterface');
+    });
+
+    it('reports its type name', function () {
+        expect(AGObjectInterface.$gtype.name).toEqual('ArbitraryGTypeName');
+    });
+
+    it('can be implemented by a GObject class', function () {
+        let obj;
+        expect(() => { obj = new GObjectImplementingGObjectInterface(); })
+            .not.toThrow();
+        expect(obj instanceof AGObjectInterface).toBeTruthy();
+    });
+
+    it('is implemented by a GObject class with the correct class object', function () {
+        let obj = new GObjectImplementingGObjectInterface();
+        expect(obj.constructor).toBe(GObjectImplementingGObjectInterface);
+        expect(obj.constructor.name)
+            .toEqual('GObjectImplementingGObjectInterface');
+    });
+
+    it('can have its required function implemented', function () {
+        expect(() => {
+            let obj = new GObjectImplementingGObjectInterface();
+            obj.requiredG();
+        }).not.toThrow();
+    });
+
+    it('must have its required function implemented', function () {
+        const BadObject = GObject.registerClass({
+            Implements: [ AGObjectInterface ],
+            Properties: {
+                'interface-prop': GObject.ParamSpec.override('interface-prop',
+                    AGObjectInterface)
+            }
+        }, class BadObject extends GObject.Object {});
+        expect(() => new BadObject().requiredG())
+           .toThrowError(GObject.NotImplementedError);
+    });
+
+    it("doesn't have to have its optional function implemented", function () {
+        let obj;
+        expect(() => { obj = new MinimalImplementationOfAGObjectInterface(); })
+            .not.toThrow();
+        expect(obj instanceof AGObjectInterface).toBeTruthy();
+    });
+
+    it('can have its optional function deferred to by the implementation', function () {
+        let obj = new MinimalImplementationOfAGObjectInterface();
+        expect(obj.optionalG()).toEqual('AGObjectInterface.optionalG()');
+    });
+
+    it('can have its function chained up to', function () {
+        let obj = new GObjectImplementingGObjectInterface();
+        expect(obj.optionalG()).toEqual('AGObjectInterface.optionalG()');
+    });
+
+    it('can require another interface', function () {
+        let obj;
+        expect(() => { obj = new ImplementationOfTwoInterfaces(); }).not.toThrow();
+        expect(obj instanceof AGObjectInterface).toBeTruthy();
+        expect(obj instanceof InterfaceRequiringGObjectInterface).toBeTruthy();
+    });
+
+    it('can chain up to another interface', function () {
+        let obj = new ImplementationOfTwoInterfaces();
+        expect(obj.optionalG())
+            .toEqual('InterfaceRequiringGObjectInterface.optionalG()\nAGObjectInterface.optionalG()');
+    });
+
+    it("defers to the last interface's optional function", function () {
+        const MinimalImplementationOfTwoInterfaces = GObject.registerClass({
+            Implements: [ AGObjectInterface, InterfaceRequiringGObjectInterface ],
+            Properties: {
+                'interface-prop': GObject.ParamSpec.override('interface-prop',
+                    AGObjectInterface)
+            },
+        }, class MinimalImplementationOfTwoInterfaces extends GObject.Object {
+            requiredG() {}
+        });
+        let obj = new MinimalImplementationOfTwoInterfaces();
+        expect(obj.optionalG())
+            .toEqual('InterfaceRequiringGObjectInterface.optionalG()\nAGObjectInterface.optionalG()');
+    });
+
+    it('must be implemented by a class that implements all required interfaces', function () {
+        expect(() => GObject.registerClass({
+            Implements: [ InterfaceRequiringGObjectInterface ],
+        }, class BadObject {
+            required() {}
+        })).toThrow();
+    });
+
+    it('must be implemented by a class that implements required interfaces in correct order', function () {
+        expect(() => GObject.registerClass({
+            Implements: [ InterfaceRequiringGObjectInterface, AGObjectInterface ],
+        }, class BadObject {
+            required() {}
+        })).toThrow();
+    });
+
+    it('can require an interface from C', function () {
+        const InitableInterface = GObject.registerClass({
+            Requires: [ GObject.Object, Gio.Initable ]
+        }, class InitableInterface extends GObject.Interface {});
+        expect(() => GObject.registerClass({
+            Implements: [ InitableInterface ],
+        }, class BadObject {})).toThrow();
+    });
+
+    it('can define signals on the implementing class', function () {
+        function quitLoop() {
+            Mainloop.quit('signal');
+        }
+        let obj = new GObjectImplementingGObjectInterface();
+        let interfaceSignalSpy = jasmine.createSpy('interfaceSignalSpy')
+            .and.callFake(quitLoop);
+        let classSignalSpy = jasmine.createSpy('classSignalSpy')
+            .and.callFake(quitLoop);
+        obj.connect('interface-signal', interfaceSignalSpy);
+        obj.connect('class-signal', classSignalSpy);
+        GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+            obj.emit('interface-signal');
+            return GLib.SOURCE_REMOVE;
+        });
+        Mainloop.run('signal');
+        GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+            obj.emit('class-signal');
+            return GLib.SOURCE_REMOVE;
+        });
+        Mainloop.run('signal');
+        expect(interfaceSignalSpy).toHaveBeenCalled();
+        expect(classSignalSpy).toHaveBeenCalled();
+    });
+
+    it('can define properties on the implementing class', function () {
+        let obj = new GObjectImplementingGObjectInterface();
+        expect(obj.interface_prop).toEqual('foobar');
+        expect(obj.class_prop).toEqual('meh');
+    });
+
+    it('must have its properties overridden', function () {
+        // Failing to override an interface property doesn't raise an error but
+        // instead logs a critical warning.
+        GLib.test_expect_message('GLib-GObject', GLib.LogLevelFlags.LEVEL_CRITICAL,
+            "Object class * doesn't implement property 'interface-prop' from " +
+            "interface 'ArbitraryGTypeName'");
+        GObject.registerClass({
+            Implements: [ AGObjectInterface ],
+        }, class MyNaughtyObject extends GObject.Object {
+            requiredG() {}
+        });
+        // g_test_assert_expected_messages() is a macro, not introspectable
+        GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectInterface.js',
+            253, 'testGObjectMustOverrideInterfaceProperties');
+    });
+
+    it('can be implemented by a class as well as its parent class', function () {
+        const SubObject = GObject.registerClass(
+            class SubObject extends GObjectImplementingGObjectInterface {});
+        let obj = new SubObject();
+        expect(obj instanceof AGObjectInterface).toBeTruthy();
+        expect(obj.interface_prop).toEqual('foobar');  // override not needed
+    });
+
+    it('can be reimplemented by a subclass of a class that already implements it', function () {
+        const SubImplementer = GObject.registerClass({
+            Implements: [ AGObjectInterface ],
+        }, class SubImplementer extends GObjectImplementingGObjectInterface {});
+        let obj = new SubImplementer();
+        expect(obj instanceof AGObjectInterface).toBeTruthy();
+        expect(obj.interface_prop).toEqual('foobar');  // override not needed
+    });
+});
diff --git a/modules/overrides/GObject.js b/modules/overrides/GObject.js
index 64c3b3f..2d021d6 100644
--- a/modules/overrides/GObject.js
+++ b/modules/overrides/GObject.js
@@ -1,4 +1,6 @@
+/* exported _init, interfaces, properties, registerClass, requires, signals */
 // Copyright 2011 Jasper St. Pierre
+// Copyright 2017 Philip Chimento <philip chimento gmail com>, <philip endlessm com>
 //
 // Permission is hereby granted, free of charge, to any person obtaining a copy
 // of this software and associated documentation files (the "Software"), to
@@ -24,6 +26,136 @@ const Legacy = imports._legacy;
 
 let GObject;
 
+var GTypeName = Symbol('GType name');
+var interfaces = Symbol('GObject interfaces');
+var properties = Symbol('GObject properties');
+var requires = Symbol('GObject interface requires');
+var signals = Symbol('GObject signals');
+
+function registerClass(klass) {
+    if (arguments.length == 2) {
+        // The two-argument form is the convenient syntax without ESnext
+        // decorators and class data properties. The first argument is an
+        // object with meta info such as properties and signals. The second
+        // argument is the class expression for the class itself.
+        //
+        //     var MyClass = GObject.registerClass({
+        //         Properties: { ... },
+        //         Signals: { ... },
+        //     }, class MyClass extends GObject.Object {
+        //         _init() { ... }
+        //     });
+        //
+        // When decorators and class data properties become part of the JS
+        // standard, this function can be used directly as a decorator.
+        let metaInfo = arguments[0];
+        klass = arguments[1];
+        if ('GTypeName' in metaInfo)
+            klass[GTypeName] = metaInfo.GTypeName;
+        if ('Implements' in metaInfo)
+            klass[interfaces] = metaInfo.Implements;
+        if ('Properties' in metaInfo)
+            klass[properties] = metaInfo.Properties;
+        if ('Signals' in metaInfo)
+            klass[signals] = metaInfo.Signals;
+        if ('Requires' in metaInfo)
+            klass[requires] = metaInfo.Requires;
+    }
+
+    if (!(klass.prototype instanceof GObject.Object) &&
+        !(klass.prototype instanceof GObject.Interface))
+        throw new TypeError('GObject.registerClass() used with invalid base ' +
+            `class (is ${Object.getPrototypeOf(klass).name})`);
+
+    // Find the "least derived" class with a _classInit static function; there
+    // definitely is one, since this class must inherit from GObject
+    let initclass = klass;
+    while (typeof initclass._classInit === 'undefined')
+        initclass = Object.getPrototypeOf(initclass.prototype).constructor;
+    return initclass._classInit(klass);
+}
+
+// Some common functions between GObject.Class and GObject.Interface
+
+function _createSignals(gtype, signals) {
+    for (let signalName in signals) {
+        let obj = signals[signalName];
+        let flags = (obj.flags !== undefined) ? obj.flags : GObject.SignalFlags.RUN_FIRST;
+        let accumulator = (obj.accumulator !== undefined) ? obj.accumulator : GObject.AccumulatorType.NONE;
+        let rtype = (obj.return_type !== undefined) ? obj.return_type : GObject.TYPE_NONE;
+        let paramtypes = (obj.param_types !== undefined) ? obj.param_types : [];
+
+        try {
+            obj.signal_id = Gi.signal_new(gtype, signalName, flags, accumulator, rtype, paramtypes);
+        } catch (e) {
+            throw new TypeError('Invalid signal ' + signalName + ': ' + e.message);
+        }
+    }
+}
+
+function _createGTypeName(klass) {
+    if (klass.hasOwnProperty(GTypeName))
+        return klass[GTypeName];
+    return `Gjs_${klass.name}`;
+}
+
+function _propertiesAsArray(klass) {
+    let propertiesArray = [];
+    if (klass.hasOwnProperty(properties)) {
+        for (let prop in klass[properties]) {
+            propertiesArray.push(klass[properties][prop]);
+        }
+    }
+    return propertiesArray;
+}
+
+function _copyAllDescriptors(target, source) {
+    Object.getOwnPropertyNames(source)
+    .filter(key => !['prototype', 'constructor'].includes(key))
+    .concat(Object.getOwnPropertySymbols(source))
+    .forEach(key => {
+        let descriptor = Object.getOwnPropertyDescriptor(source, key);
+        Object.defineProperty(target, key, descriptor);
+    });
+}
+
+function _interfacePresent(required, klass) {
+    if (!klass[interfaces])
+        return false;
+    if (klass[interfaces].includes(required))
+        return true;  // implemented here
+    // Might be implemented on a parent class
+    return _interfacePresent(required, Object.getPrototypeOf(klass));
+}
+
+function _checkInterface(iface, proto) {
+    // Check that proto implements all of this interface's required interfaces.
+    // "proto" refers to the object's prototype (which implements the interface)
+    // whereas "iface.prototype" is the interface's prototype (which may still
+    // contain unimplemented methods.)
+    if (typeof iface[requires] === 'undefined')
+        return;
+
+    let unfulfilledReqs = iface[requires].filter(required => {
+        // Either the interface is not present or it is not listed before the
+        // interface that requires it or the class does not inherit it. This is
+        // so that required interfaces don't copy over properties from other
+        // interfaces that require them.
+        let ifaces = proto.constructor[interfaces];
+        return ((!_interfacePresent(required, proto.constructor) ||
+            ifaces.indexOf(required) > ifaces.indexOf(iface)) &&
+            !(proto instanceof required));
+    }).map(required =>
+        // required.name will be present on JS classes, but on introspected
+        // GObjects it will be the C name. The alternative is just so that
+        // we print something if there is garbage in Requires.
+        required.name || required);
+    if (unfulfilledReqs.length > 0) {
+        throw new Error('The following interfaces must be implemented before ' +
+            `${iface.name}: ${unfulfilledReqs.join(', ')}`);
+    }
+}
+
 function _init() {
 
     GObject = this;
@@ -184,6 +316,112 @@ function _init() {
         return this;
     };
 
+    GObject.registerClass = registerClass;
+
+    GObject.Object._classInit = function(klass) {
+        let gtypename = _createGTypeName(klass);
+        let gobjectInterfaces = klass.hasOwnProperty(interfaces) ?
+            klass[interfaces] : [];
+        let propertiesArray = _propertiesAsArray(klass);
+        let parent = Object.getPrototypeOf(klass);
+        let gobjectSignals = klass.hasOwnProperty(signals) ?
+            klass[signals] : [];
+
+        let newClass = Gi.register_type(parent.prototype, gtypename,
+            gobjectInterfaces, propertiesArray);
+        Object.setPrototypeOf(newClass, parent);
+
+        _createSignals(newClass.$gtype, gobjectSignals);
+
+        _copyAllDescriptors(newClass, klass);
+        gobjectInterfaces.forEach(iface =>
+            _copyAllDescriptors(newClass.prototype, iface.prototype));
+        _copyAllDescriptors(newClass.prototype, klass.prototype);
+
+        Object.getOwnPropertyNames(newClass.prototype)
+        .filter(name => name.startsWith('vfunc_') || name.startsWith('on_'))
+        .forEach(name => {
+            let descr = Object.getOwnPropertyDescriptor(newClass.prototype, name);
+            if (typeof descr.value !== 'function')
+                return;
+
+            let func = newClass.prototype[name];
+
+            if (name.startsWith('vfunc_')) {
+                Gi.hook_up_vfunc(newClass.prototype, name.slice(6), func);
+            } else if (name.startsWith('on_')) {
+                let id = GObject.signal_lookup(name.slice(3).replace('_', '-'),
+                    newClass.$gtype);
+                if (id !== 0) {
+                    GObject.signal_override_class_closure(id, newClass.$gtype, function() {
+                        let argArray = Array.from(arguments);
+                        let emitter = argArray.shift();
+
+                        return func.apply(emitter, argArray);
+                    });
+                }
+            }
+        });
+
+        gobjectInterfaces.forEach(iface =>
+            _checkInterface(iface, newClass.prototype));
+
+        // For backwards compatibility only. Use instanceof instead.
+        newClass.implements = function(iface) {
+            if (iface.$gtype)
+                return GObject.type_is_a(newClass.$gtype, iface.$gtype);
+            return false;
+        }
+
+        return newClass;
+    };
+
+    GObject.Interface._classInit = function(klass) {
+        let gtypename = _createGTypeName(klass);
+        let gobjectInterfaces = klass.hasOwnProperty(requires) ?
+            klass[requires] : [];
+        let properties = _propertiesAsArray(klass);
+        let gobjectSignals = klass.hasOwnProperty(signals) ?
+            klass[signals] : [];
+
+        let newInterface = Gi.register_interface(gtypename, gobjectInterfaces,
+            properties);
+
+        _createSignals(newInterface.$gtype, gobjectSignals);
+
+        _copyAllDescriptors(newInterface, klass);
+
+        Object.getOwnPropertyNames(klass.prototype)
+        .filter(key => key !== 'constructor')
+        .concat(Object.getOwnPropertySymbols(klass.prototype))
+        .forEach(key => {
+            let descr = Object.getOwnPropertyDescriptor(klass.prototype, key);
+
+            // Create wrappers on the interface object so that generics work (e.g.
+            // SomeInterface.some_function(this, blah) instead of
+            // SomeInterface.prototype.some_function.call(this, blah)
+            if (typeof descr.value === 'function') {
+                let interfaceProto = klass.prototype;  // capture in closure
+                newInterface[key] = function () {
+                    return interfaceProto[key].call.apply(interfaceProto[key],
+                        arguments);
+                };
+            }
+
+            Object.defineProperty(newInterface.prototype, key, descr);
+        });
+
+        return newInterface;
+    };
+
+    /**
+     * Use this to signify a function that must be overridden in an
+     * implementation of the interface.
+     */
+    GObject.NotImplementedError = class NotImplementedError extends Error {
+        get name() { return 'NotImplementedError'; }
+    };
+
     // fake enum for signal accumulators, keep in sync with gi/object.c
     this.AccumulatorType = {
         NONE: 0,



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]