[gjs/wip/ptomato/classes] WIP - GObject class stuff
- From: Philip Chimento <pchimento src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gjs/wip/ptomato/classes] WIP - GObject class stuff
- Date: Thu, 27 Jul 2017 22:33:24 +0000 (UTC)
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]