[gjs/wip/ptomato/classes] WIP - GObject class stuff



commit dee67f9f895cd02cdf60ede30159e27acb380ff1
Author: Philip Chimento <philip endlessm com>
Date:   Thu Jul 27 23:30:58 2017 +0100

    WIP - GObject class stuff

 Makefile-test.am                       |    1 +
 gi/interface.cpp                       |   36 +++-
 installed-tests/js/testGObjectClass.js |  358 ++++++++++++++++++++++++++++++++
 modules/overrides/GObject.js           |  137 ++++++++++++
 4 files changed, 531 insertions(+), 1 deletions(-)
---
diff --git a/Makefile-test.am b/Makefile-test.am
index a679e03..1f3b209 100644
--- a/Makefile-test.am
+++ b/Makefile-test.am
@@ -226,6 +226,7 @@ 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/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/modules/overrides/GObject.js b/modules/overrides/GObject.js
index 9cf9d32..fdc2680 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,95 @@ 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');
+
+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 (!(klass.prototype instanceof GObject.Object))
+        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(name => name !== 'prototype')
+    .concat(Object.getOwnPropertySymbols(source))
+    .forEach(key => {
+        let descriptor = Object.getOwnPropertyDescriptor(source, key);
+        Object.defineProperty(target, key, descriptor);
+    });
+}
+
 function _init() {
 
     GObject = this;
@@ -184,6 +274,53 @@ 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);
+        _copyAllDescriptors(newClass.prototype, klass.prototype);
+
+        Object.getOwnPropertyNames(newClass.prototype)
+        .filter(name => name.startsWith('vfunc_') || name.startsWith('on_'))
+        .forEach(name => {
+            let descriptor = Object.getOwnPropertyDescriptor(newClass.prototype, name);
+            if (typeof descriptor.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);
+                    });
+                }
+            }
+        });
+
+        return newClass;
+    };
+
     // 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]