UNPKG

node-gtk

Version:

GNOME Gtk+ bindings for NodeJS

518 lines (416 loc) 13.1 kB
/* * bootstrap.js */ const camelCase = require('lodash.camelcase') const internal = require('./native.js') // The bootstrap from C here contains functions and methods for each object, // namespaced with underscores. See gi.cc for more information. const GI = internal.Bootstrap(); const BaseClass = internal.GetBaseClass() const moduleCache = internal.GetModuleCache(); module.exports = { GI, makeInfo, getInfoName, } // The GIRepository API is fairly poor, and contains methods on classes, // methods on objects, and what should be methods interpreted as functions, // because the scanner does not interpret methods on typedefs correctly. // We extend this bootstrap'd repo to define all flags / enums, which // are all we need to start declaring objects. bootstrapGIRepository(GI) function bootstrapGIRepository(GI) { const repo = GI.Repository_get_default() const ns = 'GIRepository' // First, grab InfoType so we can find enums / flags. const InfoType = makeEnum(GI.Repository_find_by_name.call(repo, ns, 'InfoType')) // Now, define all enums / flags. const nInfos = GI.Repository_get_n_infos.call(repo, ns) for (let i = 0; i < nInfos; i++) { const info = GI.Repository_get_info.call(repo, ns, i) const name = GI.BaseInfo_get_name.call(info) const type = GI.BaseInfo_get_type.call(info) if (type === InfoType.ENUM || type === InfoType.FLAGS) GI[name] = makeEnum(info) } } // We also define GObject#on and friends, to keep NodeJS EventEmitter // semantics. They are implemented in terms of the native .connect() // and .disconnect() methods. extendBaseClass(BaseClass) function extendBaseClass(BaseClass) { BaseClass.prototype.on = function on(event, callback, after) { defineListeners(this) if (!this._listeners.has(event)) this._listeners.set(event, new WeakMap()) const fnMap = this._listeners.get(event) const handlerID = this.connect(event, callback, after) if (handlerID === 0) { throw new Error(`Could not connect to signal ${event}`) } fnMap.set(callback, handlerID) return this } BaseClass.prototype.off = function off(event, callback) { defineListeners(this) if (!this._listeners.has(event)) return const fnMap = this._listeners.get(event) if (!fnMap.has(callback)) return const handlerID = fnMap.get(callback) this.disconnect(handlerID) fnMap.delete(callback) return this } BaseClass.prototype.once = function once(event, callback, after) { const newCallback = (...args) => { callback(...args) this.off(event, newCallback) } this.on(event, newCallback, after) return this } BaseClass.prototype.addEventListener = BaseClass.prototype.on BaseClass.prototype.removeEventListener = BaseClass.prototype.off function defineListeners(object) { if (object._listeners !== undefined) return Object.defineProperty(object, '_listeners', { value: new Map(), enumerable: false, }) } } function define(object, description) { const target = (description.isMethod || description.isProperty) ? object.prototype : object Object.defineProperty(target, description.name, description.property) } function getFunctionDescription(info) { const fn = internal.MakeFunction(info); const name = getInfoName(info); const flags = GI.function_info_get_flags(info); const isMethod = ((flags & GI.FunctionInfoFlags.IS_METHOD) != 0 && (flags & GI.FunctionInfoFlags.IS_CONSTRUCTOR) == 0); return { name, isMethod, property: { configurable: true, writable: true, value: fn } } } function getPropertyDescription(info) { const propertyName = getName(info) const name = getInfoName(info) const property = { configurable: true, enumerable: true, get: propertyGetter(propertyName), set: propertySetter(propertyName), } return {name, property, isProperty: true} } function getConstantDescription(info) { const name = getInfoName(info) const property = { configurable: false, writable: false, enumerable: true, value: makeConstant(info) } return {name, property} } function addFunction(object, info) { define(object, getFunctionDescription(info)) } function fieldGetter(fieldInfo) { return function () { return internal.StructFieldGetter(this, fieldInfo); }; } function fieldSetter(fieldInfo) { return function (value) { return internal.StructFieldSetter(this, fieldInfo, value); }; } function addField(object, fieldInfo) { const flags = GI.field_info_get_flags(fieldInfo) const writable = (flags & GI.FieldInfoFlags.WRITABLE) !== 0 const readable = (flags & GI.FieldInfoFlags.READABLE) !== 0 const name = getInfoName(fieldInfo) Object.defineProperty(object, name, { configurable: true, enumerable: readable, get: readable ? fieldGetter(fieldInfo) : undefined, set: writable ? fieldSetter(fieldInfo) : undefined }) } function propertyGetter(propertyName) { return function () { return internal.ObjectPropertyGetter(this, propertyName); }; } function propertySetter(propertyName) { return function (value) { return internal.ObjectPropertySetter(this, propertyName, value); }; } function addProperty(object, info) { define(object, getPropertyDescription(info)) } function addConstant(object, info) { define(object, getConstantDescription(info)) } function makeConstant(info) { return internal.GetConstantValue(info) } function makeFunction(info) { return internal.MakeFunction(info) } function makeEnum(info) { const object = {} const nValues = GI.enum_info_get_n_values(info); for (let i = 0; i < nValues; i++) { const valueInfo = GI.enum_info_get_value(info, i); const valueName = getName(valueInfo).toUpperCase() const value = GI.value_info_get_value(valueInfo) Object.defineProperty(object, valueName, { configurable: true, enumerable: true, value: value, }) Object.defineProperty(object, value, { configurable: true, enumerable: false, value: valueName, }) } const nMethods = GI.enum_info_get_n_methods(info) for (let i = 0; i < nMethods; i++) { const methodInfo = GI.enum_info_get_method(info, i) const methodName = camelCase(getName(methodInfo)) const method = internal.MakeFunction(methodInfo) Object.defineProperty(object, methodName, { configurable: true, writable: true, value: method }) } return object; } function makeObject(info) { const constructor = tryCall(internal.MakeObjectClass, info); if (constructor instanceof Error) { console.warn(constructor.message) return null } forLoopFn(info, GI.object_info_get_n_properties, GI.object_info_get_property, (propertyInfo) => { addProperty(constructor, propertyInfo) }) forLoopFn(info, GI.object_info_get_n_methods, GI.object_info_get_method, (methodInfo) => { addFunction(constructor, methodInfo); }) forLoopFn(info, GI.object_info_get_n_constants, GI.object_info_get_constant, (constantInfo) => { addConstant(constructor, constantInfo) }) /* * Define interface-implemented things */ forLoopFn(info, GI.object_info_get_n_interfaces, GI.object_info_get_interface, (interfaceInfo) => { const interface_ = getInterface(interfaceInfo) interface_._properties.forEach(description => { define(constructor, description) }) interface_._methods.forEach(description => { define(constructor, description) }) interface_._constants.forEach(description => { define(constructor, description) }) }) return constructor; } function makeBoxed(info) { if (getType(info) == GI.InfoType.UNION) { return makeUnion(info) } else { return makeStruct(info) } } function makeUnion(info) { const constructor = internal.MakeBoxedClass(info); const nMethods = GI.union_info_get_n_methods(info); for (let i = 0; i < nMethods; i++) { const methodInfo = GI.union_info_get_method(info, i); addFunction(constructor, methodInfo); } const nFields = GI.union_info_get_n_fields(info); for (let i = 0; i < nFields; i++) { const fieldInfo = GI.union_info_get_field(info, i); addField(constructor.prototype, fieldInfo); } return constructor } function makeStruct(info) { const constructor = internal.MakeBoxedClass(info); const nMethods = GI.struct_info_get_n_methods(info); for (let i = 0; i < nMethods; i++) { var methodInfo = GI.struct_info_get_method(info, i); addFunction(constructor, methodInfo); } const nFields = GI.struct_info_get_n_fields(info); for (let i = 0; i < nFields; i++) { const fieldInfo = GI.struct_info_get_field(info, i); addField(constructor.prototype, fieldInfo); } return constructor } function makeInterface(info) { const constructor = Object.values({ [getInfoName(info)]: function () { throw new Error('Cannot instantiate Interface (abstract type)') } })[0] /* * We need both to define things on the prototype for cases where we need * to force-assign the prototype of a returned object (eg Gtk4.FileChooserDialog), * and on `_properties/_methods/_constants` for `makeObject()` to re-use them. */ constructor._properties = [] forLoopFn(info, GI.interface_info_get_n_properties, GI.interface_info_get_property, (propertyInfo) => { constructor._properties.push(getPropertyDescription(propertyInfo)) addProperty(constructor, propertyInfo) }) constructor._methods = [] forLoopFn(info, GI.interface_info_get_n_methods, GI.interface_info_get_method, (methodInfo) => { constructor._methods.push(getFunctionDescription(methodInfo)) addFunction(constructor, methodInfo) }) constructor._constants = [] forLoopFn(info, GI.interface_info_get_n_constants, GI.interface_info_get_constant, (constantInfo) => { constructor._constants.push(getConstantDescription(constantInfo)) addConstant(constructor, constantInfo) }) return constructor; } function makeInfo(info) { const type = getType(info); /* A return value of `undefined` will not be shown * in the module properties. A value of `null` will. */ switch (type) { case GI.InfoType.FUNCTION: return makeFunction(info); case GI.InfoType.STRUCT: if (GI.struct_info_is_gtype_struct(info)) return undefined; // fallthrough case GI.InfoType.BOXED: case GI.InfoType.UNION: return makeBoxed(info); case GI.InfoType.ENUM: case GI.InfoType.FLAGS: return makeEnum(info); case GI.InfoType.OBJECT: return makeObject(info); case GI.InfoType.INTERFACE: return makeInterface(info); case GI.InfoType.CONSTANT: return makeConstant(info); // INVALID_0 // SIGNAL // VFUNC // PROPERTY // FIELD // ARG // TYPE // UNRESOLVED // INVALID // CALLBACK } } function getInfoName(info) { const type = getType(info) const name = getName(info) switch (type) { case GI.InfoType.FUNCTION: case GI.InfoType.VFUNC: return camelCase(name) // lowerCamelCase (snake_case by default) case GI.InfoType.FIELD: return camelCase(name) // lowerCamelCase (snake_case by default) case GI.InfoType.PROPERTY: return camelCase(name) // lowerCamelCase (dash-case by default) case GI.InfoType.STRUCT: case GI.InfoType.BOXED: case GI.InfoType.UNION: return name // UpperCamelCase case GI.InfoType.ENUM: case GI.InfoType.FLAGS: return name // UpperCamelCase case GI.InfoType.OBJECT: return name // UpperCamelCase case GI.InfoType.INTERFACE: return name // UpperCamelCase case GI.InfoType.CONSTANT: return name // ALL_CAPS by default case GI.InfoType.VALUE: // enum's value return name.toUpperCase() // ALL_CAPS // INVALID_0 // SIGNAL // ARG // TYPE // UNRESOLVED // INVALID // CALLBACK } } function getInterface(info) { const name = getInfoName(info) const namespace = getNamespace(info) if (moduleCache[namespace][name]) return moduleCache[namespace][name] moduleCache[namespace][name] = makeInterface(info) return moduleCache[namespace][name] } /* * Helpers */ function getName(info) { return GI.BaseInfo_get_name.call(info); } function getNamespace(info) { return GI.BaseInfo_get_namespace.call(info); } function getType(info) { return GI.BaseInfo_get_type.call(info); } function isDeprecated(info) { return GI.BaseInfo_is_deprecated.call(info) } function deprecationWrapper(info, fn) { const wrapper = Object.values({ [fn.name]: function () { console.log(`[node-gtk]: ${getInfoName(info)} is deprecated`) return fn.apply(this, arguments) } })[0] return wrapper } function forLoopFn(info, lengthFn, itemFn, fn) { const length = lengthFn(info) for (let i = 0; i < length; i++) { const item = itemFn(info, i) fn(item, i) } } function tryCall(fn, ...args) { try { return fn.apply(null, args) } catch (e) { return e } }