[gjs/wip/ptomato/classes: 10/11] GObject: Move all legacy GObject class code



commit 192bee3b4fceee5560817dd3cca336f1dd93960e
Author: Philip Chimento <philip chimento gmail com>
Date:   Sun Jul 23 21:20:56 2017 -0700

    GObject: Move all legacy GObject class code
    
    This moves the GObject.Class and GObject.Interface code into the Legacy
    module along with Lang.Class and Lang.Interface.
    
    Also moves all tests for the legacy GObject code into a separate file.
    It is named testLegacyGObject in order to indicate that it tests legacy
    code.

 Makefile-test.am                           |    3 +-
 installed-tests/js/testGObjectClass.js     |  362 --------------
 installed-tests/js/testGObjectInterface.js |  391 ---------------
 installed-tests/js/testLegacyGObject.js    |  750 ++++++++++++++++++++++++++++
 modules/_legacy.js                         |  237 +++++++++-
 modules/overrides/GObject.js               |  231 +---------
 6 files changed, 990 insertions(+), 984 deletions(-)
---
diff --git a/Makefile-test.am b/Makefile-test.am
index 71cd3da..a679e03 100644
--- a/Makefile-test.am
+++ b/Makefile-test.am
@@ -226,13 +226,12 @@ 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                      \
        installed-tests/js/testLang.js                          \
        installed-tests/js/testLegacyClass.js                   \
+       installed-tests/js/testLegacyGObject.js                 \
        installed-tests/js/testLocale.js                        \
        installed-tests/js/testMainloop.js                      \
        installed-tests/js/testNamespace.js                     \
diff --git a/installed-tests/js/testLegacyGObject.js b/installed-tests/js/testLegacyGObject.js
new file mode 100644
index 0000000..e9f0989
--- /dev/null
+++ b/installed-tests/js/testLegacyGObject.js
@@ -0,0 +1,750 @@
+// -*- mode: js; indent-tabs-mode: nil -*-
+imports.gi.versions.Gtk = '3.0';
+
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const GObject = imports.gi.GObject;
+const Gtk = imports.gi.Gtk;
+const Lang = imports.lang;
+const Mainloop = imports.mainloop;
+
+const MyObject = new GObject.Class({
+    Name: 'MyObject',
+    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 ],
+        },
+    },
+
+    _init: function(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;
+
+        this.parent(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: function() {
+        this._readonly = 'changed';
+
+        this.notify('readonly');
+    },
+
+    emit_empty: function() {
+        this.emit('empty');
+    },
+
+    emit_minimal: function(one, two) {
+        this.emit('minimal', one, two);
+    },
+
+    emit_full: function() {
+        return this.emit('full');
+    },
+
+    emit_detailed: function() {
+        this.emit('detailed::one');
+        this.emit('detailed::two');
+    },
+
+    emit_run_last: function(callback) {
+        this._run_last_callback = callback;
+        this.emit('run-last');
+    },
+
+    on_run_last: function() {
+        this._run_last_callback();
+    },
+
+    on_empty: function() {
+        this.empty_called = true;
+    },
+
+    on_full: function() {
+        this.full_default_handler_called = true;
+        return 79;
+    }
+});
+
+const MyApplication = new Lang.Class({
+    Name: 'MyApplication',
+    Extends: Gio.Application,
+    Signals: { 'custom': { param_types: [ GObject.TYPE_INT ] } },
+
+    _init: function(params) {
+        this.parent(params);
+    },
+
+    emit_custom: function(n) {
+        this.emit('custom', n);
+    }
+});
+
+const MyInitable = new Lang.Class({
+    Name: 'MyInitable',
+    Extends: GObject.Object,
+    Implements: [ Gio.Initable ],
+
+    _init: function(params) {
+        this.parent(params);
+
+        this.inited = false;
+    },
+
+    vfunc_init: function(cancellable) { // error?
+        if (!(cancellable instanceof Gio.Cancellable))
+            throw 'Bad argument';
+
+        this.inited = true;
+    }
+});
+
+const Derived = new Lang.Class({
+    Name: 'Derived',
+    Extends: MyObject,
+
+    _init: function() {
+        this.parent({ readwrite: 'yes' });
+    }
+});
+
+const MyCustomInit = new Lang.Class({
+    Name: 'MyCustomInit',
+    Extends: GObject.Object,
+
+    _init: function() {
+        this.foo = false;
+
+        this.parent();
+    },
+
+    _instance_init: function() {
+        this.foo = true;
+    }
+});
+
+describe('GObject class', function () {
+    let myInstance;
+    beforeEach(function () {
+        myInstance = new MyObject();
+    });
+
+    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.constructor.implements(Gio.Initable)).toBeTruthy();
+    });
+
+    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();
+    });
+});
+
+const AnInterface = new Lang.Interface({
+    Name: 'AnInterface',
+});
+
+const GObjectImplementingLangInterface = new Lang.Class({
+    Name: 'GObjectImplementingLangInterface',
+    Extends: GObject.Object,
+    Implements: [ AnInterface ],
+
+    _init: function (props={}) {
+        this.parent(props);
+    }
+});
+
+const AGObjectInterface = new Lang.Interface({
+    Name: 'AGObjectInterface',
+    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': {}
+    },
+
+    requiredG: Lang.Interface.UNIMPLEMENTED,
+    optionalG: function () {
+        return 'AGObjectInterface.optionalG()';
+    }
+});
+
+const InterfaceRequiringGObjectInterface = new Lang.Interface({
+    Name: 'InterfaceRequiringGObjectInterface',
+    Requires: [ AGObjectInterface ],
+
+    optionalG: function () {
+        return 'InterfaceRequiringGObjectInterface.optionalG()\n' +
+            AGObjectInterface.optionalG(this);
+    }
+});
+
+const GObjectImplementingGObjectInterface = new Lang.Class({
+    Name: 'GObjectImplementingGObjectInterface',
+    Extends: GObject.Object,
+    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': {},
+    },
+
+    get interface_prop() {
+        return 'foobar';
+    },
+
+    get class_prop() {
+        return 'meh';
+    },
+
+    _init: function (props={}) {
+        this.parent(props);
+    },
+    requiredG: function () {},
+    optionalG: function () {
+        return AGObjectInterface.optionalG(this);
+    }
+});
+
+const MinimalImplementationOfAGObjectInterface = new Lang.Class({
+    Name: 'MinimalImplementationOfAGObjectInterface',
+    Extends: GObject.Object,
+    Implements: [ AGObjectInterface ],
+    Properties: {
+        'interface-prop': GObject.ParamSpec.override('interface-prop',
+            AGObjectInterface)
+    },
+
+    _init: function (props={}) {
+        this.parent(props);
+    },
+    requiredG: function () {}
+});
+
+const ImplementationOfTwoInterfaces = new Lang.Class({
+    Name: 'ImplementationOfTwoInterfaces',
+    Extends: GObject.Object,
+    Implements: [ AGObjectInterface, InterfaceRequiringGObjectInterface ],
+    Properties: {
+        'interface-prop': GObject.ParamSpec.override('interface-prop',
+            AGObjectInterface)
+    },
+
+    _init: function (props={}) {
+        this.parent(props);
+    },
+    requiredG: function () {},
+    optionalG: function () {
+        return InterfaceRequiringGObjectInterface.optionalG(this);
+    }
+});
+
+describe('GObject interface', function () {
+    it('class can implement a Lang.Interface', function () {
+        let obj;
+        expect(() => { obj = new GObjectImplementingLangInterface(); })
+            .not.toThrow();
+        expect(obj.constructor.implements(AnInterface)).toBeTruthy();
+    });
+
+    it('throws when an interface requires a GObject interface but not GObject.Object', function () {
+        expect(() => new Lang.Interface({
+            Name: 'GObjectInterfaceNotRequiringGObject',
+            GTypeName: 'GTypeNameNotRequiringGObject',
+            Requires: [ Gio.Initable ]
+        })).toThrow();
+    });
+
+    it('can be implemented by a GObject class along with a JS interface', function () {
+        const ObjectImplementingLangInterfaceAndCInterface = new Lang.Class({
+            Name: 'ObjectImplementingLangInterfaceAndCInterface',
+            Extends: GObject.Object,
+            Implements: [ AnInterface, Gio.Initable ],
+
+            _init: function (props={}) {
+                this.parent(props);
+            }
+        });
+        let obj;
+        expect(() => { obj = new ObjectImplementingLangInterfaceAndCInterface(); })
+            .not.toThrow();
+        expect(obj.constructor.implements(AnInterface)).toBeTruthy();
+        expect(obj.constructor.implements(Gio.Initable)).toBeTruthy();
+    });
+
+    it('is an instance of the interface classes', function () {
+        expect(AGObjectInterface instanceof Lang.Interface).toBeTruthy();
+        expect(AGObjectInterface instanceof GObject.Interface).toBeTruthy();
+    });
+
+    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.constructor.implements(AGObjectInterface)).toBeTruthy();
+    });
+
+    it('is implemented by a GObject class with the correct class object', function () {
+        let obj = new GObjectImplementingGObjectInterface();
+        expect(obj.constructor).toEqual(GObjectImplementingGObjectInterface);
+        expect(obj.constructor.name)
+            .toEqual('GObjectImplementingGObjectInterface');
+    });
+
+    it('can be implemented by a class also implementing a Lang.Interface', function () {
+        const GObjectImplementingBothKindsOfInterface = new Lang.Class({
+            Name: 'GObjectImplementingBothKindsOfInterface',
+            Extends: GObject.Object,
+            Implements: [ AnInterface, AGObjectInterface ],
+            Properties: {
+                'interface-prop': GObject.ParamSpec.override('interface-prop',
+                    AGObjectInterface)
+            },
+
+            _init: function (props={}) {
+                this.parent(props);
+            },
+            required: function () {},
+            requiredG: function () {}
+        });
+        let obj;
+        expect(() => { obj = new GObjectImplementingBothKindsOfInterface(); })
+            .not.toThrow();
+        expect(obj.constructor.implements(AnInterface)).toBeTruthy();
+        expect(obj.constructor.implements(AGObjectInterface)).toBeTruthy();
+    });
+
+    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 () {
+        expect(() => new Lang.Class({
+            Name: 'BadObject',
+            Extends: GObject.Object,
+            Implements: [ AGObjectInterface ],
+            Properties: {
+                'interface-prop': GObject.ParamSpec.override('interface-prop',
+                    AGObjectInterface)
+            }
+        })).toThrow();
+    });
+
+    it("doesn't have to have its optional function implemented", function () {
+        let obj;
+        expect(() => { obj = new MinimalImplementationOfAGObjectInterface(); })
+            .not.toThrow();
+        expect(obj.constructor.implements(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.constructor.implements(AGObjectInterface)).toBeTruthy();
+        expect(obj.constructor.implements(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 = new Lang.Class({
+            Name: 'MinimalImplementationOfTwoInterfaces',
+            Extends: GObject.Object,
+            Implements: [ AGObjectInterface, InterfaceRequiringGObjectInterface ],
+            Properties: {
+                'interface-prop': GObject.ParamSpec.override('interface-prop',
+                    AGObjectInterface)
+            },
+
+            _init: function (props={}) {
+                this.parent(props);
+            },
+            requiredG: function () {}
+        });
+        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(() => new Lang.Class({
+            Name: 'BadObject',
+            Implements: [ InterfaceRequiringGObjectInterface ],
+            required: function () {}
+        })).toThrow();
+    });
+
+    it('must be implemented by a class that implements required interfaces in correct order', function () {
+        expect(() => new Lang.Class({
+            Name: 'BadObject',
+            Implements: [ InterfaceRequiringGObjectInterface, AGObjectInterface ],
+            required: function () {}
+        })).toThrow();
+    });
+
+    it('can require an interface from C', function () {
+        const InitableInterface = new Lang.Interface({
+            Name: 'InitableInterface',
+            Requires: [ GObject.Object, Gio.Initable ]
+        });
+        expect(() => new Lang.Class({
+            Name: 'BadObject',
+            Implements: [ InitableInterface ]
+        })).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'");
+        new Lang.Class({
+            Name: 'MyNaughtyObject',
+            Extends: GObject.Object,
+            Implements: [ AGObjectInterface ],
+            _init: function (props={}) {
+                this.parent(props);
+            },
+            requiredG: function () {}
+        });
+        // g_test_assert_expected_messages() is a macro, not introspectable
+        GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectInterface.js',
+            416, 'testGObjectMustOverrideInterfaceProperties');
+    });
+
+    // This makes sure that we catch the case where the metaclass (e.g.
+    // GtkWidgetClass) doesn't specify a meta-interface. In that case we get the
+    // meta-interface from the metaclass's parent.
+    it('gets the correct type for its metaclass', function () {
+        const MyMeta = new Lang.Class({
+            Name: 'MyMeta',
+            Extends: GObject.Class
+        });
+        const MyMetaObject = new MyMeta({
+            Name: 'MyMetaObject'
+        });
+        const MyMetaInterface = new Lang.Interface({
+            Name: 'MyMetaInterface',
+            Requires: [ MyMetaObject ]
+        });
+        expect(MyMetaInterface instanceof GObject.Interface).toBeTruthy();
+    });
+
+    it('can be implemented by a class as well as its parent class', function () {
+        const SubObject = new Lang.Class({
+            Name: 'SubObject',
+            Extends: GObjectImplementingGObjectInterface
+        });
+        let obj = new SubObject();
+        expect(obj.constructor.implements(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 = new Lang.Class({
+            Name: 'SubImplementer',
+            Extends: GObjectImplementingGObjectInterface,
+            Implements: [ AGObjectInterface ]
+        });
+        let obj = new SubImplementer();
+        expect(obj.constructor.implements(AGObjectInterface)).toBeTruthy();
+        expect(obj.interface_prop).toEqual('foobar');  // override not needed
+    });
+});
diff --git a/modules/_legacy.js b/modules/_legacy.js
index 2091488..edf2f33 100644
--- a/modules/_legacy.js
+++ b/modules/_legacy.js
@@ -1,5 +1,5 @@
 /* -*- mode: js; indent-tabs-mode: nil; -*- */
-/* exported Class, Interface */
+/* exported Class, Interface, defineGObjectLegacyObjects */
 // Copyright 2008  litl, LLC
 // Copyright 2011  Jasper St. Pierre
 
@@ -388,3 +388,238 @@ Interface.prototype._init = function (params) {
         },
     });
 };
+
+// GObject Lang.Class magic
+
+function defineGObjectLegacyObjects(GObject) {
+    const Gi = imports._gi;
+
+    // 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(params) {
+        if (params.GTypeName)
+            return params.GTypeName;
+        else
+            return 'Gjs_' + params.Name;
+    }
+
+    function _getGObjectInterfaces(interfaces) {
+        return interfaces.filter((iface) => iface.hasOwnProperty('$gtype'));
+    }
+
+    function _propertiesAsArray(params) {
+        let propertiesArray = [];
+        if (params.Properties) {
+            for (let prop in params.Properties) {
+                propertiesArray.push(params.Properties[prop]);
+            }
+        }
+        return propertiesArray;
+    }
+
+    const GObjectMeta = new Class({
+        Name: 'GObjectClass',
+        Extends: Class,
+
+        _init: function (params) {
+            // retrieve signals and remove them from params before chaining
+            let signals = params.Signals;
+            delete params.Signals;
+
+            this.parent(params);
+
+            if (signals)
+                _createSignals(this.$gtype, signals);
+
+            Object.getOwnPropertyNames(params).forEach(function(name) {
+                if (name == 'Name' || name == 'Extends' || name == 'Abstract')
+                    return;
+
+                let descriptor = Object.getOwnPropertyDescriptor(params, name);
+
+                if (typeof descriptor.value === 'function') {
+                    let wrapped = this.prototype[name];
+
+                    if (name.slice(0, 6) == 'vfunc_') {
+                        Gi.hook_up_vfunc(this.prototype, name.slice(6), wrapped);
+                    } else if (name.slice(0, 3) == 'on_') {
+                        let id = GObject.signal_lookup(name.slice(3).replace('_', '-'), this.$gtype);
+                        if (id !== 0) {
+                            GObject.signal_override_class_closure(id, this.$gtype, function() {
+                                let argArray = Array.prototype.slice.call(arguments);
+                                let emitter = argArray.shift();
+
+                                return wrapped.apply(emitter, argArray);
+                            });
+                        }
+                    }
+                }
+            }.bind(this));
+        },
+
+        _isValidClass: function(klass) {
+            let proto = klass.prototype;
+
+            if (!proto)
+                return false;
+
+            // If proto == GObject.Object.prototype, then
+            // proto.__proto__ is Object, so "proto instanceof GObject.Object"
+            // will return false.
+            return proto == GObject.Object.prototype ||
+                proto instanceof GObject.Object;
+        },
+
+        // If we want an object with a custom JSClass, we can't just
+        // use a function. We have to use a custom constructor here.
+        _construct: function(params) {
+            if (!params.Name)
+                throw new TypeError("Classes require an explicit 'Name' parameter.");
+            let name = params.Name;
+
+            let gtypename = _createGTypeName(params);
+
+            if (!params.Extends)
+                params.Extends = GObject.Object;
+            let parent = params.Extends;
+
+            if (!this._isValidClass(parent))
+                throw new TypeError('GObject.Class used with invalid base class (is ' + parent + ')');
+
+            let interfaces = params.Implements || [];
+            if (parent instanceof Class)
+                interfaces = interfaces.filter(iface => !parent.implements(iface));
+            let gobjectInterfaces = _getGObjectInterfaces(interfaces);
+
+            let propertiesArray = _propertiesAsArray(params);
+            delete params.Properties;
+
+            let newClass = Gi.register_type(parent.prototype, gtypename,
+                gobjectInterfaces, propertiesArray);
+
+            // See Class.prototype._construct for the reasoning
+            // behind this direct prototype set.
+            Object.setPrototypeOf(newClass, this.constructor.prototype);
+            newClass.__super__ = parent;
+
+            newClass._init.apply(newClass, arguments);
+
+            Object.defineProperties(newClass.prototype, {
+                '__metaclass__': {
+                    writable: false,
+                    configurable: false,
+                    enumerable: false,
+                    value: this.constructor,
+                },
+                '__interfaces__': {
+                    writable: false,
+                    configurable: false,
+                    enumerable: false,
+                    value: interfaces,
+                },
+            });
+            // Overwrite the C++-set class name, as if it were an ES6 class
+            Object.defineProperty(newClass, 'name', {
+                writable: false,
+                configurable: true,
+                enumerable: false,
+                value: name,
+            });
+
+            interfaces.forEach((iface) => {
+                if (iface instanceof Interface)
+                    iface._check(newClass.prototype);
+            });
+
+            return newClass;
+        },
+
+        // Overrides Lang.Class.implements()
+        implements: function (iface) {
+            if (iface instanceof GObject.Interface) {
+                return GObject.type_is_a(this.$gtype, iface.$gtype);
+            } else {
+                return this.parent(iface);
+            }
+        }
+    });
+
+    function GObjectInterface() {
+        return this._construct.apply(this, arguments);
+    }
+
+    GObjectMeta.MetaInterface = GObjectInterface;
+
+    GObjectInterface.__super__ = Interface;
+    GObjectInterface.prototype = Object.create(Interface.prototype);
+    GObjectInterface.prototype.constructor = GObjectInterface;
+
+    GObjectInterface.prototype._construct = function (params) {
+        if (!params.Name) {
+            throw new TypeError("Interfaces require an explicit 'Name' parameter.");
+        }
+
+        let gtypename = _createGTypeName(params);
+        delete params.GTypeName;
+
+        let interfaces = params.Requires || [];
+        let gobjectInterfaces = _getGObjectInterfaces(interfaces);
+
+        let properties = _propertiesAsArray(params);
+        delete params.Properties;
+
+        let newInterface = Gi.register_interface(gtypename, gobjectInterfaces,
+            properties);
+
+        // See Class.prototype._construct for the reasoning
+        // behind this direct prototype set.
+        Object.setPrototypeOf(newInterface, this.constructor.prototype);
+        newInterface.__super__ = GObjectInterface;
+        newInterface.prototype.constructor = newInterface;
+
+        newInterface._init.apply(newInterface, arguments);
+
+        Object.defineProperty(newInterface.prototype, '__metaclass__', {
+            writable: false,
+            configurable: false,
+            enumerable: false,
+            value: this.constructor,
+        });
+        // Overwrite the C++-set class name, as if it were an ES6 class
+        Object.defineProperty(newInterface, 'name', {
+            writable: false,
+            configurable: true,
+            enumerable: false,
+            value: params.Name,
+        });
+
+        return newInterface;
+    };
+
+    GObjectInterface.prototype._init = function (params) {
+        let signals = params.Signals;
+        delete params.Signals;
+
+        Interface.prototype._init.call(this, params);
+
+        _createSignals(this.$gtype, signals);
+    };
+
+    return {GObjectMeta, GObjectInterface};
+}
diff --git a/modules/overrides/GObject.js b/modules/overrides/GObject.js
index 5918de4..9cf9d32 100644
--- a/modules/overrides/GObject.js
+++ b/modules/overrides/GObject.js
@@ -20,235 +20,10 @@
 
 const Gi = imports._gi;
 const GjsPrivate = imports.gi.GjsPrivate;
-const Lang = imports.lang;
 const Legacy = imports._legacy;
 
 let GObject;
 
-// 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(params) {
-    if (params.GTypeName)
-        return params.GTypeName;
-    else
-        return 'Gjs_' + params.Name;
-}
-
-function _getGObjectInterfaces(interfaces) {
-    return interfaces.filter((iface) => iface.hasOwnProperty('$gtype'));
-}
-
-function _propertiesAsArray(params) {
-    let propertiesArray = [];
-    if (params.Properties) {
-        for (let prop in params.Properties) {
-            propertiesArray.push(params.Properties[prop]);
-        }
-    }
-    return propertiesArray;
-}
-
-const GObjectMeta = new Lang.Class({
-    Name: 'GObjectClass',
-    Extends: Lang.Class,
-
-    _init: function (params) {
-        // retrieve signals and remove them from params before chaining
-       let signals = params.Signals;
-        delete params.Signals;
-
-        this.parent(params);
-
-        if (signals)
-            _createSignals(this.$gtype, signals);
-
-       let propertyObj = { };
-       Object.getOwnPropertyNames(params).forEach(function(name) {
-            if (name == 'Name' || name == 'Extends' || name == 'Abstract')
-               return;
-
-            let descriptor = Object.getOwnPropertyDescriptor(params, name);
-
-            if (typeof descriptor.value === 'function') {
-               let wrapped = this.prototype[name];
-
-                if (name.slice(0, 6) == 'vfunc_') {
-                    Gi.hook_up_vfunc(this.prototype, name.slice(6), wrapped);
-                } else if (name.slice(0, 3) == 'on_') {
-                    let id = GObject.signal_lookup(name.slice(3).replace('_', '-'), this.$gtype);
-                    if (id != 0) {
-                        GObject.signal_override_class_closure(id, this.$gtype, function() {
-                            let argArray = Array.prototype.slice.call(arguments);
-                            let emitter = argArray.shift();
-
-                            return wrapped.apply(emitter, argArray);
-                        });
-                    }
-                }
-           }
-       }.bind(this));
-    },
-
-    _isValidClass: function(klass) {
-        let proto = klass.prototype;
-
-        if (!proto)
-            return false;
-
-        // If proto == GObject.Object.prototype, then
-        // proto.__proto__ is Object, so "proto instanceof GObject.Object"
-        // will return false.
-        return proto == GObject.Object.prototype ||
-            proto instanceof GObject.Object;
-    },
-
-    // If we want an object with a custom JSClass, we can't just
-    // use a function. We have to use a custom constructor here.
-    _construct: function(params) {
-        if (!params.Name)
-            throw new TypeError("Classes require an explicit 'Name' parameter.");
-        let name = params.Name;
-
-        let gtypename = _createGTypeName(params);
-
-        if (!params.Extends)
-            params.Extends = GObject.Object;
-        let parent = params.Extends;
-
-        if (!this._isValidClass(parent))
-            throw new TypeError('GObject.Class used with invalid base class (is ' + parent + ')');
-
-        let interfaces = params.Implements || [];
-        if (parent instanceof Lang.Class)
-            interfaces = interfaces.filter((iface) => !parent.implements(iface));
-        let gobjectInterfaces = _getGObjectInterfaces(interfaces);
-
-        let propertiesArray = _propertiesAsArray(params);
-        delete params.Properties;
-
-        let newClass = Gi.register_type(parent.prototype, gtypename,
-            gobjectInterfaces, propertiesArray);
-
-        // See Class.prototype._construct in _legacy.js for the reasoning
-        // behind this direct prototype set.
-        Object.setPrototypeOf(newClass, this.constructor.prototype);
-        newClass.__super__ = parent;
-
-        newClass._init.apply(newClass, arguments);
-
-        Object.defineProperties(newClass.prototype, {
-            '__metaclass__': { writable: false,
-                               configurable: false,
-                               enumerable: false,
-                               value: this.constructor },
-            '__interfaces__': { writable: false,
-                                configurable: false,
-                                enumerable: false,
-                                value: interfaces }
-        });
-        // Overwrite the C++-set class name, as if it were an ES6 class
-        Object.defineProperty(newClass, 'name', {
-            writable: false,
-            configurable: true,
-            enumerable: false,
-            value: name,
-        });
-
-        interfaces.forEach((iface) => {
-            if (iface instanceof Lang.Interface)
-                iface._check(newClass.prototype);
-        });
-
-        return newClass;
-    },
-
-    // Overrides Lang.Class.implements()
-    implements: function (iface) {
-        if (iface instanceof GObject.Interface) {
-            return GObject.type_is_a(this.$gtype, iface.$gtype);
-        } else {
-            return this.parent(iface);
-        }
-    }
-});
-
-function GObjectInterface(params) {
-    return this._construct.apply(this, arguments);
-}
-
-GObjectMeta.MetaInterface = GObjectInterface;
-
-GObjectInterface.__super__ = Lang.Interface;
-GObjectInterface.prototype = Object.create(Lang.Interface.prototype);
-GObjectInterface.prototype.constructor = GObjectInterface;
-
-GObjectInterface.prototype._construct = function (params) {
-    if (!params.Name) {
-        throw new TypeError("Interfaces require an explicit 'Name' parameter.");
-    }
-
-    let gtypename = _createGTypeName(params);
-    delete params.GTypeName;
-
-    let interfaces = params.Requires || [];
-    let gobjectInterfaces = _getGObjectInterfaces(interfaces);
-
-    let properties = _propertiesAsArray(params);
-    delete params.Properties;
-
-    let newInterface = Gi.register_interface(gtypename, gobjectInterfaces,
-        properties);
-
-    // See Class.prototype._construct in _legacy.js for the reasoning
-    // behind this direct prototype set.
-    Object.setPrototypeOf(newInterface, this.constructor.prototype);
-    newInterface.__super__ = GObjectInterface;
-    newInterface.prototype.constructor = newInterface;
-
-    newInterface._init.apply(newInterface, arguments);
-
-    Object.defineProperty(newInterface.prototype, '__metaclass__', {
-        writable: false,
-        configurable: false,
-        enumerable: false,
-        value: this.constructor
-    });
-    // Overwrite the C++-set class name, as if it were an ES6 class
-    Object.defineProperty(newInterface, 'name', {
-        writable: false,
-        configurable: true,
-        enumerable: false,
-        value: params.Name,
-    });
-
-    return newInterface;
-};
-
-GObjectInterface.prototype._init = function (params) {
-    let signals = params.Signals;
-    delete params.Signals;
-
-    Lang.Interface.prototype._init.call(this, params);
-
-    _createSignals(this.$gtype, signals);
-};
-
 function _init() {
 
     GObject = this;
@@ -397,9 +172,9 @@ function _init() {
                          get: function() { return GjsPrivate.param_spec_get_owner_type(this) } },
     });
 
-
-    this.Class = GObjectMeta;
-    this.Interface = GObjectInterface;
+    let legacyObjects = Legacy.defineGObjectLegacyObjects(GObject);
+    this.Class = legacyObjects.GObjectMeta;
+    this.Interface = legacyObjects.GObjectInterface;
     this.Object.prototype.__metaclass__ = this.Class;
 
     // For compatibility with Lang.Class... we need a _construct



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