diff options
-rw-r--r-- | examples/qtobject/qml/qtobject/main.qml | 1 | ||||
-rw-r--r-- | examples/qtobject/testobject.cpp | 2 | ||||
-rw-r--r-- | examples/qtobject/testobject.h | 6 | ||||
-rw-r--r-- | src/MetaObjectPublisher.qml | 266 | ||||
-rw-r--r-- | src/qobject.js | 204 | ||||
-rw-r--r-- | src/qtmetaobjectpublisher.cpp | 85 | ||||
-rw-r--r-- | src/qtmetaobjectpublisher.h | 10 | ||||
-rw-r--r-- | src/src.pro | 2 | ||||
-rw-r--r-- | src/webchannel.js | 2 |
9 files changed, 467 insertions, 111 deletions
diff --git a/examples/qtobject/qml/qtobject/main.qml b/examples/qtobject/qml/qtobject/main.qml index 7a63cd2..d00c9ac 100644 --- a/examples/qtobject/qml/qtobject/main.qml +++ b/examples/qtobject/qml/qtobject/main.qml @@ -49,6 +49,7 @@ import QtWebKit.experimental 1.0 Rectangle { MetaObjectPublisher { id: publisher + webChannel: webChannel } TestObject { diff --git a/examples/qtobject/testobject.cpp b/examples/qtobject/testobject.cpp index 4bcd92f..e18dbc0 100644 --- a/examples/qtobject/testobject.cpp +++ b/examples/qtobject/testobject.cpp @@ -18,6 +18,7 @@ void TestObject::setProp1(const QString& s) p1 = s; qWarning() << __func__ << p1; emit sig1(1, 0.5, QStringLiteral("asdf")); + emit prop1Changed(); } void TestObject::setProp2(const QString& s) @@ -25,6 +26,7 @@ void TestObject::setProp2(const QString& s) p2 = s; qWarning() << __func__ << p2; emit sig2(); + emit prop2Changed(s); } QString TestObject::manyArgs(int a, float b, const QString& c) const diff --git a/examples/qtobject/testobject.h b/examples/qtobject/testobject.h index 39dc608..f3f1c03 100644 --- a/examples/qtobject/testobject.h +++ b/examples/qtobject/testobject.h @@ -7,8 +7,8 @@ class TestObject : public QObject { Q_OBJECT - Q_PROPERTY(QString prop1 READ prop1 WRITE setProp1) - Q_PROPERTY(QString prop2 READ prop2 WRITE setProp2) + Q_PROPERTY(QString prop1 READ prop1 WRITE setProp1 NOTIFY prop1Changed) + Q_PROPERTY(QString prop2 READ prop2 WRITE setProp2 NOTIFY prop2Changed) public: explicit TestObject(QObject *parent = 0); QString prop1() const { return "p1" + p1 + objectName(); } @@ -21,6 +21,8 @@ signals: void timeout(); void sig1(int a, float b, const QString& c); void sig2(); + void prop1Changed(); + void prop2Changed(const QString& newValue); public slots: void startTimer(int millis) diff --git a/src/MetaObjectPublisher.qml b/src/MetaObjectPublisher.qml index 6ac588c..2b97c02 100644 --- a/src/MetaObjectPublisher.qml +++ b/src/MetaObjectPublisher.qml @@ -44,17 +44,57 @@ import Qt.labs.WebChannel 1.0 MetaObjectPublisherImpl { + id: publisher + + // The web channel this publisher works on. + property var webChannel + /** * This map contains the registered objects indexed by their name. */ property variant registeredObjects + property var subscriberCountMap + + // Map of object names to maps of signal names to an array of all their properties. + // The last value is an array as a signal can be the notify signal of multiple properties. + property var signalToPropertyMap + + // Objects that changed their properties and are waiting for idle client. + // map of object name to map of signal name to arguments + property var pendingPropertyUpdates + + // true when the client is idle, false otherwise + property bool clientIsIdle: false + + // true when no property updates should be sent, false otherwise + property bool blockUpdates: false + + // true when at least one client needs to be initialized, + // i.e. when a Qt.init came in which was not handled yet. + property bool pendingInit: false + + // true when at least one client was initialized and thus + // the property updates have been initialized and the + // object info map set. + property bool propertyUpdatesInitialized: false + + function convertQMLArgsToJSArgs(qmlArgs) + { + // NOTE: QML arguments is a map not an array it seems... + // so do the conversion manually + var args = []; + for (var i = 0; i < qmlArgs.length; ++i) { + args.push(qmlArgs[i]); + } + return args; + } /** * Handle the given WebChannel client request and potentially give a response. * * @return true if the request was handled, false otherwise. */ - function handleRequest(data, webChannel) + function handleRequest(data) { var message = typeof(data) === "string" ? JSON.parse(data) : data; if (!message.data) { @@ -64,60 +104,204 @@ MetaObjectPublisherImpl if (!payload.type) { return false; } - var object = payload.object ? registeredObjects[payload.object] : null; - - if (payload.type === "Qt.invokeMethod" && object) { - var method = object[payload.method]; - webChannel.respond(message.id, method.apply(method, payload.args)); - } else if (payload.type === "Qt.connectToSignal" && object) { - object[payload.signal].connect(function() { - // NOTE: QML arguments is a map not an array it seems... - // so do the conversion manually - var args = []; - for (var i = 0; i < arguments.length; ++i) { - args.push(arguments[i]); + + if (payload.object) { + var object = registeredObjects[payload.object]; + + if (payload.type === "Qt.invokeMethod") { + var method = object[payload.method]; + if (method !== undefined) { + webChannel.respond(message.id, method.apply(method, payload.args)); + return true; } - webChannel.sendMessage("Qt.signal", { - object: payload.object, - signal: payload.signal, - args: args - }); - }); - } else if (payload.type === "Qt.getProperty" && object) { - webChannel.respond(message.id, object[payload.property]); - } else if (payload.type === "Qt.setProperty" && object) { - object[payload.property] = payload.value; - } else if (payload.type === "Qt.getObjects") { - webChannel.respond(message.id, registeredObjectInfos()); - } else if (payload.type === "Qt.Debug") { + return false; + } + if (payload.type === "Qt.connectToSignal") { + if (object.hasOwnProperty(payload.signal)) { + subscriberCountMap = subscriberCountMap || {}; + subscriberCountMap[payload.object] = subscriberCountMap[payload.object] || {}; + + // if no one is connected, connect. + if (!subscriberCountMap[payload.object].hasOwnProperty(payload.signal)) { + object[payload.signal].connect(function() { + var args = convertQMLArgsToJSArgs(arguments); + webChannel.sendMessage("Qt.signal", { + object: payload.object, + signal: payload.signal, + args: args + }); + }); + subscriberCountMap[payload.object][payload.signal] = true; + } + return true; + } + return false; + } + if (payload.type === "Qt.setProperty") { + object[payload.property] = payload.value; + return true; + } + } + if (payload.type === "Qt.idle") { + clientIsIdle = true; + return true; + } + if (payload.type === "Qt.init") { + if (!blockUpdates) { + initializeClients(); + } else { + pendingInit = true; + } + return true; + } + if (payload.type === "Qt.Debug") { console.log("DEBUG: ", payload.message); - } else { - return false; + return true; } - - return true; + return false; } function registerObjects(objects) { + if (propertyUpdatesInitialized) { + console.error("Registered new object after initialization. This does not work!"); + } // joining a JS map and a QML one is not as easy as one would assume... - for (var name in registeredObjects) { - if (!objects[name]) { - objects[name] = registeredObjects[name]; + // NOTE: the extra indirection via "merged" is required, using registeredObjects directly + // does not work! this looks like a QML/v8 bug to me, but I could not find a + // standalone testcase which reproduces this behavior :( + var merged = registeredObjects; + for (var name in objects) { + if (!merged.hasOwnProperty(name)) { + merged[name] = objects[name]; + } + } + registeredObjects = merged; + } + + function initializeClients() + { + var objectInfos = classInfoForObjects(registeredObjects); + webChannel.sendMessage("Qt.init", objectInfos); + if (!propertyUpdatesInitialized) { + for (var objectName in objectInfos) { + var objectInfo = objectInfos[objectName]; + var object = registeredObjects[objectName]; + initializePropertyUpdates(objectName, objectInfo, object); } + propertyUpdatesInitialized = true; } - registeredObjects = objects; + pendingInit = false; } - function registeredObjectInfos() + // This function goes through all properties of all objects and connects against + // their notify signal. + // When receiving a notify signal, it will send a Qt.propertyUpdate message to the + // server. + function initializePropertyUpdates(objectName, objectInfo, object) { - var objectInfos = {}; - for (var name in registeredObjects) { - var object = registeredObjects[name]; - if (object) { - objectInfos[name] = classInfoForObject(object); + for (var propertyIndex in objectInfo.properties) { + var propertyInfo = objectInfo.properties[propertyIndex]; + var propertyName = propertyInfo[0]; + var signalName = propertyInfo[1]; + + if (!signalName) // Property without NOTIFY signal + continue; + + if (signalName === 1) { + /// signal name is optimized away, reconstruct the actual name + signalName = propertyName + "Changed"; + } + + signalToPropertyMap[objectName] = signalToPropertyMap[objectName] || {}; + signalToPropertyMap[objectName][signalName] = signalToPropertyMap[objectName][signalName] || []; + var connectedProperties = signalToPropertyMap[objectName][signalName]; + var numConnectedProperties = connectedProperties === undefined ? 0 : connectedProperties.length; + + // Only connect for a property update once + if (numConnectedProperties === 0) { + (function(signalName) { + object[signalName].connect(function() { + pendingPropertyUpdates[objectName] = pendingPropertyUpdates[objectName] || {}; + pendingPropertyUpdates[objectName][signalName] = arguments; + }); + })(signalName); + } + + if (connectedProperties.indexOf(propertyName) === -1) { + /// TODO: this ensures that a given property is only once in + /// the list of connected properties. + /// This happens when multiple clients are connected to + /// a single webchannel. A better place for the initialization + /// should be found. + connectedProperties.push(propertyName); } } - return objectInfos; + } + + function sendPendingPropertyUpdates() + { + if (blockUpdates || !clientIsIdle) { + return; + } + + var data = []; + for (var objectName in pendingPropertyUpdates) { + var object = registeredObjects[objectName]; + var signals = pendingPropertyUpdates[objectName]; + var propertyMap = {}; + for (var signalName in signals) { + var propertyList = signalToPropertyMap[objectName][signalName]; + for (var propertyIndex in propertyList) { + var propertyName = propertyList[propertyIndex]; + var propertyValue = object[propertyName]; + propertyMap[propertyName] = propertyValue; + } + signals[signalName] = convertQMLArgsToJSArgs(signals[signalName]); + } + data.push({ + object: objectName, + signals: signals, + propertyMap: propertyMap + }); + } + pendingPropertyUpdates = {}; + if (data.length > 0) { + webChannel.sendMessage("Qt.propertyUpdate", data); + clientIsIdle = false; + } + } + + Component.onCompleted: { + // Initializing this in the property declaration is not possible and yields to "undefined" + signalToPropertyMap = {} + pendingPropertyUpdates = {} + registeredObjects = {} + } + + onBlockUpdatesChanged: { + if (blockUpdates) { + return; + } + + if (pendingInit) { + initializeClients(); + } else { + sendPendingPropertyUpdates(); + } + } + + /** + * Aggregate property updates since we get multiple Qt.idle message when we have multiple + * clients. They all share the same QWebProcess though so we must take special care to + * prevent message flooding. + */ + Timer { + id: propertyUpdateTimer + /// TODO: what is the proper value here? + interval: 50; + running: !publisher.blockUpdates && publisher.clientIsIdle; + repeat: true; + onTriggered: publisher.sendPendingPropertyUpdates() } } diff --git a/src/qobject.js b/src/qobject.js index d221525..012332e 100644 --- a/src/qobject.js +++ b/src/qobject.js @@ -42,17 +42,78 @@ function QObject(name, data, webChannel) { this.__id__ = name; + webChannel.objectMap[name] = this; + + // List of callbacks that get invoked upon signal emission this.__objectSignals__ = {}; - var methodsAndSignals = []; - for (var i in data.methods) - methodsAndSignals.push(data.methods[i]); - for (var i in data.signals) - methodsAndSignals.push(data.signals[i]); + // Cache of all properties, updated when a notify signal is emitted + this.__propertyCache__ = {}; var object = this; - methodsAndSignals.forEach(function(method) { + // ---------------------------------------------------------------------- + + function addSignal(signal, isPropertyNotifySignal) + { + object[signal] = { + connect: function(callback) { + if (typeof(callback) !== "function") { + console.error("Bad callback given to connect to signal " + signal); + return; + } + + object.__objectSignals__[signal] = object.__objectSignals__[signal] || []; + object.__objectSignals__[signal].push(callback); + + if (!isPropertyNotifySignal) { + // only required for "pure" signals, handled separately for properties in propertyUpdate + webChannel.exec({ + type: "Qt.connectToSignal", + object: object.__id__, + signal: signal + }); + } + } + // TODO: disconnect eventually + }; + } + + /** + * 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 (var propertyName in propertyMap) { + var propertyValue = propertyMap[propertyName]; + object.__propertyCache__[propertyName] = propertyValue; + } + + for (var signalName in 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, signalArgs); + } + + function addMethod(method) + { object[method] = function() { var args = []; var callback; @@ -64,80 +125,123 @@ function QObject(name, data, webChannel) } webChannel.exec({"type": "Qt.invokeMethod", "object": object.__id__, "method": method, "args": args}, function(response) { - if ((response != undefined) && callback) { + if ( (response !== undefined) && callback ) { (callback)(response); } }); }; - }); + } - function connectToSignal(signal) + function bindGetterSetter(propertyInfo) { - object[signal].connect = function(callback) { - if (typeof(callback) !== "function") { - console.error("Bad callback given to connect to signal " + signal); + var propertyName = propertyInfo[0]; + var notifySignal = propertyInfo[1]; + // initialize property cache with current value + object.__propertyCache__[propertyName] = propertyInfo[2] + + if (notifySignal) { + if (notifySignal === 1) { + /// signal name is optimized away, reconstruct the actual name + notifySignal = propertyName + "Changed"; + } + addSignal(notifySignal, true); + } + + object.__defineSetter__(propertyName, function(value) { + if (value === undefined) { + console.warn("Property setter for " + propertyName + " called with undefined value!"); return; } - object.__objectSignals__[signal] = object.__objectSignals__[signal] || []; - webChannel.exec({"type": "Qt.connectToSignal", "object": object.__id__, "signal": signal}); - object.__objectSignals__[signal].push(callback); - }; - } - for (var i in data.signals) { - var signal = data.signals[i]; - connectToSignal(data.signals[i]); - } + object.__propertyCache__[propertyName] = value; + webChannel.exec({"type": "Qt.setProperty", "object": object.__id__, "property": propertyName, "value": value }); - function bindGetterSetter(property) - { - object.__defineSetter__(property, function(value) { - webChannel.exec({"type": "Qt.setProperty", "object": object.__id__, "property": property, "value": value }); }); - object.__defineGetter__(property, function() { - return (function(callback) { - webChannel.exec({"type": "Qt.getProperty", "object": object.__id__, "property": property}, function(response) { + object.__defineGetter__(propertyName, function () { + return (function (callback) { + var propertyValue = object.__propertyCache__[propertyName]; + if (propertyValue === undefined) { + // This shouldn't happen + console.warn("Undefined value in property cache for property \"" + propertyName + "\" in object " + object.__id__); + } + + // TODO: A callback is not required here anymore, but is kept for backwards compatibility + if (callback !== undefined) { if (typeof(callback) !== "function") { console.error("Bad callback given to get property " + property); return; } - callback(response); - }); + callback(propertyValue); + } else { + return propertyValue; + } }); }); } - for (i in data.properties) { - bindGetterSetter(data.properties[i]); - } - for (i in data.enums) { - object[i] = data.enums[i]; + // ---------------------------------------------------------------------- + + data.methods.forEach(addMethod); + + data.properties.forEach(bindGetterSetter); + + data.signals.forEach(function(signal) { addSignal(signal, false); }); + + for (var name in data.enums) { + object[name] = data.enums[name]; } } window.setupQObjectWebChannel = function(webChannel, doneCallback) { + // prevent multiple initialization which might happen with multiple webchannel clients. + var initialized = false; + webChannel.subscribe( "Qt.signal", function(payload) { - var object = window[payload.object]; + var object = webChannel.objectMap[payload.object]; if (object) { - var connections = object.__objectSignals__[payload.signal]; - if (connections) { - connections.forEach(function(callback) { - callback.apply(callback, payload.args); - }); - } + object.signalEmitted(payload.signal, payload.args); + } else { + console.warn("Unhandled signal: " + payload.object + "::" + payload.signal); } } ); - webChannel.exec({type:"Qt.getObjects"}, function(payload) { - for (var objectName in payload) { - var data = payload[objectName]; - var object = new QObject(objectName, data, webChannel); - window[objectName] = object; + + webChannel.subscribe( + "Qt.propertyUpdate", + function(payload) { + for (var i in payload) { + var data = payload[i]; + var object = webChannel.objectMap[data.object]; + if (object) { + object.propertyUpdate(data.signals, data.propertyMap); + } else { + console.warn("Unhandled property update: " + data.object + "::" + data.signal); + } + } + setTimeout(function() { webChannel.exec({type: "Qt.idle"}); }, 0); } - if (doneCallback) { - doneCallback(); + ); + + webChannel.subscribe( + "Qt.init", + function(payload) { + if (initialized) { + return; + } + initialized = true; + for (var objectName in payload) { + var data = payload[objectName]; + var object = new QObject(objectName, data, webChannel); + window[objectName] = object; + } + if (doneCallback) { + doneCallback(); + } + setTimeout(function() { webChannel.exec({type: "Qt.idle"}); }, 0); } - }); + ); + + webChannel.exec({type:"Qt.init"}); }; diff --git a/src/qtmetaobjectpublisher.cpp b/src/qtmetaobjectpublisher.cpp index 1646434..8e871e0 100644 --- a/src/qtmetaobjectpublisher.cpp +++ b/src/qtmetaobjectpublisher.cpp @@ -45,11 +45,33 @@ #include <QMetaObject> #include <QMetaProperty> -QtMetaObjectPublisher::QtMetaObjectPublisher(QObject *parent) - : QObject(parent) +static const QString KEY_SIGNALS = QStringLiteral("signals"); +static const QString KEY_METHODS = QStringLiteral("methods"); +static const QString KEY_PROPERTIES = QStringLiteral("properties"); +static const QString KEY_ENUMS = QStringLiteral("enums"); + +QtMetaObjectPublisher::QtMetaObjectPublisher(QQuickItem *parent) + : QQuickItem(parent) { } +QVariantMap QtMetaObjectPublisher::classInfoForObjects(const QVariantMap &objectMap) const +{ + QVariantMap ret; + QMap<QString, QVariant>::const_iterator it = objectMap.constBegin(); + while (it != objectMap.constEnd()) { + QObject* object = it.value().value<QObject*>(); + if (object) { + const QVariantMap &info = classInfoForObject(object); + if (!info.isEmpty()) { + ret[it.key()] = info; + } + } + ++it; + } + return ret; +} + QVariantMap QtMetaObjectPublisher::classInfoForObject(QObject *object) const { QVariantMap data; @@ -57,19 +79,54 @@ QVariantMap QtMetaObjectPublisher::classInfoForObject(QObject *object) const qWarning("null object given to MetaObjectPublisher - bad API usage?"); return data; } - QStringList qtSignals, qtMethods, qtProperties; + QVariantList qtSignals, qtMethods; + QVariantList qtProperties; QVariantMap qtEnums; const QMetaObject* metaObject = object->metaObject(); - for (int i = 0; i < metaObject->propertyCount(); ++i) - qtProperties.append(metaObject->property(i).name()); + QSet<int> notifySignals; + QSet<QString> properties; + for (int i = 0; i < metaObject->propertyCount(); ++i) { + const QMetaProperty &prop = metaObject->property(i); + QVariantList propertyInfo; + const QString &propertyName = QString::fromLatin1(prop.name()); + propertyInfo.append(propertyName); + properties << propertyName; + if (prop.hasNotifySignal()) { + notifySignals << prop.notifySignalIndex(); + const int numParams = prop.notifySignal().parameterCount(); + if (numParams > 1) { + qWarning("Notify signal for property '%s' has %d parameters, expected zero or one.", + prop.name(), numParams); + } + propertyInfo.append(QString::fromLatin1(prop.notifySignal().name())); + } else { + if (!prop.isConstant()) { + qWarning("Property '%s'' of object '%s' has no notify signal and is not constant, " + "value updates in HTML will be broken!", + prop.name(), object->metaObject()->className()); + } + propertyInfo.append(QString()); + } + propertyInfo.append(prop.read(object)); + qtProperties.append(QVariant::fromValue(propertyInfo)); + } for (int i = 0; i < metaObject->methodCount(); ++i) { - QMetaMethod method = metaObject->method(i); - QString signature = method.methodSignature(); - QString name = signature.left(signature.indexOf("(")); + if (notifySignals.contains(i)) { + continue; + } + const QMetaMethod &method = metaObject->method(i); + //NOTE: This will not work for overloaded methods/signals. + //NOTE: this must be a string, otherwise it will be converted to '{}' in QML + const QString &name = QString::fromLatin1(method.name()); + if (properties.contains(name)) { + // optimize: Don't send the getter method, it gets overwritten by the + // property on the client side anyways. + continue; + } if (method.access() == QMetaMethod::Public) - qtMethods << signature << name; + qtMethods << name; if (method.methodType() == QMetaMethod::Signal) - qtSignals << signature << name; + qtSignals << name; } for (int i = 0; i < metaObject->enumeratorCount(); ++i) { QMetaEnum enumerator = metaObject->enumerator(i); @@ -79,9 +136,9 @@ QVariantMap QtMetaObjectPublisher::classInfoForObject(QObject *object) const } qtEnums[enumerator.name()] = values; } - data["signals"] = qtSignals; - data["methods"] = qtMethods; - data["properties"] = qtProperties; - data["enums"] = qtEnums; + data[KEY_SIGNALS] = qtSignals; + data[KEY_METHODS] = qtMethods; + data[KEY_PROPERTIES] = QVariant::fromValue(qtProperties); + data[KEY_ENUMS] = qtEnums; return data; } diff --git a/src/qtmetaobjectpublisher.h b/src/qtmetaobjectpublisher.h index 573d120..0c19374 100644 --- a/src/qtmetaobjectpublisher.h +++ b/src/qtmetaobjectpublisher.h @@ -44,15 +44,19 @@ #include <QObject> #include <QVariantMap> +#include <QQuickItem> -class QtMetaObjectPublisher : public QObject +class QObjectWrapper; + +// NOTE: QQuickItem inheritance required to enable QML item nesting (i.e. Timer in MetaObjectPublisher) +class QtMetaObjectPublisher : public QQuickItem { Q_OBJECT public: - explicit QtMetaObjectPublisher(QObject *parent = 0); + explicit QtMetaObjectPublisher(QQuickItem *parent = 0); + Q_INVOKABLE QVariantMap classInfoForObjects(const QVariantMap &objects) const; Q_INVOKABLE QVariantMap classInfoForObject(QObject *object) const; }; #endif // QTMETAOBJECTPUBLISHER_H - diff --git a/src/src.pro b/src/src.pro index 8792e04..ee77e4b 100644 --- a/src/src.pro +++ b/src/src.pro @@ -3,7 +3,7 @@ include(src.pri) TEMPLATE = lib TARGET = qwebchannel TARGETPATH = Qt/labs/WebChannel -QT += qml +QT += qml quick CONFIG += qt plugin TARGET = $$qtLibraryTarget($$TARGET) diff --git a/src/webchannel.js b/src/webchannel.js index 6141268..1e6603c 100644 --- a/src/webchannel.js +++ b/src/webchannel.js @@ -123,4 +123,6 @@ var QWebChannel = function(baseUrl, initCallback) { channel.send({"data" : {"type" : "Qt.Debug", "message" : message}}); }; + + this.objectMap = {}; }; |