qwebchannel.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. /****************************************************************************
  2. **
  3. ** Copyright (C) 2016 The Qt Company Ltd.
  4. ** Copyright (C) 2016 Klar채lvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
  5. ** Contact: https://www.qt.io/licensing/
  6. **
  7. ** This file is part of the QtWebChannel module of the Qt Toolkit.
  8. **
  9. ** $QT_BEGIN_LICENSE:LGPL$
  10. ** Commercial License Usage
  11. ** Licensees holding valid commercial Qt licenses may use this file in
  12. ** accordance with the commercial license agreement provided with the
  13. ** Software or, alternatively, in accordance with the terms contained in
  14. ** a written agreement between you and The Qt Company. For licensing terms
  15. ** and conditions see https://www.qt.io/terms-conditions. For further
  16. ** information use the contact form at https://www.qt.io/contact-us.
  17. **
  18. ** GNU Lesser General Public License Usage
  19. ** Alternatively, this file may be used under the terms of the GNU Lesser
  20. ** General Public License version 3 as published by the Free Software
  21. ** Foundation and appearing in the file LICENSE.LGPL3 included in the
  22. ** packaging of this file. Please review the following information to
  23. ** ensure the GNU Lesser General Public License version 3 requirements
  24. ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
  25. **
  26. ** GNU General Public License Usage
  27. ** Alternatively, this file may be used under the terms of the GNU
  28. ** General Public License version 2.0 or (at your option) the GNU General
  29. ** Public license version 3 or any later version approved by the KDE Free
  30. ** Qt Foundation. The licenses are as published by the Free Software
  31. ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
  32. ** included in the packaging of this file. Please review the following
  33. ** information to ensure the GNU General Public License requirements will
  34. ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
  35. ** https://www.gnu.org/licenses/gpl-3.0.html.
  36. **
  37. ** $QT_END_LICENSE$
  38. **
  39. ****************************************************************************/
  40. 'use strict';
  41. var QWebChannelMessageTypes = {
  42. signal: 1,
  43. propertyUpdate: 2,
  44. init: 3,
  45. idle: 4,
  46. debug: 5,
  47. invokeMethod: 6,
  48. connectToSignal: 7,
  49. disconnectFromSignal: 8,
  50. setProperty: 9,
  51. response: 10
  52. };
  53. var QWebChannel = function(transport, initCallback) {
  54. if (typeof transport !== 'object' || typeof transport.send !== 'function') {
  55. console.error(
  56. 'The QWebChannel expects a transport object with a send function and onmessage callback property.' +
  57. ' Given is: transport: ' +
  58. typeof transport +
  59. ', transport.send: ' +
  60. typeof transport.send
  61. );
  62. return;
  63. }
  64. var channel = this;
  65. this.transport = transport;
  66. this.send = function(data) {
  67. if (typeof data !== 'string') {
  68. data = JSON.stringify(data);
  69. }
  70. channel.transport.send(data);
  71. };
  72. this.transport.onmessage = function(message) {
  73. var data = message.data;
  74. if (typeof data === 'string') {
  75. data = JSON.parse(data);
  76. }
  77. switch (data.type) {
  78. case QWebChannelMessageTypes.signal:
  79. channel.handleSignal(data);
  80. break;
  81. case QWebChannelMessageTypes.response:
  82. channel.handleResponse(data);
  83. break;
  84. case QWebChannelMessageTypes.propertyUpdate:
  85. channel.handlePropertyUpdate(data);
  86. break;
  87. default:
  88. console.error('invalid message received:', message.data);
  89. break;
  90. }
  91. };
  92. this.execCallbacks = {};
  93. this.execId = 0;
  94. this.exec = function(data, callback) {
  95. if (!callback) {
  96. // if no callback is given, send directly
  97. channel.send(data);
  98. return;
  99. }
  100. if (channel.execId === Number.MAX_VALUE) {
  101. // wrap
  102. channel.execId = Number.MIN_VALUE;
  103. }
  104. if (data.hasOwnProperty('id')) {
  105. console.error('Cannot exec message with property id: ' + JSON.stringify(data));
  106. return;
  107. }
  108. data.id = channel.execId++;
  109. channel.execCallbacks[data.id] = callback;
  110. channel.send(data);
  111. };
  112. this.objects = {};
  113. this.handleSignal = function(message) {
  114. var object = channel.objects[message.object];
  115. if (object) {
  116. object.signalEmitted(message.signal, message.args);
  117. } else {
  118. console.warn('Unhandled signal: ' + message.object + '::' + message.signal);
  119. }
  120. };
  121. this.handleResponse = function(message) {
  122. if (!message.hasOwnProperty('id')) {
  123. console.error('Invalid response message received: ', JSON.stringify(message));
  124. return;
  125. }
  126. channel.execCallbacks[message.id](message.data);
  127. delete channel.execCallbacks[message.id];
  128. };
  129. this.handlePropertyUpdate = function(message) {
  130. message.data.forEach((data) => {
  131. var object = channel.objects[data.object];
  132. if (object) {
  133. object.propertyUpdate(data.signals, data.properties);
  134. } else {
  135. console.warn('Unhandled property update: ' + data.object + '::' + data.signal);
  136. }
  137. });
  138. channel.exec({ type: QWebChannelMessageTypes.idle });
  139. };
  140. this.debug = function(message) {
  141. channel.send({ type: QWebChannelMessageTypes.debug, data: message });
  142. };
  143. channel.exec({ type: QWebChannelMessageTypes.init }, function(data) {
  144. for (const objectName of Object.keys(data)) {
  145. new QObject(objectName, data[objectName], channel);
  146. }
  147. // now unwrap properties, which might reference other registered objects
  148. for (const objectName of Object.keys(channel.objects)) {
  149. channel.objects[objectName].unwrapProperties();
  150. }
  151. if (initCallback) {
  152. initCallback(channel);
  153. }
  154. channel.exec({ type: QWebChannelMessageTypes.idle });
  155. });
  156. };
  157. function QObject(name, data, webChannel) {
  158. this.__id__ = name;
  159. webChannel.objects[name] = this;
  160. // List of callbacks that get invoked upon signal emission
  161. this.__objectSignals__ = {};
  162. // Cache of all properties, updated when a notify signal is emitted
  163. this.__propertyCache__ = {};
  164. var object = this;
  165. // ----------------------------------------------------------------------
  166. this.unwrapQObject = function(response) {
  167. if (response instanceof Array) {
  168. // support list of objects
  169. return response.map((qobj) => object.unwrapQObject(qobj));
  170. }
  171. if (!(response instanceof Object)) return response;
  172. if (!response['__QObject*__'] || response.id === undefined) {
  173. var jObj = {};
  174. for (const propName of Object.keys(response)) {
  175. jObj[propName] = object.unwrapQObject(response[propName]);
  176. }
  177. return jObj;
  178. }
  179. var objectId = response.id;
  180. if (webChannel.objects[objectId]) return webChannel.objects[objectId];
  181. if (!response.data) {
  182. console.error('Cannot unwrap unknown QObject ' + objectId + ' without data.');
  183. return;
  184. }
  185. var qObject = new QObject(objectId, response.data, webChannel);
  186. qObject.destroyed.connect(function() {
  187. if (webChannel.objects[objectId] === qObject) {
  188. delete webChannel.objects[objectId];
  189. // reset the now deleted QObject to an empty {} object
  190. // just assigning {} though would not have the desired effect, but the
  191. // below also ensures all external references will see the empty map
  192. // NOTE: this detour is necessary to workaround QTBUG-40021
  193. Object.keys(qObject).forEach((name) => delete qObject[name]);
  194. }
  195. });
  196. // here we are already initialized, and thus must directly unwrap the properties
  197. qObject.unwrapProperties();
  198. return qObject;
  199. };
  200. this.unwrapProperties = function() {
  201. for (const propertyIdx of Object.keys(object.__propertyCache__)) {
  202. object.__propertyCache__[propertyIdx] = object.unwrapQObject(object.__propertyCache__[propertyIdx]);
  203. }
  204. };
  205. function addSignal(signalData, isPropertyNotifySignal) {
  206. var signalName = signalData[0];
  207. var signalIndex = signalData[1];
  208. object[signalName] = {
  209. connect: function(callback) {
  210. if (typeof callback !== 'function') {
  211. console.error('Bad callback given to connect to signal ' + signalName);
  212. return;
  213. }
  214. object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
  215. object.__objectSignals__[signalIndex].push(callback);
  216. // only required for "pure" signals, handled separately for properties in propertyUpdate
  217. if (isPropertyNotifySignal) return;
  218. // also note that we always get notified about the destroyed signal
  219. if (signalName === 'destroyed' || signalName === 'destroyed()' || signalName === 'destroyed(QObject*)')
  220. return;
  221. // and otherwise we only need to be connected only once
  222. if (object.__objectSignals__[signalIndex].length == 1) {
  223. webChannel.exec({
  224. type: QWebChannelMessageTypes.connectToSignal,
  225. object: object.__id__,
  226. signal: signalIndex
  227. });
  228. }
  229. },
  230. disconnect: function(callback) {
  231. if (typeof callback !== 'function') {
  232. console.error('Bad callback given to disconnect from signal ' + signalName);
  233. return;
  234. }
  235. object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
  236. var idx = object.__objectSignals__[signalIndex].indexOf(callback);
  237. if (idx === -1) {
  238. console.error('Cannot find connection of signal ' + signalName + ' to ' + callback.name);
  239. return;
  240. }
  241. object.__objectSignals__[signalIndex].splice(idx, 1);
  242. if (!isPropertyNotifySignal && object.__objectSignals__[signalIndex].length === 0) {
  243. // only required for "pure" signals, handled separately for properties in propertyUpdate
  244. webChannel.exec({
  245. type: QWebChannelMessageTypes.disconnectFromSignal,
  246. object: object.__id__,
  247. signal: signalIndex
  248. });
  249. }
  250. }
  251. };
  252. }
  253. /**
  254. * Invokes all callbacks for the given signalname. Also works for property notify callbacks.
  255. */
  256. function invokeSignalCallbacks(signalName, signalArgs) {
  257. var connections = object.__objectSignals__[signalName];
  258. if (connections) {
  259. connections.forEach(function(callback) {
  260. callback.apply(callback, signalArgs);
  261. });
  262. }
  263. }
  264. this.propertyUpdate = function(signals, propertyMap) {
  265. // update property cache
  266. for (const propertyIndex of Object.keys(propertyMap)) {
  267. var propertyValue = propertyMap[propertyIndex];
  268. object.__propertyCache__[propertyIndex] = this.unwrapQObject(propertyValue);
  269. }
  270. for (const signalName of Object.keys(signals)) {
  271. // Invoke all callbacks, as signalEmitted() does not. This ensures the
  272. // property cache is updated before the callbacks are invoked.
  273. invokeSignalCallbacks(signalName, signals[signalName]);
  274. }
  275. };
  276. this.signalEmitted = function(signalName, signalArgs) {
  277. invokeSignalCallbacks(signalName, this.unwrapQObject(signalArgs));
  278. };
  279. function addMethod(methodData) {
  280. var methodName = methodData[0];
  281. var methodIdx = methodData[1];
  282. // Fully specified methods are invoked by id, others by name for host-side overload resolution
  283. var invokedMethod = methodName[methodName.length - 1] === ')' ? methodIdx : methodName;
  284. object[methodName] = function() {
  285. var args = [];
  286. var callback;
  287. var errCallback;
  288. for (var i = 0; i < arguments.length; ++i) {
  289. var argument = arguments[i];
  290. if (typeof argument === 'function') callback = argument;
  291. else if (argument instanceof QObject && webChannel.objects[argument.__id__] !== undefined)
  292. args.push({
  293. id: argument.__id__
  294. });
  295. else args.push(argument);
  296. }
  297. var result;
  298. // during test, webChannel.exec synchronously calls the callback
  299. // therefore, the promise must be constucted before calling
  300. // webChannel.exec to ensure the callback is set up
  301. if (!callback && typeof Promise === 'function') {
  302. result = new Promise(function(resolve, reject) {
  303. callback = resolve;
  304. errCallback = reject;
  305. });
  306. }
  307. webChannel.exec(
  308. {
  309. type: QWebChannelMessageTypes.invokeMethod,
  310. object: object.__id__,
  311. method: invokedMethod,
  312. args: args
  313. },
  314. function(response) {
  315. if (response !== undefined) {
  316. var result = object.unwrapQObject(response);
  317. if (callback) {
  318. callback(result);
  319. }
  320. } else if (errCallback) {
  321. errCallback();
  322. }
  323. }
  324. );
  325. return result;
  326. };
  327. }
  328. function bindGetterSetter(propertyInfo) {
  329. var propertyIndex = propertyInfo[0];
  330. var propertyName = propertyInfo[1];
  331. var notifySignalData = propertyInfo[2];
  332. // initialize property cache with current value
  333. // NOTE: if this is an object, it is not directly unwrapped as it might
  334. // reference other QObject that we do not know yet
  335. object.__propertyCache__[propertyIndex] = propertyInfo[3];
  336. if (notifySignalData) {
  337. if (notifySignalData[0] === 1) {
  338. // signal name is optimized away, reconstruct the actual name
  339. notifySignalData[0] = propertyName + 'Changed';
  340. }
  341. addSignal(notifySignalData, true);
  342. }
  343. Object.defineProperty(object, propertyName, {
  344. configurable: true,
  345. get: function() {
  346. var propertyValue = object.__propertyCache__[propertyIndex];
  347. if (propertyValue === undefined) {
  348. // This shouldn't happen
  349. console.warn(
  350. 'Undefined value in property cache for property "' +
  351. propertyName +
  352. '" in object ' +
  353. object.__id__
  354. );
  355. }
  356. return propertyValue;
  357. },
  358. set: function(value) {
  359. if (value === undefined) {
  360. console.warn('Property setter for ' + propertyName + ' called with undefined value!');
  361. return;
  362. }
  363. object.__propertyCache__[propertyIndex] = value;
  364. var valueToSend = value;
  365. if (valueToSend instanceof QObject && webChannel.objects[valueToSend.__id__] !== undefined)
  366. valueToSend = { id: valueToSend.__id__ };
  367. webChannel.exec({
  368. type: QWebChannelMessageTypes.setProperty,
  369. object: object.__id__,
  370. property: propertyIndex,
  371. value: valueToSend
  372. });
  373. }
  374. });
  375. }
  376. // ----------------------------------------------------------------------
  377. data.methods.forEach(addMethod);
  378. data.properties.forEach(bindGetterSetter);
  379. data.signals.forEach(function(signal) {
  380. addSignal(signal, false);
  381. });
  382. Object.assign(object, data.enums);
  383. }
  384. //required for use with nodejs
  385. if (typeof module === 'object') {
  386. module.exports = {
  387. QWebChannel: QWebChannel
  388. };
  389. }