File indexing completed on 2024-11-24 04:54:31
0001 /* 0002 SPDX-License-Identifier: LGPL-3.0-only OR GPL-2.0-or-later OR GPL-3.0-or-later 0003 */ 0004 0005 /**************************************************************************** 0006 ** 0007 ** Copyright (C) 2016 The Qt Company Ltd. 0008 ** Copyright (C) 2016 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com> 0009 ** Contact: https://www.qt.io/licensing/ 0010 ** 0011 ** This file is part of the QtWebChannel module of the Qt Toolkit. 0012 ** 0013 ** $QT_BEGIN_LICENSE:LGPL$ 0014 ** Commercial License Usage 0015 ** Licensees holding valid commercial Qt licenses may use this file in 0016 ** accordance with the commercial license agreement provided with the 0017 ** Software or, alternatively, in accordance with the terms contained in 0018 ** a written agreement between you and The Qt Company. For licensing terms 0019 ** and conditions see https://www.qt.io/terms-conditions. For further 0020 ** information use the contact form at https://www.qt.io/contact-us. 0021 ** 0022 ** GNU Lesser General Public License Usage 0023 ** Alternatively, this file may be used under the terms of the GNU Lesser 0024 ** General Public License version 3 as published by the Free Software 0025 ** Foundation and appearing in the file LICENSE.LGPL3 included in the 0026 ** packaging of this file. Please review the following information to 0027 ** ensure the GNU Lesser General Public License version 3 requirements 0028 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. 0029 ** 0030 ** GNU General Public License Usage 0031 ** Alternatively, this file may be used under the terms of the GNU 0032 ** General Public License version 2.0 or (at your option) the GNU General 0033 ** Public license version 3 or any later version approved by the KDE Free 0034 ** Qt Foundation. The licenses are as published by the Free Software 0035 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 0036 ** included in the packaging of this file. Please review the following 0037 ** information to ensure the GNU General Public License requirements will 0038 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and 0039 ** https://www.gnu.org/licenses/gpl-3.0.html. 0040 ** 0041 ** $QT_END_LICENSE$ 0042 ** 0043 ****************************************************************************/ 0044 0045 "use strict"; 0046 0047 var QWebChannelMessageTypes = { 0048 signal: 1, 0049 propertyUpdate: 2, 0050 init: 3, 0051 idle: 4, 0052 debug: 5, 0053 invokeMethod: 6, 0054 connectToSignal: 7, 0055 disconnectFromSignal: 8, 0056 setProperty: 9, 0057 response: 10, 0058 }; 0059 0060 var QWebChannel = function(transport, initCallback) 0061 { 0062 if (typeof transport !== "object" || typeof transport.send !== "function") { 0063 console.error("The QWebChannel expects a transport object with a send function and onmessage callback property." + 0064 " Given is: transport: " + typeof(transport) + ", transport.send: " + typeof(transport.send)); 0065 return; 0066 } 0067 0068 var channel = this; 0069 this.transport = transport; 0070 0071 this.send = function(data) 0072 { 0073 if (typeof(data) !== "string") { 0074 data = JSON.stringify(data); 0075 } 0076 channel.transport.send(data); 0077 } 0078 0079 this.transport.onmessage = function(message) 0080 { 0081 var data = message.data; 0082 if (typeof data === "string") { 0083 data = JSON.parse(data); 0084 } 0085 switch (data.type) { 0086 case QWebChannelMessageTypes.signal: 0087 channel.handleSignal(data); 0088 break; 0089 case QWebChannelMessageTypes.response: 0090 channel.handleResponse(data); 0091 break; 0092 case QWebChannelMessageTypes.propertyUpdate: 0093 channel.handlePropertyUpdate(data); 0094 break; 0095 default: 0096 console.error("invalid message received:", message.data); 0097 break; 0098 } 0099 } 0100 0101 this.execCallbacks = {}; 0102 this.execId = 0; 0103 this.exec = function(data, callback) 0104 { 0105 if (!callback) { 0106 // if no callback is given, send directly 0107 channel.send(data); 0108 return; 0109 } 0110 if (channel.execId === Number.MAX_VALUE) { 0111 // wrap 0112 channel.execId = Number.MIN_VALUE; 0113 } 0114 if (data.hasOwnProperty("id")) { 0115 console.error("Cannot exec message with property id: " + JSON.stringify(data)); 0116 return; 0117 } 0118 data.id = channel.execId++; 0119 channel.execCallbacks[data.id] = callback; 0120 channel.send(data); 0121 }; 0122 0123 this.objects = {}; 0124 0125 this.handleSignal = function(message) 0126 { 0127 var object = channel.objects[message.object]; 0128 if (object) { 0129 object.signalEmitted(message.signal, message.args); 0130 } else { 0131 console.warn("Unhandled signal: " + message.object + "::" + message.signal); 0132 } 0133 } 0134 0135 this.handleResponse = function(message) 0136 { 0137 if (!message.hasOwnProperty("id")) { 0138 console.error("Invalid response message received: ", JSON.stringify(message)); 0139 return; 0140 } 0141 channel.execCallbacks[message.id](message.data); 0142 delete channel.execCallbacks[message.id]; 0143 } 0144 0145 this.handlePropertyUpdate = function(message) 0146 { 0147 for (var i in message.data) { 0148 var data = message.data[i]; 0149 var object = channel.objects[data.object]; 0150 if (object) { 0151 object.propertyUpdate(data.signals, data.properties); 0152 } else { 0153 console.warn("Unhandled property update: " + data.object + "::" + data.signal); 0154 } 0155 } 0156 channel.exec({type: QWebChannelMessageTypes.idle}); 0157 } 0158 0159 this.debug = function(message) 0160 { 0161 channel.send({type: QWebChannelMessageTypes.debug, data: message}); 0162 }; 0163 0164 channel.exec({type: QWebChannelMessageTypes.init}, function(data) { 0165 for (var objectName in data) { 0166 var object = new QObject(objectName, data[objectName], channel); 0167 } 0168 // now unwrap properties, which might reference other registered objects 0169 for (var objectName in channel.objects) { 0170 channel.objects[objectName].unwrapProperties(); 0171 } 0172 if (initCallback) { 0173 initCallback(channel); 0174 } 0175 channel.exec({type: QWebChannelMessageTypes.idle}); 0176 }); 0177 }; 0178 0179 function QObject(name, data, webChannel) 0180 { 0181 this.__id__ = name; 0182 webChannel.objects[name] = this; 0183 0184 // List of callbacks that get invoked upon signal emission 0185 this.__objectSignals__ = {}; 0186 0187 // Cache of all properties, updated when a notify signal is emitted 0188 this.__propertyCache__ = {}; 0189 0190 var object = this; 0191 0192 // ---------------------------------------------------------------------- 0193 0194 this.unwrapQObject = function(response) 0195 { 0196 if (response instanceof Array) { 0197 // support list of objects 0198 var ret = new Array(response.length); 0199 for (var i = 0; i < response.length; ++i) { 0200 ret[i] = object.unwrapQObject(response[i]); 0201 } 0202 return ret; 0203 } 0204 if (!response 0205 || !response["__QObject*__"] 0206 || response.id === undefined) { 0207 return response; 0208 } 0209 0210 var objectId = response.id; 0211 if (webChannel.objects[objectId]) 0212 return webChannel.objects[objectId]; 0213 0214 if (!response.data) { 0215 console.error("Cannot unwrap unknown QObject " + objectId + " without data."); 0216 return; 0217 } 0218 0219 var qObject = new QObject( objectId, response.data, webChannel ); 0220 qObject.destroyed.connect(function() { 0221 if (webChannel.objects[objectId] === qObject) { 0222 delete webChannel.objects[objectId]; 0223 // reset the now deleted QObject to an empty {} object 0224 // just assigning {} though would not have the desired effect, but the 0225 // below also ensures all external references will see the empty map 0226 // NOTE: this detour is necessary to workaround QTBUG-40021 0227 var propertyNames = []; 0228 for (var propertyName in qObject) { 0229 propertyNames.push(propertyName); 0230 } 0231 for (var idx in propertyNames) { 0232 delete qObject[propertyNames[idx]]; 0233 } 0234 } 0235 }); 0236 // here we are already initialized, and thus must directly unwrap the properties 0237 qObject.unwrapProperties(); 0238 return qObject; 0239 } 0240 0241 this.unwrapProperties = function() 0242 { 0243 for (var propertyIdx in object.__propertyCache__) { 0244 object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]); 0245 } 0246 } 0247 0248 function addSignal(signalData, isPropertyNotifySignal) 0249 { 0250 var signalName = signalData[0]; 0251 var signalIndex = signalData[1]; 0252 object[signalName] = { 0253 connect: function(callback) { 0254 if (typeof(callback) !== "function") { 0255 console.error("Bad callback given to connect to signal " + signalName); 0256 return; 0257 } 0258 0259 object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; 0260 object.__objectSignals__[signalIndex].push(callback); 0261 0262 if (!isPropertyNotifySignal && signalName !== "destroyed") { 0263 // only required for "pure" signals, handled separately for properties in propertyUpdate 0264 // also note that we always get notified about the destroyed signal 0265 webChannel.exec({ 0266 type: QWebChannelMessageTypes.connectToSignal, 0267 object: object.__id__, 0268 signal: signalIndex 0269 }); 0270 } 0271 }, 0272 disconnect: function(callback) { 0273 if (typeof(callback) !== "function") { 0274 console.error("Bad callback given to disconnect from signal " + signalName); 0275 return; 0276 } 0277 object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || []; 0278 var idx = object.__objectSignals__[signalIndex].indexOf(callback); 0279 if (idx === -1) { 0280 console.error("Cannot find connection of signal " + signalName + " to " + callback.name); 0281 return; 0282 } 0283 object.__objectSignals__[signalIndex].splice(idx, 1); 0284 if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) { 0285 // only required for "pure" signals, handled separately for properties in propertyUpdate 0286 webChannel.exec({ 0287 type: QWebChannelMessageTypes.disconnectFromSignal, 0288 object: object.__id__, 0289 signal: signalIndex 0290 }); 0291 } 0292 } 0293 }; 0294 } 0295 0296 /** 0297 * Invokes all callbacks for the given signalname. Also works for property notify callbacks. 0298 */ 0299 function invokeSignalCallbacks(signalName, signalArgs) 0300 { 0301 var connections = object.__objectSignals__[signalName]; 0302 if (connections) { 0303 connections.forEach(function(callback) { 0304 callback.apply(callback, signalArgs); 0305 }); 0306 } 0307 } 0308 0309 this.propertyUpdate = function(signals, propertyMap) 0310 { 0311 // update property cache 0312 for (var propertyIndex in propertyMap) { 0313 var propertyValue = propertyMap[propertyIndex]; 0314 object.__propertyCache__[propertyIndex] = propertyValue; 0315 } 0316 0317 for (var signalName in signals) { 0318 // Invoke all callbacks, as signalEmitted() does not. This ensures the 0319 // property cache is updated before the callbacks are invoked. 0320 invokeSignalCallbacks(signalName, signals[signalName]); 0321 } 0322 } 0323 0324 this.signalEmitted = function(signalName, signalArgs) 0325 { 0326 invokeSignalCallbacks(signalName, this.unwrapQObject(signalArgs)); 0327 } 0328 0329 function addMethod(methodData) 0330 { 0331 var methodName = methodData[0]; 0332 var methodIdx = methodData[1]; 0333 object[methodName] = function() { 0334 var args = []; 0335 var callback; 0336 for (var i = 0; i < arguments.length; ++i) { 0337 var argument = arguments[i]; 0338 if (typeof argument === "function") 0339 callback = argument; 0340 else if (argument instanceof QObject && webChannel.objects[argument.__id__] !== undefined) 0341 args.push({ 0342 "id": argument.__id__ 0343 }); 0344 else 0345 args.push(argument); 0346 } 0347 0348 webChannel.exec({ 0349 "type": QWebChannelMessageTypes.invokeMethod, 0350 "object": object.__id__, 0351 "method": methodIdx, 0352 "args": args 0353 }, function(response) { 0354 if (response !== undefined) { 0355 var result = object.unwrapQObject(response); 0356 if (callback) { 0357 (callback)(result); 0358 } 0359 } 0360 }); 0361 }; 0362 } 0363 0364 function bindGetterSetter(propertyInfo) 0365 { 0366 var propertyIndex = propertyInfo[0]; 0367 var propertyName = propertyInfo[1]; 0368 var notifySignalData = propertyInfo[2]; 0369 // initialize property cache with current value 0370 // NOTE: if this is an object, it is not directly unwrapped as it might 0371 // reference other QObject that we do not know yet 0372 object.__propertyCache__[propertyIndex] = propertyInfo[3]; 0373 0374 if (notifySignalData) { 0375 if (notifySignalData[0] === 1) { 0376 // signal name is optimized away, reconstruct the actual name 0377 notifySignalData[0] = propertyName + "Changed"; 0378 } 0379 addSignal(notifySignalData, true); 0380 } 0381 0382 Object.defineProperty(object, propertyName, { 0383 configurable: true, 0384 get: function () { 0385 var propertyValue = object.__propertyCache__[propertyIndex]; 0386 if (propertyValue === undefined) { 0387 // This shouldn't happen 0388 console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__); 0389 } 0390 0391 return propertyValue; 0392 }, 0393 set: function(value) { 0394 if (value === undefined) { 0395 console.warn("Property setter for " + propertyName + " called with undefined value!"); 0396 return; 0397 } 0398 object.__propertyCache__[propertyIndex] = value; 0399 var valueToSend = value; 0400 if (valueToSend instanceof QObject && webChannel.objects[valueToSend.__id__] !== undefined) 0401 valueToSend = { "id": valueToSend.__id__ }; 0402 webChannel.exec({ 0403 "type": QWebChannelMessageTypes.setProperty, 0404 "object": object.__id__, 0405 "property": propertyIndex, 0406 "value": valueToSend 0407 }); 0408 } 0409 }); 0410 0411 } 0412 0413 // ---------------------------------------------------------------------- 0414 0415 data.methods.forEach(addMethod); 0416 0417 data.properties.forEach(bindGetterSetter); 0418 0419 data.signals.forEach(function(signal) { addSignal(signal, false); }); 0420 0421 for (var name in data.enums) { 0422 object[name] = data.enums[name]; 0423 } 0424 } 0425 0426 //required for use with nodejs 0427 if (typeof module === 'object') { 0428 module.exports = { 0429 QWebChannel: QWebChannel 0430 }; 0431 }