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