[gjs/wip/ptomato/classes: 11/12] FIXME: GObject: Adapt GObject class framework to ES6



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

    FIXME: GObject: Adapt GObject class framework to ES6
    
    Need commit message

 Makefile-test.am                           |    2 +
 gi/interface.cpp                           |   36 +++-
 installed-tests/js/testGObjectClass.js     |  358 ++++++++++++++++++++++++++++
 installed-tests/js/testGObjectInterface.js |  266 +++++++++++++++++++++
 modules/overrides/GObject.js               |  229 ++++++++++++++++++
 5 files changed, 890 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..32684a1
--- /dev/null
+++ b/installed-tests/js/testGObjectClass.js
@@ -0,0 +1,358 @@
+// -*- 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 Lang = imports.lang;
+
+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 {
+    _init(props={}) {
+        // check that it's safe to set properties before
+        // chaining up (priv is NULL at this point, remember)
+        this._readwrite = 'foo';
+        this._readonly = 'bar';
+        this._constructProp = null;
+        this._constructCalled = false;
+
+        super._init(props);
+    }
+
+    get readwrite() {
+        return this._readwrite;
+    }
+
+    set readwrite(val) {
+        if (val == 'ignore')
+            return;
+
+        this._readwrite = val;
+    }
+
+    get readonly() {
+        return this._readonly;
+    }
+
+    set readonly(val) {
+        // this should never be called
+        this._readonly = 'bogus';
+    }
+
+    get construct() {
+        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 {
+    _init(props={}) {
+        super._init(props);
+        this.inited = false;
+    }
+
+    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 {
+    _init() {
+        this.foo = false;
+
+        super._init();
+    }
+
+    _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 = new Lang.Class({
+            Name: 'InterfacePropObject',
+            Extends: GObject.Object,
+            Properties: {
+                'file': GObject.ParamSpec.object('file', 'File', 'File',
+                    GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
+                    Gio.File.$gtype)
+            },
+        });
+        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 = new Lang.Class({
+            Name: 'OverrideObject',
+            Extends: MyObject,
+            Properties: {
+                'readwrite': GObject.ParamSpec.override('readwrite', 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(() => new Lang.Class({
+            Name: 'BadOverride',
+            Extends: GObject.Object,
+            Properties: {
+                'nonexistent': GObject.ParamSpec.override('nonexistent', 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 9cf9d32..24f4cea 100644
--- a/modules/overrides/GObject.js
+++ b/modules/overrides/GObject.js
@@ -1,3 +1,4 @@
+/* exported _init, interfaces, properties, registerClass, signals */
 // Copyright 2011 Jasper St. Pierre
 //
 // Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -24,6 +25,136 @@ const Legacy = imports._legacy;
 
 let GObject;
 
+var GTypeName = Symbol('GTypeName');
+var interfaces = Symbol('GObject interfaces');
+var properties = Symbol('GObject properties');
+var signals = Symbol('GObject signals');
+var requires = Symbol('GObject interface requires');
+
+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 {
+        //         constructor() { ... }
+        //     });
+        //
+        // When decorators and class data properties become part of the
+        // 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 +315,104 @@ function _init() {
         return this;
     };
 
+    this.registerClass = registerClass;
+
+    this.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);
+
+        _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));
+
+        return newClass;
+    };
+
+    this.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.
+     */
+    this.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]