[gjs/wip/gobj-kitchen-sink: 14/21] object: Introduce support for signals



commit f1ee866062298889f84f94e699f6652e22a79c12
Author: Giovanni Campagna <gcampagna src gnome org>
Date:   Sun Dec 11 02:18:18 2011 +0100

    object: Introduce support for signals
    
    Introduce Gi.signal_new, which is a thin wrapper over g_signal_newv,
    and then make GObject.Class read a map of signal descriptions as
    parameters to the class.
    Also, use g_signal_override_class_closure to connect "on_foo" to
    the respective signals.

 gi/object.c                  |   95 +++++++++++++++++
 modules/overrides/GObject.js |   40 +++++++-
 test/js/testGObjectClass.js  |  236 ++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 369 insertions(+), 2 deletions(-)
---
diff --git a/gi/object.c b/gi/object.c
index 9decf0c..bccecb0 100644
--- a/gi/object.c
+++ b/gi/object.c
@@ -2060,6 +2060,9 @@ gjs_register_type(JSContext *cx,
 
     JS_SetPrivate(cx, object, priv);
 
+    gtype = INT_TO_JSVAL(instance_type);
+    JS_SET_RVAL(cx, vp, gtype);
+
     JS_EndRequest(cx);
 
     return JS_TRUE;
@@ -2099,6 +2102,92 @@ gjs_register_property(JSContext *cx,
 }
 
 static JSBool
+gjs_signal_new(JSContext *cx,
+               uintN      argc,
+               jsval     *vp)
+{
+    jsval *argv = JS_ARGV(cx, vp);
+    JSObject *obj;
+    ObjectInstance *priv;
+    gchar *signal_name = NULL;
+    GSignalAccumulator accumulator;
+    gint signal_id;
+    guint i, n_parameters;
+    GType *params;
+    JSBool ret;
+
+    if (argc != 6)
+        return JS_FALSE;
+
+    JS_BeginRequest(cx);
+
+    if (!gjs_string_to_utf8(cx, argv[1], &signal_name)) {
+        ret = JS_FALSE;
+        goto out;
+    }
+
+    obj = JSVAL_TO_OBJECT(argv[0]);
+    priv = priv_from_js(cx, obj);
+
+    /* we only support standard accumulators for now */
+    switch (JSVAL_TO_INT(argv[3])) {
+    case 1:
+        accumulator = g_signal_accumulator_first_wins;
+        break;
+    case 2:
+        accumulator = g_signal_accumulator_true_handled;
+        break;
+    case 0:
+    default:
+        accumulator = NULL;
+    }
+
+    if (accumulator == g_signal_accumulator_true_handled &&
+        JSVAL_TO_INT(argv[4]) != G_TYPE_BOOLEAN) {
+        gjs_throw (cx, "GObject.SignalAccumulator.TRUE_HANDLED can only be used with boolean signals");
+        ret = JS_FALSE;
+        goto out;
+    }
+
+    if (!JS_GetArrayLength(cx, JSVAL_TO_OBJECT(argv[5]), &n_parameters)) {
+        ret = JS_FALSE;
+        goto out;
+    }
+    params = g_newa(GType, n_parameters);
+    for (i = 0; i < n_parameters; i++) {
+        jsval gtype_val;
+        if (!JS_GetElement(cx, JSVAL_TO_OBJECT(argv[5]), i, &gtype_val) ||
+            !JSVAL_IS_OBJECT(gtype_val)) {
+            gjs_throw(cx, "Invalid signal parameter number %d", i);
+            ret = JS_FALSE;
+            goto out;
+        }
+
+        params[i] = gjs_gtype_get_actual_gtype(cx, JSVAL_TO_OBJECT(gtype_val));
+    }
+
+    signal_id = g_signal_newv(signal_name,
+                              priv->gtype,
+                              JSVAL_TO_INT(argv[2]), /* signal_flags */
+                              NULL, /* class closure */
+                              accumulator,
+                              NULL, /* accu_data */
+                              g_cclosure_marshal_generic,
+                              gjs_gtype_get_actual_gtype(cx, JSVAL_TO_OBJECT(argv[4])), /* return type */
+                              n_parameters,
+                              params);
+
+    JS_SET_RVAL(cx, vp, INT_TO_JSVAL(signal_id));
+    ret = JS_TRUE;
+
+ out:
+    JS_EndRequest(cx);
+
+    free (signal_name);
+    return ret;
+}
+
+static JSBool
 gjs_define_stuff(JSContext *context,
                     JSObject  *module_obj)
 {
@@ -2120,6 +2209,12 @@ gjs_define_stuff(JSContext *context,
                            2, GJS_MODULE_PROP_FLAGS))
         return JS_FALSE;
 
+    if (!JS_DefineFunction(context, module_obj,
+                           "signal_new",
+                           (JSNative)gjs_signal_new,
+                           6, GJS_MODULE_PROP_FLAGS))
+        return JS_FALSE;
+
     return JS_TRUE;
 }
 
diff --git a/modules/overrides/GObject.js b/modules/overrides/GObject.js
index 572aff1..f95435d 100644
--- a/modules/overrides/GObject.js
+++ b/modules/overrides/GObject.js
@@ -44,12 +44,41 @@ const GObjectMeta = new Lang.Class({
                 Gi.register_property(this.prototype, params.Properties[prop]);
             }
         }
+
+        if (params.Signals) {
+            for (let signalName in params.Signals) {
+                let obj = params.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(this.prototype, signal_name, flags, accumulator, rtype, paramtypes);
+                } catch(e) {
+                    throw new TypeError('Invalid signal ' + signal_name + ': ' + e.message);
+                }
+            }
+        }
         delete params.Properties;
+        delete params.Signals;
 
         for (let prop in params) {
             let value = this.prototype[prop];
-            if (typeof value === 'function' && prop.slice(0, 3) == 'do_') {
-                Gi.hook_up_vfunc(this.prototype, prop.slice(3), value);
+            if (typeof value === 'function') {
+                if (prop.slice(0, 3) == 'do_') {
+                    Gi.hook_up_vfunc(this.prototype, prop.slice(3), value);
+                } else if (prop.slice(0, 3) == 'on_') {
+                    let id = GObject.signal_lookup(prop.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();
+
+                            value.apply(emitter, argArray);
+                        });
+                    }
+                }
             }
         }
     },
@@ -173,4 +202,11 @@ function _init() {
         this._init.apply(this, arguments);
         return this;
     };
+
+    // fake enum for signal accumulators, keep in sync with gi/object.c
+    this.AccumulatorType = {
+        NONE: 0,
+        FIRST_WINS: 1,
+        TRUE_HANDLED: 2
+    };
 }
diff --git a/test/js/testGObjectClass.js b/test/js/testGObjectClass.js
new file mode 100644
index 0000000..48ae5b6
--- /dev/null
+++ b/test/js/testGObjectClass.js
@@ -0,0 +1,236 @@
+// application/javascript;version=1.8 -*- mode: js; indent-tabs-mode: nil -*-
+
+if (!('assertEquals' in this)) { /* allow running this test standalone */
+    imports.lang.copyPublicProperties(imports.jsUnit, this);
+    gjstestRun = function() { return imports.jsUnit.gjstestRun(window); };
+}
+
+const Lang = imports.lang;
+const GObject = imports.gi.GObject;
+const Gio = imports.gi.Gio;
+
+const MyObject = new GObject.Class({
+    Name: 'MyObject',
+    Properties: {
+        'readwrite': GObject.ParamSpec.string('readwrite', 'ParamReadwrite',
+                                              'A read write parameter',
+                                              GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE,
+                                              ''),
+        '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.READABLE | GObject.ParamFlags.WRITABLE | 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;
+    }
+});
+
+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);
+    }
+});
+printerr("almost");
+
+function testGObjectClass() {
+    let myInstance = new MyObject();
+
+    assertEquals('foo', myInstance.readwrite);
+    assertEquals('bar', myInstance.readonly);
+    assertEquals('default', myInstance.construct);
+
+    let myInstance2 = new MyObject({ readwrite: 'baz', construct: 'asdf' });
+
+    assertEquals('baz', myInstance2.readwrite);
+    assertEquals('bar', myInstance2.readonly);
+    assertEquals('asdf', myInstance2.construct);
+
+    // the following would (should) cause a CRITICAL:
+    // myInstance.readonly = 'val';
+    // myInstance.construct = 'val';
+}
+
+function testNotify() {
+    let myInstance = new MyObject();
+    let counter = 0;
+
+    myInstance.connect('notify::readonly', function(obj) {
+        if (obj.readonly == 'changed')
+            counter++;
+    });
+
+    myInstance.notify_prop();
+    myInstance.notify_prop();
+
+    assertEquals(2, counter);
+}
+
+function testSignals() {
+    let myInstance = new MyObject();
+    let ok = false;
+
+    myInstance.connect('empty', function() {
+        ok = true;
+    });
+    myInstance.emit_empty();
+
+    assertEquals(true, ok);
+    assertEquals(true, myInstance.empty_called);
+
+    let args = [ ];
+    myInstance.connect('minimal', function(emitter, one, two) {
+        args.push(one);
+        args.push(two);
+
+        return true;
+    });
+    myInstance.emit_minimal(7, 5);
+
+    assertEquals(7, args[0]);
+    assertEquals(5, args[1]);
+
+    ok = true;
+    myInstance.connect('full', function() {
+        ok = true;
+
+        return 42;
+    });
+    myInstance.connect('full', function() {
+        // this should never be called
+        ok = false;
+
+        return -1;
+    });
+    result = myInstance.emit_full();
+
+    assertEquals(true, ok);
+    assertEquals(42, result);
+
+    let stack = [ ];
+    myInstance.connect('run-last', function() {
+        stack.push(1);
+    });
+    myInstance.emit_run_last(function() {
+        stack.push(2);
+    });
+
+    assertEquals(1, stack[0]);
+    assertEquals(2, stack[1]);
+}
+
+function testSubclass() {
+    // test that we can inherit from something that's not
+    // GObject.Object and still get all the goodies of
+    // GObject.Class
+
+    let instance = new MyApplication({ application_id: 'org.gjs.Application' });
+    let v;
+
+    instance.connect('custom', function(app, num) {
+        v = num;
+    });
+
+    instance.emit_custom(73);
+    assertEquals(73, v);
+}
+
+gjstestRun();



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