| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com> |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the QtWebChannel module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| "use strict"; |
| |
| var QWebChannelMessageTypes = { |
| signal: 1, |
| propertyUpdate: 2, |
| init: 3, |
| idle: 4, |
| debug: 5, |
| invokeMethod: 6, |
| connectToSignal: 7, |
| disconnectFromSignal: 8, |
| setProperty: 9, |
| response: 10, |
| }; |
| |
| var QWebChannel = function(transport, initCallback) |
| { |
| if (typeof transport !== "object" || typeof transport.send !== "function") { |
| console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." + |
| " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send)); |
| return; |
| } |
| |
| var channel = this; |
| this.transport = transport; |
| |
| this.send = function(data) |
| { |
| if (typeof(data) !== "string") { |
| data = JSON.stringify(data); |
| } |
| channel.transport.send(data); |
| } |
| |
| this.transport.onmessage = function(message) |
| { |
| var data = message.data; |
| if (typeof data === "string") { |
| data = JSON.parse(data); |
| } |
| switch (data.type) { |
| case QWebChannelMessageTypes.signal: |
| channel.handleSignal(data); |
| break; |
| case QWebChannelMessageTypes.response: |
| channel.handleResponse(data); |
| break; |
| case QWebChannelMessageTypes.propertyUpdate: |
| channel.handlePropertyUpdate(data); |
| break; |
| default: |
| console.error("invalid message received:", message.data); |
| break; |
| } |
| } |
| |
| this.execCallbacks = {}; |
| this.execId = 0; |
| this.exec = function(data, callback) |
| { |
| if (!callback) { |
| // if no callback is given, send directly |
| channel.send(data); |
| return; |
| } |
| if (channel.execId === Number.MAX_VALUE) { |
| // wrap |
| channel.execId = Number.MIN_VALUE; |
| } |
| if (data.hasOwnProperty("id")) { |
| console.error("Cannot exec message with property id: " + JSON.stringify(data)); |
| return; |
| } |
| data.id = channel.execId++; |
| channel.execCallbacks[data.id] = callback; |
| channel.send(data); |
| }; |
| |
| this.objects = {}; |
| |
| this.handleSignal = function(message) |
| { |
| var object = channel.objects[message.object]; |
| if (object) { |
| object.signalEmitted(message.signal, message.args); |
| } else { |
| console.warn("Unhandled signal: " + message.object + "::" + message.signal); |
| } |
| } |
| |
| this.handleResponse = function(message) |
| { |
| if (!message.hasOwnProperty("id")) { |
| console.error("Invalid response message received: ", JSON.stringify(message)); |
| return; |
| } |
| channel.execCallbacks[message.id](message.data); |
| delete channel.execCallbacks[message.id]; |
| } |
| |
| this.handlePropertyUpdate = function(message) |
| { |
| message.data.forEach(data => { |
| var object = channel.objects[data.object]; |
| if (object) { |
| object.propertyUpdate(data.signals, data.properties); |
| } else { |
| console.warn("Unhandled property update: " + data.object + "::" + data.signal); |
| } |
| }); |
| channel.exec({type: QWebChannelMessageTypes.idle}); |
| } |
| |
| this.debug = function(message) |
| { |
| channel.send({type: QWebChannelMessageTypes.debug, data: message}); |
| }; |
| |
| channel.exec({type: QWebChannelMessageTypes.init}, function(data) { |
| for (const objectName of Object.keys(data)) { |
| new QObject(objectName, data[objectName], channel); |
| } |
| |
| // now unwrap properties, which might reference other registered objects |
| for (const objectName of Object.keys(channel.objects)) { |
| channel.objects[objectName].unwrapProperties(); |
| } |
| |
| if (initCallback) { |
| initCallback(channel); |
| } |
| channel.exec({type: QWebChannelMessageTypes.idle}); |
| }); |
| }; |
| |
| function QObject(name, data, webChannel) |
| { |
| this.__id__ = name; |
| webChannel.objects[name] = this; |
| |
| // List of callbacks that get invoked upon signal emission |
| this.__objectSignals__ = {}; |
| |
| // Cache of all properties, updated when a notify signal is emitted |
| this.__propertyCache__ = {}; |
| |
| var object = this; |
| |
| // ---------------------------------------------------------------------- |
| |
| this.unwrapQObject = function(response) |
| { |
| if (response instanceof Array) { |
| // support list of objects |
| return response.map(qobj => object.unwrapQObject(qobj)) |
| } |
| if (!(response instanceof Object)) |
| return response; |
| |
| if (!response["__QObject*__"] || response.id === undefined) { |
| var jObj = {}; |
| for (const propName of Object.keys(response)) { |
| jObj[propName] = object.unwrapQObject(response[propName]); |
| } |
| return jObj; |
| } |
| |
| var objectId = response.id; |
| if (webChannel.objects[objectId]) |
| return webChannel.objects[objectId]; |
| |
| if (!response.data) { |
| console.error("Cannot unwrap unknown QObject " + objectId + " without data."); |
| return; |
| } |
| |
| var qObject = new QObject( objectId, response.data, webChannel ); |
| qObject.destroyed.connect(function() { |
| if (webChannel.objects[objectId] === qObject) { |
| delete webChannel.objects[objectId]; |
| // reset the now deleted QObject to an empty {} object |
| // just assigning {} though would not have the desired effect, but the |
| // below also ensures all external references will see the empty map |
| // NOTE: this detour is necessary to workaround QTBUG-40021 |
| Object.keys(qObject).forEach(name => delete qObject[name]); |
| } |
| }); |
| // here we are already initialized, and thus must directly unwrap the properties |
| qObject.unwrapProperties(); |
| return qObject; |
| } |
| |
| this.unwrapProperties = function() |
| { |
| for (const propertyIdx of Object.keys(object.__propertyCache__)) { |
| object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]); |
| } |
| } |
| |
| function addSignal(signalData, isPropertyNotifySignal) |
| { |
| var signalName = signalData[0]; |
| var signalIndex = signalData[1]; |
| object[signalName] = { |
| connect: function(callback) { |
| if (typeof(callback) !== "function") { |
| console.error("Bad callback given to connect to signal " + signalName); |
| return; |
| } |
| |
| object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; |
| object.__objectSignals__[signalIndex].push(callback); |
| |
| // only required for "pure" signals, handled separately for properties in propertyUpdate |
| if (isPropertyNotifySignal) |
| return; |
| |
| // also note that we always get notified about the destroyed signal |
| if (signalName === "destroyed" || signalName === "destroyed()" || signalName === "destroyed(QObject*)") |
| return; |
| |
| // and otherwise we only need to be connected only once |
| if (object.__objectSignals__[signalIndex].length == 1) { |
| webChannel.exec({ |
| type: QWebChannelMessageTypes.connectToSignal, |
| object: object.__id__, |
| signal: signalIndex |
| }); |
| } |
| }, |
| disconnect: function(callback) { |
| if (typeof(callback) !== "function") { |
| console.error("Bad callback given to disconnect from signal " + signalName); |
| return; |
| } |
| object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; |
| var idx = object.__objectSignals__[signalIndex].indexOf(callback); |
| if (idx === -1) { |
| console.error("Cannot find connection of signal " + signalName + " to " + callback.name); |
| return; |
| } |
| object.__objectSignals__[signalIndex].splice(idx, 1); |
| if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) { |
| // only required for "pure" signals, handled separately for properties in propertyUpdate |
| webChannel.exec({ |
| type: QWebChannelMessageTypes.disconnectFromSignal, |
| object: object.__id__, |
| signal: signalIndex |
| }); |
| } |
| } |
| }; |
| } |
| |
| /** |
| * Invokes all callbacks for the given signalname. Also works for property notify callbacks. |
| */ |
| function invokeSignalCallbacks(signalName, signalArgs) |
| { |
| var connections = object.__objectSignals__[signalName]; |
| if (connections) { |
| connections.forEach(function(callback) { |
| callback.apply(callback, signalArgs); |
| }); |
| } |
| } |
| |
| this.propertyUpdate = function(signals, propertyMap) |
| { |
| // update property cache |
| for (const propertyIndex of Object.keys(propertyMap)) { |
| var propertyValue = propertyMap[propertyIndex]; |
| object.__propertyCache__[propertyIndex] = this.unwrapQObject(propertyValue); |
| } |
| |
| for (const signalName of Object.keys(signals)) { |
| // Invoke all callbacks, as signalEmitted() does not. This ensures the |
| // property cache is updated before the callbacks are invoked. |
| invokeSignalCallbacks(signalName, signals[signalName]); |
| } |
| } |
| |
| this.signalEmitted = function(signalName, signalArgs) |
| { |
| invokeSignalCallbacks(signalName, this.unwrapQObject(signalArgs)); |
| } |
| |
| function addMethod(methodData) |
| { |
| var methodName = methodData[0]; |
| var methodIdx = methodData[1]; |
| |
| // Fully specified methods are invoked by id, others by name for host-side overload resolution |
| var invokedMethod = methodName[methodName.length - 1] === ')' ? methodIdx : methodName |
| |
| object[methodName] = function() { |
| var args = []; |
| var callback; |
| var errCallback; |
| for (var i = 0; i < arguments.length; ++i) { |
| var argument = arguments[i]; |
| if (typeof argument === "function") |
| callback = argument; |
| else if (argument instanceof QObject && webChannel.objects[argument.__id__] !== undefined) |
| args.push({ |
| "id": argument.__id__ |
| }); |
| else |
| args.push(argument); |
| } |
| |
| var result; |
| // during test, webChannel.exec synchronously calls the callback |
| // therefore, the promise must be constucted before calling |
| // webChannel.exec to ensure the callback is set up |
| if (!callback && (typeof(Promise) === 'function')) { |
| result = new Promise(function(resolve, reject) { |
| callback = resolve; |
| errCallback = reject; |
| }); |
| } |
| |
| webChannel.exec({ |
| "type": QWebChannelMessageTypes.invokeMethod, |
| "object": object.__id__, |
| "method": invokedMethod, |
| "args": args |
| }, function(response) { |
| if (response !== undefined) { |
| var result = object.unwrapQObject(response); |
| if (callback) { |
| (callback)(result); |
| } |
| } else if (errCallback) { |
| (errCallback)(); |
| } |
| }); |
| |
| return result; |
| }; |
| } |
| |
| function bindGetterSetter(propertyInfo) |
| { |
| var propertyIndex = propertyInfo[0]; |
| var propertyName = propertyInfo[1]; |
| var notifySignalData = propertyInfo[2]; |
| // initialize property cache with current value |
| // NOTE: if this is an object, it is not directly unwrapped as it might |
| // reference other QObject that we do not know yet |
| object.__propertyCache__[propertyIndex] = propertyInfo[3]; |
| |
| if (notifySignalData) { |
| if (notifySignalData[0] === 1) { |
| // signal name is optimized away, reconstruct the actual name |
| notifySignalData[0] = propertyName + "Changed"; |
| } |
| addSignal(notifySignalData, true); |
| } |
| |
| Object.defineProperty(object, propertyName, { |
| configurable: true, |
| get: function () { |
| var propertyValue = object.__propertyCache__[propertyIndex]; |
| if (propertyValue === undefined) { |
| // This shouldn't happen |
| console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__); |
| } |
| |
| return propertyValue; |
| }, |
| set: function(value) { |
| if (value === undefined) { |
| console.warn("Property setter for " + propertyName + " called with undefined value!"); |
| return; |
| } |
| object.__propertyCache__[propertyIndex] = value; |
| var valueToSend = value; |
| if (valueToSend instanceof QObject && webChannel.objects[valueToSend.__id__] !== undefined) |
| valueToSend = { "id": valueToSend.__id__ }; |
| webChannel.exec({ |
| "type": QWebChannelMessageTypes.setProperty, |
| "object": object.__id__, |
| "property": propertyIndex, |
| "value": valueToSend |
| }); |
| } |
| }); |
| |
| } |
| |
| // ---------------------------------------------------------------------- |
| |
| data.methods.forEach(addMethod); |
| |
| data.properties.forEach(bindGetterSetter); |
| |
| data.signals.forEach(function(signal) { addSignal(signal, false); }); |
| |
| Object.assign(object, data.enums); |
| } |
| |
| //required for use with nodejs |
| if (typeof module === 'object') { |
| module.exports = { |
| QWebChannel: QWebChannel |
| }; |
| } |