summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/MetaObjectPublisher.qml384
-rw-r--r--src/qmldir1
-rw-r--r--src/qobject.js53
-rw-r--r--src/qtmetaobjectpublisher.cpp629
-rw-r--r--src/qtmetaobjectpublisher.h73
-rw-r--r--src/qwebchannel_plugin.cpp2
-rw-r--r--src/signalhandler.h278
-rw-r--r--src/src.pri4
-rw-r--r--src/src.pro6
-rw-r--r--src/variantargument.h103
10 files changed, 1041 insertions, 492 deletions
diff --git a/src/MetaObjectPublisher.qml b/src/MetaObjectPublisher.qml
deleted file mode 100644
index 8d93fc4..0000000
--- a/src/MetaObjectPublisher.qml
+++ /dev/null
@@ -1,384 +0,0 @@
-/****************************************************************************
-**
-** Copyright (C) 2011 Nokia Corporation and/or its subsidiary(-ies).
-** All rights reserved.
-** Contact: Nokia Corporation (qt-info@nokia.com)
-**
-** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
-** Contact: http://www.qt-project.org/legal
-**
-** This file is part of the QWebChannel module on Qt labs.
-**
-** $QT_BEGIN_LICENSE:LGPL$
-** No Commercial Usage
-** This file contains pre-release code and may not be distributed.
-** You may use this file in accordance with the terms and conditions
-** contained in the Technology Preview License Agreement accompanying
-** this package.
-**
-** GNU Lesser General Public License Usage
-** Alternatively, this file may be used under the terms of the GNU Lesser
-** General Public License version 2.1 as published by the Free Software
-** Foundation and appearing in the file LICENSE.LGPL included in the
-** packaging of this file. Please review the following information to
-** ensure the GNU Lesser General Public License version 2.1 requirements
-** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
-**
-** In addition, as a special exception, Nokia gives you certain additional
-** rights. These rights are described in the Nokia Qt LGPL Exception
-** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
-**
-** If you have questions regarding the use of this file, please contact
-** Nokia at qt-info@nokia.com.
-**
-**
-**
-**
-**
-**
-**
-**
-** $QT_END_LICENSE$
-**
-****************************************************************************/
-
-import QtQuick 2.0
-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: ({})
- /**
- * Tracks how many connections are active to object signals.
- *
- * Maps objectName -> signalName -> {handler: functor, subscribers: int}.
- */
- 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
-
- /**
- * Wrap a result value if it's a Qt QObject
- *
- * @return object info for wrapped Qt Object,
- * or the same value if no wrapping needed
- *
- */
- function wrapResult(result)
- {
- if (typeof(result) === "object"
- && result["objectName"] !== undefined)
- {
- var ret = wrapObject(result);
- initializePropertyUpdates(ret.id, ret.data, result, webChannel);
- return ret;
- }
- return result;
- }
-
- 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)
- {
- var message = typeof(data) === "string" ? JSON.parse(data) : data;
- if (!message.data) {
- return false;
- }
- var payload = message.data;
- if (!payload.type) {
- return false;
- }
-
- if (payload.object) {
- var isWrapped = false;
- var object = registeredObjects[payload.object];
- if (!object) {
- object = unwrapObject(payload.object);
- if (object)
- isWrapped = true;
- else
- return false
- }
-
- if (payload.type === "Qt.invokeMethod") {
- var method = object[payload.method];
- if (method !== undefined) {
- webChannel.respond(message.id,
- wrapResult(method.apply(method, payload.args)));
- return true;
- }
- if (isWrapped && payload.method === "deleteLater") {
- // invoke `deleteLater` on wrapped QObject indirectly
- deleteWrappedObject(object);
- return true;
- }
- return false;
- }
- if (payload.type === "Qt.connectToSignal") {
- if (object.hasOwnProperty(payload.signal)) {
- subscriberCountMap[payload.object] = subscriberCountMap[payload.object] || {};
-
- // if no one is connected, connect.
- if (!subscriberCountMap[payload.object].hasOwnProperty(payload.signal)) {
- subscriberCountMap[payload.object][payload.signal] = {
- subscribers: 1,
- handler: function() {
- var args = convertQMLArgsToJSArgs(arguments);
- webChannel.sendMessage("Qt.signal", {
- object: payload.object,
- signal: payload.signal,
- args: args
- });
- }
- };
- object[payload.signal].connect(subscriberCountMap[payload.object][payload.signal].handler);
- } else {
- ++subscriberCountMap[payload.object][payload.signal].subscribers;
- }
- return true;
- }
- // connecting to `destroyed` signal of wrapped QObject
- if (isWrapped && payload.signal === "destroyed") {
- // is a no-op on this side
- return true;
- }
- return false;
- }
- if (payload.type === "Qt.disconnectFromSignal") {
- if (!object.hasOwnProperty(payload.signal)) {
- return false;
- }
- subscriberCountMap[payload.object] = subscriberCountMap[payload.object] || {};
-
- if (!subscriberCountMap[payload.object].hasOwnProperty(payload.signal)) {
- return false
- }
- if (--subscriberCountMap[payload.object][payload.signal].subscribers === 0) {
- object[payload.signal].disconnect(subscriberCountMap[payload.object][payload.signal].handler);
- delete subscriberCountMap[payload.object][payload.signal];
- }
- return true;
- }
- 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);
- 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...
- // 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;
- }
- pendingInit = false;
- }
-
- // 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)
- {
- 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);
- }
- }
- }
-
- function sendPendingPropertyUpdates()
- {
- if (blockUpdates || !clientIsIdle) {
- return;
- }
-
- var data = [];
- for (var objectName in pendingPropertyUpdates) {
- var object = registeredObjects[objectName];
- if (!object) {
- object = unwrapObject(objectName);
- if (!object) {
- console.error("Got property update for unknown object " + objectName);
- continue;
- }
- }
- 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;
- }
- }
-
- onBlockUpdatesChanged: {
- if (blockUpdates) {
- return;
- }
-
- if (pendingInit) {
- initializeClients();
- } else {
- sendPendingPropertyUpdates();
- }
- }
-
- onWrappedObjectDestroyed: { // (const QString& id)
- // act as if object had sent `destroyed` signal
- webChannel.sendMessage("Qt.signal", {
- object: id,
- signal: "destroyed",
- args: []
- });
- delete subscriberCountMap[id];
- delete pendingPropertyUpdates[id];
- delete signalToPropertyMap[id]
- }
-
- /**
- * 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/qmldir b/src/qmldir
index f0b0992..ebd912a 100644
--- a/src/qmldir
+++ b/src/qmldir
@@ -1,3 +1,2 @@
module Qt.labs.WebChannel
plugin qwebchannel
-MetaObjectPublisher 1.0 MetaObjectPublisher.qml
diff --git a/src/qobject.js b/src/qobject.js
index 27cb1e5..b783a5b 100644
--- a/src/qobject.js
+++ b/src/qobject.js
@@ -4,6 +4,9 @@
** All rights reserved.
** Contact: Nokia Corporation (qt-info@nokia.com)
**
+** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt-project.org/legal
+**
** This file is part of the QWebChannel module on Qt labs.
**
** $QT_BEGIN_LICENSE:LGPL$
@@ -82,45 +85,47 @@ function QObject(name, data, webChannel)
return qObject;
}
- function addSignal(signal, isPropertyNotifySignal)
+ function addSignal(signalData, isPropertyNotifySignal)
{
- object[signal] = {
+ 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 " + signal);
+ console.error("Bad callback given to connect to signal " + signalName);
return;
}
- object.__objectSignals__[signal] = object.__objectSignals__[signal] || [];
- object.__objectSignals__[signal].push(callback);
+ object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
+ object.__objectSignals__[signalIndex].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
+ signal: signalIndex
});
}
},
disconnect: function(callback) {
if (typeof(callback) !== "function") {
- console.error("Bad callback given to disconnect from signal " + signal);
+ console.error("Bad callback given to disconnect from signal " + signalName);
return;
}
- object.__objectSignals__[signal] = object.__objectSignals__[signal] || [];
- var idx = object.__objectSignals__[signal].indexOf(callback);
+ object.__objectSignals__[signalIndex] = object.__objectSignals__[signalIndex] || [];
+ var idx = object.__objectSignals__[signalIndex].indexOf(callback);
if (idx === -1) {
- console.error("Cannot find connection for given callback to signal" + signal, callback);
+ console.error("Cannot find connection of signal " + signalName + " to " + callback.name);
return;
}
- object.__objectSignals__[signal].splice(idx, 1);
- if (!isPropertyNotifySignal && object.__objectSignals__[signal].length === 0) {
+ 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: "Qt.disconnectFromSignal",
object: object.__id__,
- signal: signal
+ signal: signalIndex
});
}
}
@@ -160,9 +165,11 @@ function QObject(name, data, webChannel)
invokeSignalCallbacks(signalName, signalArgs);
}
- function addMethod(method)
+ function addMethod(methodData)
{
- object[method] = function() {
+ var methodName = methodData[0];
+ var methodIdx = methodData[1];
+ object[methodName] = function() {
var args = [];
var callback;
for (var i = 0; i < arguments.length; ++i) {
@@ -172,7 +179,7 @@ function QObject(name, data, webChannel)
args.push(arguments[i]);
}
- webChannel.exec({"type": "Qt.invokeMethod", "object": object.__id__, "method": method, "args": args}, function(response) {
+ webChannel.exec({"type": "Qt.invokeMethod", "object": object.__id__, "method": methodIdx, "args": args}, function(response) {
if ( (response !== undefined) && callback ) {
(callback)(unwrapQObject(response));
}
@@ -183,16 +190,16 @@ function QObject(name, data, webChannel)
function bindGetterSetter(propertyInfo)
{
var propertyName = propertyInfo[0];
- var notifySignal = propertyInfo[1];
+ var notifySignalData = 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";
+ if (notifySignalData) {
+ if (notifySignalData[0] === 1) {
+ // signal name is optimized away, reconstruct the actual name
+ notifySignalData[0] = propertyName + "Changed";
}
- addSignal(notifySignal, true);
+ addSignal(notifySignalData, true);
}
object.__defineSetter__(propertyName, function(value) {
@@ -263,7 +270,7 @@ window.setupQObjectWebChannel = function(webChannel, doneCallback)
var data = payload[i];
var object = window[data.object] || webChannel.objectMap[data.object];
if (object) {
- object.propertyUpdate(data.signals, data.propertyMap);
+ object.propertyUpdate(data.signals, data.properties);
} else {
console.warn("Unhandled property update: " + data.object + "::" + data.signal);
}
diff --git a/src/qtmetaobjectpublisher.cpp b/src/qtmetaobjectpublisher.cpp
index 511f514..9ca0d93 100644
--- a/src/qtmetaobjectpublisher.cpp
+++ b/src/qtmetaobjectpublisher.cpp
@@ -43,24 +43,433 @@
****************************************************************************/
#include "qtmetaobjectpublisher.h"
+#include "qwebchannel.h"
+#include "variantargument.h"
+#include "signalhandler.h"
#include <QStringList>
#include <QMetaObject>
-#include <QMetaProperty>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QBasicTimer>
#include <QDebug>
+#include <QPointer>
+#include <QEvent>
-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");
+namespace {
+const QString KEY_SIGNALS = QStringLiteral("signals");
+const QString KEY_METHODS = QStringLiteral("methods");
+const QString KEY_PROPERTIES = QStringLiteral("properties");
+const QString KEY_ENUMS = QStringLiteral("enums");
+const QString KEY_QOBJECT = QStringLiteral("__QObject*__");
+const QString KEY_ID = QStringLiteral("id");
+const QString KEY_DATA = QStringLiteral("data");
+const QString KEY_OBJECT = QStringLiteral("object");
+const QString KEY_DESTROYED = QStringLiteral("destroyed");
+const QString KEY_SIGNAL = QStringLiteral("signal");
+const QString KEY_TYPE = QStringLiteral("type");
+const QString KEY_MESSAGE = QStringLiteral("message");
+const QString KEY_METHOD = QStringLiteral("method");
+const QString KEY_ARGS = QStringLiteral("args");
+const QString KEY_PROPERTY = QStringLiteral("property");
+const QString KEY_VALUE = QStringLiteral("value");
-static const QString KEY_QOBJECT = QStringLiteral("__QObject*__");
-static const QString KEY_ID = QStringLiteral("id");
-static const QString KEY_DATA = QStringLiteral("data");
+const QString TYPE_SIGNAL = QStringLiteral("Qt.signal");
+const QString TYPE_PROPERTY_UPDATE = QStringLiteral("Qt.propertyUpdate");
+const QString TYPE_INIT = QStringLiteral("Qt.init");
+const QString TYPE_IDLE = QStringLiteral("Qt.idle");
+const QString TYPE_DEBUG = QStringLiteral("Qt.debug");
+const QString TYPE_INVOKE_METHOD = QStringLiteral("Qt.invokeMethod");
+const QString TYPE_CONNECT_TO_SIGNAL = QStringLiteral("Qt.connectToSignal");
+const QString TYPE_DISCONNECT_FROM_SIGNAL = QStringLiteral("Qt.disconnectFromSignal");
+const QString TYPE_SET_PROPERTY = QStringLiteral("Qt.setProperty");
-QtMetaObjectPublisher::QtMetaObjectPublisher(QQuickItem *parent)
- : QQuickItem(parent)
+QString objectId(const QObject *object)
{
+ return QString::number(quintptr(object), 16);
+}
+
+const int s_destroyedSignalIndex = QObject::staticMetaObject.indexOfMethod("destroyed(QObject*)");
+
+/// TODO: what is the proper value here?
+const int PROPERTY_UPDATE_INTERVAL = 50;
+}
+
+struct QtMetaObjectPublisherPrivate
+{
+ QtMetaObjectPublisherPrivate(QtMetaObjectPublisher *q)
+ : q(q)
+ , signalHandler(this)
+ , clientIsIdle(false)
+ , blockUpdates(false)
+ , pendingInit(false)
+ , propertyUpdatesInitialized(false)
+ {
+ }
+
+ /**
+ * Set the client to idle or busy, based on the value of @p isIdle.
+ *
+ * When the value changed, start/stop the property update timer accordingly.
+ */
+ void setClientIsIdle(bool isIdle);
+
+ /**
+ * Initialize clients by sending them the class information of the registered objects.
+ *
+ * Furthermore, if that was not done already, connect to their property notify signals.
+ */
+ void initializeClients();
+
+ /**
+ * Go through all properties of the given object and connect to their notify signal.
+ *
+ * When receiving a notify signal, it will store the information in pendingPropertyUpdates which
+ * gets send via a Qt.propertyUpdate message to the server when the grouping timer timeouts.
+ */
+ void initializePropertyUpdates(const QObject *const object, const QVariantMap &objectInfo);
+
+ /**
+ * Send the clients the new property values since the last time this function was invoked.
+ *
+ * This is a grouped batch of all properties for which their notify signal was emitted.
+ * The list of signals as well as the arguments they contained, are also transmitted to
+ * the remote clients.
+ *
+ * @sa timer, initializePropertyUpdates
+ */
+ void sendPendingPropertyUpdates();
+
+ /**
+ * Invoke the method of index @p methodIndex on @p object with the arguments @p args.
+ *
+ * The return value of the method invocation is then transmitted to the calling client
+ * via a webchannel response to the message identified by @p id.
+ */
+ bool invokeMethod(QObject *const object, const int methodIndex, const QJsonArray &args, const QJsonValue &id);
+
+ /**
+ * Callback of the signalHandler which forwards the signal invocation to the webchannel clients.
+ */
+ void signalEmitted(const QObject *object, const int signalIndex, const QVariantList &arguments);
+
+ /**
+ * Callback for registered or wrapped objects which erases all data related to @p object.
+ *
+ * @sa signalEmitted
+ */
+ void objectDestroyed(const QObject *object);
+
+ /**
+ * Given a QVariant containing a QObject*, wrap the object and register for property updates
+ * return the objects class information.
+ *
+ * All other input types are returned as-is.
+ *
+ * TODO: support wrapping of initially-registered objects
+ */
+ QVariant wrapResult(const QVariant &result);
+
+ /**
+ * Invoke delete later on @p object.
+ */
+ void deleteWrappedObject(QObject *object) const;
+
+ QtMetaObjectPublisher *q;
+ QPointer<QWebChannel> webChannel;
+ SignalHandler<QtMetaObjectPublisherPrivate> signalHandler;
+
+ // true when the client is idle, false otherwise
+ bool clientIsIdle;
+
+ // true when no property updates should be sent, false otherwise
+ bool blockUpdates;
+
+ // true when at least one client needs to be initialized,
+ // i.e. when a Qt.init came in which was not handled yet.
+ bool pendingInit;
+
+ // true when at least one client was initialized and thus
+ // the property updates have been initialized and the
+ // object info map set.
+ bool propertyUpdatesInitialized;
+
+ // Map of registered objects indexed by their id.
+ QHash<QString, QObject *> registeredObjects;
+
+ // Map the registered objects to their id.
+ QHash<const QObject *, QString> registeredObjectIds;
+
+ // Map of object names to maps of signal indices to a set of all their properties.
+ // The last value is a set as a signal can be the notify signal of multiple properties.
+ typedef QHash<int, QSet<QString> > SignalToPropertyNameMap;
+ QHash<const QObject *, SignalToPropertyNameMap> signalToPropertyMap;
+
+ // Objects that changed their properties and are waiting for idle client.
+ // map of object name to map of signal index to arguments
+ typedef QHash<int, QVariantList> SignalToArgumentsMap;
+ typedef QHash<const QObject *, SignalToArgumentsMap> PendingPropertyUpdates;
+ PendingPropertyUpdates pendingPropertyUpdates;
+
+ // Maps wrapped object to class info
+ QHash<const QObject *, QVariantMap> wrappedObjects;
+
+ // 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.
+ QBasicTimer timer;
+};
+
+void QtMetaObjectPublisherPrivate::setClientIsIdle(bool isIdle)
+{
+ if (clientIsIdle == isIdle) {
+ return;
+ }
+ clientIsIdle = isIdle;
+ if (!isIdle && timer.isActive()) {
+ timer.stop();
+ } else if (isIdle && !timer.isActive()) {
+ timer.start(PROPERTY_UPDATE_INTERVAL, q);
+ }
+}
+
+void QtMetaObjectPublisherPrivate::initializeClients()
+{
+ QJsonObject objectInfos;
+ {
+ const QHash<QString, QObject *>::const_iterator end = registeredObjects.constEnd();
+ for (QHash<QString, QObject *>::const_iterator it = registeredObjects.constBegin(); it != end; ++it) {
+ const QVariantMap &info = q->classInfoForObject(it.value());
+ if (!propertyUpdatesInitialized) {
+ initializePropertyUpdates(it.value(), info);
+ }
+ objectInfos[it.key()] = QJsonObject::fromVariantMap(info);
+ }
+ }
+ webChannel->sendMessage(TYPE_INIT, objectInfos);
+ propertyUpdatesInitialized = true;
+ pendingInit = false;
+}
+
+void QtMetaObjectPublisherPrivate::initializePropertyUpdates(const QObject *const object, const QVariantMap &objectInfo)
+{
+ foreach (const QVariant &propertyInfoVar, objectInfo[KEY_PROPERTIES].toList()) {
+ const QVariantList &propertyInfo = propertyInfoVar.toList();
+ if (propertyInfo.size() < 2) {
+ qWarning() << "Invalid property info encountered:" << propertyInfoVar;
+ continue;
+ }
+ const QString &propertyName = propertyInfo.at(0).toString();
+ const QVariantList &signalData = propertyInfo.at(1).toList();
+
+ if (signalData.isEmpty()) {
+ // Property without NOTIFY signal
+ continue;
+ }
+
+ const int signalIndex = signalData.at(1).toInt();
+
+ QSet<QString> &connectedProperties = signalToPropertyMap[object][signalIndex];
+
+ // Only connect for a property update once
+ if (connectedProperties.isEmpty()) {
+ signalHandler.connectTo(object, signalIndex);
+ }
+
+ connectedProperties.insert(propertyName);
+ }
+
+ // also always connect to destroyed signal
+ signalHandler.connectTo(object, s_destroyedSignalIndex);
+}
+
+void QtMetaObjectPublisherPrivate::sendPendingPropertyUpdates()
+{
+ if (blockUpdates || !clientIsIdle || pendingPropertyUpdates.isEmpty()) {
+ return;
+ }
+
+ QJsonArray data;
+
+ // convert pending property updates to JSON data
+ const PendingPropertyUpdates::const_iterator end = pendingPropertyUpdates.constEnd();
+ for (PendingPropertyUpdates::const_iterator it = pendingPropertyUpdates.constBegin(); it != end; ++it) {
+ const QObject *object = it.key();
+ const QMetaObject *const metaObject = object->metaObject();
+ const SignalToPropertyNameMap &objectsSignalToPropertyMap = signalToPropertyMap.value(object);
+ // maps property name to current property value
+ QJsonObject properties;
+ // maps signal index to list of arguments of the last emit
+ QJsonObject sigs;
+ const SignalToArgumentsMap::const_iterator sigEnd = it.value().constEnd();
+ for (SignalToArgumentsMap::const_iterator sigIt = it.value().constBegin(); sigIt != sigEnd; ++sigIt) {
+ // TODO: use property indices
+ foreach (const QString &propertyName, objectsSignalToPropertyMap.value(sigIt.key())) {
+ int propertyIndex = metaObject->indexOfProperty(qPrintable(propertyName));
+ if (propertyIndex == -1) {
+ qWarning("Unknown property %d encountered", propertyIndex);
+ continue;
+ }
+ const QMetaProperty &property = metaObject->property(propertyIndex);
+ properties[QString::fromLatin1(property.name())] = QJsonValue::fromVariant(property.read(object));
+ }
+ // TODO: can we get rid of the int <-> string conversions here?
+ sigs[QString::number(sigIt.key())] = QJsonArray::fromVariantList(sigIt.value());
+ }
+ QJsonObject obj;
+ obj[KEY_OBJECT] = registeredObjectIds.value(object);
+ obj[KEY_SIGNALS] = sigs;
+ obj[KEY_PROPERTIES] = properties;
+ data.push_back(obj);
+ }
+
+ pendingPropertyUpdates.clear();
+ webChannel->sendMessage(TYPE_PROPERTY_UPDATE, data);
+ setClientIsIdle(false);
+}
+
+bool QtMetaObjectPublisherPrivate::invokeMethod(QObject *const object, const int methodIndex,
+ const QJsonArray &args, const QJsonValue &id)
+{
+ const QMetaMethod &method = object->metaObject()->method(methodIndex);
+
+ if (method.name() == QByteArrayLiteral("deleteLater")) {
+ // invoke `deleteLater` on wrapped QObject indirectly
+ deleteWrappedObject(object);
+ return true;
+ } else if (!method.isValid()) {
+ qWarning() << "Cannot invoke unknown method of index" << methodIndex << "on object" << object << '.';
+ return false;
+ } else if (method.access() != QMetaMethod::Public) {
+ qWarning() << "Cannot invoke non-public method" << method.name() << "on object" << object << '.';
+ return false;
+ } else if (method.methodType() != QMetaMethod::Method && method.methodType() != QMetaMethod::Slot) {
+ qWarning() << "Cannot invoke non-public method" << method.name() << "on object" << object << '.';
+ return false;
+ } else if (args.size() > 10) {
+ qWarning() << "Cannot invoke method" << method.name() << "on object" << object << "with more than 10 arguments, as that is not supported by QMetaMethod::invoke.";
+ return false;
+ } else if (args.size() > method.parameterCount()) {
+ qWarning() << "Ignoring additional arguments while invoking method" << method.name() << "on object" << object << ':'
+ << args.size() << "arguments given, but method only takes" << method.parameterCount() << '.';
+ }
+
+ // construct converter objects of QVariant to QGenericArgument
+ VariantArgument arguments[10];
+ for (int i = 0; i < qMin(args.size(), method.parameterCount()); ++i) {
+ arguments[i].setValue(args.at(i).toVariant(), method.parameterType(i));
+ }
+
+ // construct QGenericReturnArgument
+ QVariant returnValue;
+ if (method.returnType() != qMetaTypeId<QVariant>() && method.returnType() != qMetaTypeId<void>()) {
+ // Only init variant with return type if its not a variant itself, which would
+ // lead to nested variants which is not what we want.
+ // Also, skip void-return types for obvious reasons (and to prevent a runtime warning inside Qt).
+ returnValue = QVariant(method.returnType(), 0);
+ }
+ QGenericReturnArgument returnArgument(method.typeName(), returnValue.data());
+
+ // now we can call the method
+ method.invoke(object, returnArgument,
+ arguments[0], arguments[1], arguments[2], arguments[3], arguments[4],
+ arguments[5], arguments[6], arguments[7], arguments[8], arguments[9]);
+
+ // and send the return value to the client
+ webChannel->respond(id, QJsonValue::fromVariant(wrapResult(returnValue)));
+
+ return true;
+}
+
+void QtMetaObjectPublisherPrivate::signalEmitted(const QObject *object, const int signalIndex, const QVariantList &arguments)
+{
+ if (!webChannel) {
+ return;
+ }
+ if (!signalToPropertyMap.value(object).contains(signalIndex)) {
+ QJsonObject data;
+ const QString &objectName = registeredObjectIds.value(object);
+ Q_ASSERT(!objectName.isEmpty());
+ data[KEY_OBJECT] = objectName;
+ data[KEY_SIGNAL] = signalIndex;
+ if (!arguments.isEmpty()) {
+ // TODO: wrap (new) objects on the fly
+ data[KEY_ARGS] = QJsonArray::fromVariantList(arguments);
+ }
+ webChannel->sendMessage(TYPE_SIGNAL, data);
+
+ if (signalIndex == s_destroyedSignalIndex) {
+ objectDestroyed(object);
+ }
+ } else {
+ pendingPropertyUpdates[object][signalIndex] = arguments;
+ if (clientIsIdle && !blockUpdates && !timer.isActive()) {
+ timer.start(PROPERTY_UPDATE_INTERVAL, q);
+ }
+ }
+}
+
+void QtMetaObjectPublisherPrivate::objectDestroyed(const QObject *object)
+{
+ const QString &id = registeredObjectIds.take(object);
+ Q_ASSERT(!id.isEmpty());
+ bool removed = registeredObjects.remove(id);
+ Q_ASSERT(removed);
+ Q_UNUSED(removed);
+
+ signalToPropertyMap.remove(object);
+ pendingPropertyUpdates.remove(object);
+ wrappedObjects.remove(object);
+}
+
+QVariant QtMetaObjectPublisherPrivate::wrapResult(const QVariant &result)
+{
+ if (QObject *object = result.value<QObject *>()) {
+ QVariantMap &objectInfo = wrappedObjects[object];
+ if (!objectInfo.isEmpty()) {
+ // already registered, use cached information
+ Q_ASSERT(registeredObjectIds.contains(object));
+ return objectInfo;
+ } // else the object is not yet wrapped, do it now
+
+ const QString &id = objectId(object);
+ Q_ASSERT(!registeredObjects.contains(id));
+ Q_ASSERT(!registeredObjectIds.contains(object));
+
+ objectInfo[KEY_QOBJECT] = true;
+ objectInfo[KEY_ID] = id;
+ objectInfo[KEY_DATA] = q->classInfoForObject(object);
+
+ registeredObjectIds[object] = id;
+ registeredObjects[id] = object;
+ wrappedObjects.insert(object, objectInfo);
+
+ initializePropertyUpdates(object, objectInfo);
+ return objectInfo;
+ }
+
+ // no need to wrap this
+ return result;
+}
+
+void QtMetaObjectPublisherPrivate::deleteWrappedObject(QObject *object) const
+{
+ if (!wrappedObjects.contains(object)) {
+ qWarning() << "Not deleting non-wrapped object" << object;
+ return;
+ }
+ object->deleteLater();
+}
+
+QtMetaObjectPublisher::QtMetaObjectPublisher(QObject *parent)
+ : QObject(parent)
+ , d(new QtMetaObjectPublisherPrivate(this))
+{
+}
+
+QtMetaObjectPublisher::~QtMetaObjectPublisher()
+{
+
}
QVariantMap QtMetaObjectPublisher::classInfoForObjects(const QVariantMap &objectMap) const
@@ -68,7 +477,7 @@ QVariantMap QtMetaObjectPublisher::classInfoForObjects(const QVariantMap &object
QVariantMap ret;
QMap<QString, QVariant>::const_iterator it = objectMap.constBegin();
while (it != objectMap.constEnd()) {
- QObject* object = it.value().value<QObject*>();
+ QObject *object = it.value().value<QObject *>();
if (object) {
const QVariantMap &info = classInfoForObject(object);
if (!info.isEmpty()) {
@@ -90,15 +499,15 @@ QVariantMap QtMetaObjectPublisher::classInfoForObject(QObject *object) const
QVariantList qtSignals, qtMethods;
QVariantList qtProperties;
QVariantMap qtEnums;
- const QMetaObject* metaObject = object->metaObject();
+ const QMetaObject *metaObject = object->metaObject();
QSet<int> notifySignals;
- QSet<QString> properties;
+ QSet<QString> identifiers;
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;
+ identifiers << propertyName;
if (prop.hasNotifySignal()) {
notifySignals << prop.notifySignalIndex();
const int numParams = prop.notifySignal().parameterCount();
@@ -112,9 +521,9 @@ QVariantMap QtMetaObjectPublisher::classInfoForObject(QObject *object) const
if (notifySignal.length() == changedSuffix.length() + propertyName.length() &&
notifySignal.endsWith(changedSuffix) && notifySignal.startsWith(prop.name()))
{
- propertyInfo.append(1);
+ propertyInfo.append(QVariant::fromValue(QVariantList() << 1 << prop.notifySignalIndex()));
} else {
- propertyInfo.append(QString::fromLatin1(notifySignal));
+ propertyInfo.append(QVariant::fromValue(QVariantList() << QString::fromLatin1(notifySignal) << prop.notifySignalIndex()));
}
} else {
if (!prop.isConstant()) {
@@ -122,7 +531,7 @@ QVariantMap QtMetaObjectPublisher::classInfoForObject(QObject *object) const
"value updates in HTML will be broken!",
prop.name(), object->metaObject()->className());
}
- propertyInfo.append(0);
+ propertyInfo.append(QVariant::fromValue(QVariantList()));
}
propertyInfo.append(prop.read(object));
qtProperties.append(QVariant::fromValue(propertyInfo));
@@ -132,18 +541,22 @@ QVariantMap QtMetaObjectPublisher::classInfoForObject(QObject *object) const
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.
+ // optimize: skip overloaded methods/signals or property getters, on the JS side we can only
+ // call one of them anyways
+ // TODO: basic support for overloaded signals, methods
+ if (identifiers.contains(name)) {
continue;
}
- if (method.methodType() == QMetaMethod::Signal)
- qtSignals << name;
- else if (method.access() == QMetaMethod::Public)
- qtMethods << name;
+ identifiers << name;
+ // send data as array to client with format: [name, index]
+ const QVariant data = QVariant::fromValue(QVariantList() << name << i);
+ if (method.methodType() == QMetaMethod::Signal) {
+ qtSignals.append(data);
+ } else if (method.access() == QMetaMethod::Public) {
+ qtMethods.append(data);
+ }
}
for (int i = 0; i < metaObject->enumeratorCount(); ++i) {
QMetaEnum enumerator = metaObject->enumerator(i);
@@ -160,53 +573,159 @@ QVariantMap QtMetaObjectPublisher::classInfoForObject(QObject *object) const
return data;
}
-static QString objectId(QObject *object)
+void QtMetaObjectPublisher::registerObjects(const QVariantMap &objects)
{
- return QString::number(quintptr(object), 16);
+ if (d->propertyUpdatesInitialized) {
+ qWarning("Registered new object after initialization. This does not work!");
+ return;
+ }
+ const QMap<QString, QVariant>::const_iterator end = objects.end();
+ for (QMap<QString, QVariant>::const_iterator it = objects.begin(); it != end; ++it) {
+ QObject *object = it.value().value<QObject *>();
+ if (!object) {
+ qWarning("Invalid QObject given to register under name %s", qPrintable(it.key()));
+ continue;
+ }
+ d->registeredObjects[it.key()] = object;
+ d->registeredObjectIds[object] = it.key();
+ }
}
-QVariant QtMetaObjectPublisher::wrapObject(QObject *object)
+bool QtMetaObjectPublisher::handleRequest(const QJsonObject &message)
{
- if (!object)
- return QVariant();
-
- const QString& id = objectId(object);
+ if (!message.contains(KEY_DATA)) {
+ return false;
+ }
- const WrapMapCIt& p = m_wrappedObjects.constFind(id);
- if (p != m_wrappedObjects.constEnd())
- return p.value().second;
+ const QJsonObject &payload = message.value(KEY_DATA).toObject();
+ if (!payload.contains(KEY_TYPE)) {
+ return false;
+ }
- QVariantMap objectInfo;
- objectInfo[KEY_QOBJECT] = true;
- objectInfo[KEY_ID] = id;
- objectInfo[KEY_DATA] = classInfoForObject(object);
+ const QString &type = payload.value(KEY_TYPE).toString();
+ if (type == TYPE_IDLE) {
+ d->setClientIsIdle(true);
+ return true;
+ } else if (type == TYPE_INIT) {
+ if (!d->blockUpdates) {
+ d->initializeClients();
+ } else {
+ d->pendingInit = true;
+ }
+ return true;
+ } else if (type == TYPE_DEBUG) {
+ static QTextStream out(stdout);
+ out << "DEBUG: " << payload.value(KEY_MESSAGE).toString() << endl;
+ return true;
+ } else if (payload.contains(KEY_OBJECT)) {
+ const QString &objectName = payload.value(KEY_OBJECT).toString();
+ QObject *object = d->registeredObjects.value(objectName);
+ if (!object) {
+ qWarning() << "Unknown object encountered" << objectName;
+ return false;
+ }
- m_wrappedObjects.insert(id, WrapInfo(object, objectInfo));
- connect(object, SIGNAL(destroyed(QObject*)), SLOT(wrappedObjectDestroyed(QObject*)));
+ if (type == TYPE_INVOKE_METHOD) {
+ return d->invokeMethod(object, payload.value(KEY_METHOD).toInt(-1), payload.value(KEY_ARGS).toArray(), message.value(KEY_ID));
+ } else if (type == TYPE_CONNECT_TO_SIGNAL) {
+ d->signalHandler.connectTo(object, payload.value(KEY_SIGNAL).toInt(-1));
+ return true;
+ } else if (type == TYPE_DISCONNECT_FROM_SIGNAL) {
+ d->signalHandler.disconnectFrom(object, payload.value(KEY_SIGNAL).toInt(-1));
+ return true;
+ } else if (type == TYPE_SET_PROPERTY) {
+ // TODO: use property indices
+ const QString &propertyName = payload.value(KEY_PROPERTY).toString();
+ const int propertyIdx = object->metaObject()->indexOfProperty(qPrintable(propertyName));
+ if (propertyIdx == -1) {
+ qWarning() << "Cannot set unknown property" << propertyName << "of object" << objectName;
+ return false;
+ }
+ object->metaObject()->property(propertyIdx).write(object, payload.value(KEY_VALUE).toVariant());
+ return true;
+ }
+ }
+ return false;
+}
- return objectInfo;
+QWebChannel *QtMetaObjectPublisher::webChannel() const
+{
+ return d->webChannel;
}
-QObject *QtMetaObjectPublisher::unwrapObject(const QString& id) const
+void QtMetaObjectPublisher::setWebChannel(QWebChannel *webChannel)
{
- const WrapMapCIt& p = m_wrappedObjects.constFind(id);
- if (p != m_wrappedObjects.constEnd())
- return p.value().first;
- return 0;
+ if (d->webChannel == webChannel) {
+ return;
+ }
+
+ d->webChannel = webChannel;
+
+ emit webChannelChanged(webChannel);
}
-void QtMetaObjectPublisher::wrappedObjectDestroyed(QObject* object)
+bool QtMetaObjectPublisher::blockUpdates() const
{
- const QString& id = objectId(object);
- m_wrappedObjects.remove(id);
- emit wrappedObjectDestroyed(id);
+ return d->blockUpdates;
}
-void QtMetaObjectPublisher::deleteWrappedObject(QObject* object) const
+void QtMetaObjectPublisher::setBlockUpdates(bool block)
{
- if (!m_wrappedObjects.contains(objectId(object))) {
- qWarning() << "Not deleting non-wrapped object" << object;
+ if (d->blockUpdates == block) {
return;
}
- object->deleteLater();
+ d->blockUpdates = block;
+
+ if (!d->blockUpdates) {
+ if (d->pendingInit) {
+ d->initializeClients();
+ } else {
+ d->sendPendingPropertyUpdates();
+ }
+ } else if (d->timer.isActive()) {
+ d->timer.stop();
+ }
+
+ emit blockUpdatesChanged(block);
+}
+
+void QtMetaObjectPublisher::bench_ensureUpdatesInitialized()
+{
+ if (!d->propertyUpdatesInitialized) {
+ d->initializeClients();
+ }
+}
+
+void QtMetaObjectPublisher::bench_sendPendingPropertyUpdates()
+{
+ d->clientIsIdle = true;
+ d->sendPendingPropertyUpdates();
+}
+
+void QtMetaObjectPublisher::bench_initializeClients()
+{
+ d->propertyUpdatesInitialized = false;
+ d->signalToPropertyMap.clear();
+ d->signalHandler.clear();
+ d->initializeClients();
+}
+
+void QtMetaObjectPublisher::bench_registerObjects(const QVariantMap &objects)
+{
+ d->propertyUpdatesInitialized = false;
+ registerObjects(objects);
+}
+
+bool QtMetaObjectPublisher::test_clientIsIdle() const
+{
+ return d->clientIsIdle;
+}
+
+void QtMetaObjectPublisher::timerEvent(QTimerEvent *event)
+{
+ if (event->timerId() == d->timer.timerId()) {
+ d->sendPendingPropertyUpdates();
+ } else {
+ QObject::timerEvent(event);
+ }
}
diff --git a/src/qtmetaobjectpublisher.h b/src/qtmetaobjectpublisher.h
index 2fd3c0d..88a54c4 100644
--- a/src/qtmetaobjectpublisher.h
+++ b/src/qtmetaobjectpublisher.h
@@ -46,42 +46,69 @@
#define QTMETAOBJECTPUBLISHER_H
#include <QObject>
-#include <QVariantMap>
-#include <QQuickItem>
+#include <QVariant>
-// NOTE: QQuickItem inheritance required to enable QML item nesting (i.e. Timer in MetaObjectPublisher)
-class QtMetaObjectPublisher : public QQuickItem
+class QWebChannel;
+
+struct QtMetaObjectPublisherPrivate;
+
+class QtMetaObjectPublisher : public QObject
{
Q_OBJECT
+ Q_PROPERTY(QWebChannel *webChannel READ webChannel WRITE setWebChannel NOTIFY webChannelChanged);
+ Q_PROPERTY(bool blockUpdates READ blockUpdates WRITE setBlockUpdates NOTIFY blockUpdatesChanged);
+
public:
- explicit QtMetaObjectPublisher(QQuickItem *parent = 0);
+ explicit QtMetaObjectPublisher(QObject *parent = 0);
+ virtual ~QtMetaObjectPublisher();
Q_INVOKABLE QVariantMap classInfoForObjects(const QVariantMap &objects) const;
Q_INVOKABLE QVariantMap classInfoForObject(QObject *object) const;
- /// wrap object and return class info
- Q_INVOKABLE QVariant wrapObject(QObject *object);
- /// Search object by id and return it, or null if it could not be found.
- Q_INVOKABLE QObject *unwrapObject(const QString &id) const;
- /// Invoke delete later on @p object, but only if it is a wrapped object.
- Q_INVOKABLE void deleteWrappedObject(QObject *object) const;
+ /**
+ * Register a map of string ID to QObject* objects.
+ *
+ * The properties, signals and public methods of the QObject are
+ * published to the remote client, where an object with the given identifier
+ * is constructed.
+ *
+ * TODO: This must be called, before clients are initialized.
+ */
+ Q_INVOKABLE void registerObjects(const QVariantMap &objects);
+
+ /**
+ * Handle the given WebChannel client request and potentially give a response.
+ *
+ * @return true if the request was handled, false otherwise.
+ */
+ Q_INVOKABLE bool handleRequest(const QJsonObject &message);
+
+ QWebChannel *webChannel() const;
+ void setWebChannel(QWebChannel *webChannel);
+
+ /**
+ * When updates are blocked, no property updates are transmitted to remote clients.
+ */
+ bool blockUpdates() const;
+ void setBlockUpdates(bool block);
+
+ /// TODO: cleanup: rewrite tests in C++ and access PIMPL data from there
+ Q_INVOKABLE void bench_ensureUpdatesInitialized();
+ Q_INVOKABLE void bench_sendPendingPropertyUpdates();
+ Q_INVOKABLE void bench_registerObjects(const QVariantMap &objects);
+ Q_INVOKABLE void bench_initializeClients();
+ Q_INVOKABLE bool test_clientIsIdle() const;
signals:
- void wrappedObjectDestroyed(const QString& id);
+ void webChannelChanged(QWebChannel *channel);
+ void blockUpdatesChanged(bool block);
-private slots:
- void wrappedObjectDestroyed(QObject* object);
+protected:
+ virtual void timerEvent(QTimerEvent *);
private:
- /// Pairs of QObject and generated object info
- typedef QPair<QObject *, QVariantMap> WrapInfo;
- /// Maps object id to wrap info
- typedef QHash<QString, WrapInfo> WrapMap;
- /// Const iterator for map
- typedef WrapMap::const_iterator WrapMapCIt;
-
- /// Map of wrapped objects
- WrapMap m_wrappedObjects;
+ QScopedPointer<QtMetaObjectPublisherPrivate> d;
+ friend struct QtMetaObjectPublisherPrivate;
};
#endif // QTMETAOBJECTPUBLISHER_H
diff --git a/src/qwebchannel_plugin.cpp b/src/qwebchannel_plugin.cpp
index a5834ba..13a38a5 100644
--- a/src/qwebchannel_plugin.cpp
+++ b/src/qwebchannel_plugin.cpp
@@ -49,6 +49,6 @@
void QWebChannelPlugin::registerTypes(const char *uri)
{
qmlRegisterType<QWebChannel>(uri, 1, 0, "WebChannel");
- qmlRegisterType<QtMetaObjectPublisher>(uri, 1, 0, "MetaObjectPublisherImpl");
+ qmlRegisterType<QtMetaObjectPublisher>(uri, 1, 0, "MetaObjectPublisher");
}
diff --git a/src/signalhandler.h b/src/signalhandler.h
new file mode 100644
index 0000000..0db6085
--- /dev/null
+++ b/src/signalhandler.h
@@ -0,0 +1,278 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt-project.org/legal
+*
+** This file is part of the QtCore 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 Digia. For licensing terms and
+** conditions see http://qt.digia.com/licensing. For further information
+** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, Digia gives you certain additional
+** rights. These rights are described in the Digia Qt LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3.0 as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 3.0 requirements will be
+** met: http://www.gnu.org/copyleft/gpl.html.
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef SIGNALHANDLER_H
+#define SIGNALHANDLER_H
+
+#include <QObject>
+#include <QHash>
+#include <QVector>
+#include <QMetaMethod>
+#include <QDebug>
+
+/**
+ * The signal handler is similar to QSignalSpy, but geared towards the usecase of the web channel.
+ *
+ * It allows connecting to any number of signals of arbitrary objects and forwards the signal
+ * invocations to the Receiver by calling its signalEmitted function, which takes the object,
+ * signal index and a QVariantList of arguments.
+ */
+template<class Receiver>
+class SignalHandler : public QObject
+{
+public:
+ SignalHandler(Receiver *receiver, QObject *parent = 0)
+ : QObject(parent)
+ , m_receiver(receiver)
+ {
+ }
+
+ /**
+ * Connect to a signal of @p object identified by @p signalIndex.
+ *
+ * If the handler is already connected to the signal, an internal counter is increased,
+ * i.e. the handler never connects multiple times to the same signal.
+ */
+ void connectTo(const QObject *object, const int signalIndex);
+
+ /**
+ * Decrease the connection counter for the connection to the given signal.
+ *
+ * When the counter drops to zero, the connection is disconnected.
+ */
+ void disconnectFrom(const QObject *object, const int signalIndex);
+
+ /**
+ * @internal
+ *
+ * Custom implementation of qt_metacall which calls dispatch() for connected signals.
+ */
+ int qt_metacall(QMetaObject::Call call, int methodId, void **args) Q_DECL_OVERRIDE;
+
+ /**
+ * Reset all connections, useful for benchmarks.
+ */
+ void clear();
+
+private:
+ /**
+ * Exctract the arguments of a signal call and pass them to the receiver.
+ *
+ * The @p argumentData is converted to a QVariantList and then passed to the receiver's
+ * signalEmitted method.
+ */
+ void dispatch(const QObject *object, const int signalIdx, void **argumentData);
+
+ Receiver *m_receiver;
+
+ // maps meta object -> signalIndex -> list of arguments
+ // NOTE: This data is "leaked" on disconnect until deletion of the handler, is this a problem?
+ typedef QVector<int> ArgumentTypeList;
+ typedef QHash<int, ArgumentTypeList> SignalArgumentHash;
+ QHash<const QMetaObject *, SignalArgumentHash > m_signalArgumentTypes;
+
+ /*
+ * Tracks how many connections are active to object signals.
+ *
+ * Maps object -> signalIndex -> pair of connection and number of connections
+ *
+ * Note that the handler is connected to the signal only once, whereas clients
+ * may have connected multiple times.
+ *
+ * TODO: Move more of this logic to the HTML client side, esp. the connection counting.
+ */
+ typedef QPair<QMetaObject::Connection, int> ConnectionPair;
+ typedef QHash<int, ConnectionPair> SignalConnectionHash;
+ typedef QHash<const QObject*, SignalConnectionHash> ConnectionHash;
+ ConnectionHash m_connectionsCounter;
+};
+
+/**
+ * Find and return the signal of index @p signalIndex in the meta object of @p object and return it.
+ *
+ * The return value is also verified to ensure it is a signal.
+ */
+QMetaMethod findSignal(const QMetaObject *metaObject, const int signalIndex)
+{
+ QMetaMethod signal = metaObject->method(signalIndex);
+ if (!signal.isValid()) {
+ qWarning("Cannot find signal with index %d of object %s", signalIndex, metaObject->className());
+ return QMetaMethod();
+ }
+ Q_ASSERT(signal.methodType() == QMetaMethod::Signal);
+ return signal;
+}
+
+template<class Receiver>
+void SignalHandler<Receiver>::connectTo(const QObject *object, const int signalIndex)
+{
+ const QMetaObject *metaObject = object->metaObject();
+ const QMetaMethod &signal = findSignal(metaObject, signalIndex);
+ if (!signal.isValid()) {
+ return;
+ }
+
+ ConnectionPair &connectionCounter = m_connectionsCounter[object][signalIndex];
+ if (connectionCounter.first) {
+ // increase connection counter if already connected
+ ++connectionCounter.second;
+ return;
+ } // otherwise not yet connected, do so now
+
+ const int memberOffset = QObject::staticMetaObject.methodCount();
+ QMetaObject::Connection connection = QMetaObject::connect(object, signal.methodIndex(), this, memberOffset, Qt::DirectConnection, 0);
+ if (!connection) {
+ qWarning() << "SignalHandler: QMetaObject::connect returned false. Unable to connect to" << object << signal.name() << signal.methodSignature();
+ return;
+ }
+ connectionCounter.first = connection;
+ connectionCounter.second = 1;
+
+ if (!m_signalArgumentTypes.value(metaObject).contains(signal.methodIndex())) {
+ // find the type ids of the signal parameters, see also QSignalSpy::initArgs
+ QVector<int> args;
+ args.reserve(signal.parameterCount());
+ for (int i = 0; i < signal.parameterCount(); ++i) {
+ int tp = signal.parameterType(i);
+ if (tp == QMetaType::UnknownType && object) {
+ void *argv[] = { &tp, &i };
+ QMetaObject::metacall(const_cast<QObject *>(object),
+ QMetaObject::RegisterMethodArgumentMetaType,
+ signal.methodIndex(), argv);
+ if (tp == -1) {
+ tp = QMetaType::UnknownType;
+ }
+ }
+ if (tp == QMetaType::UnknownType) {
+ Q_ASSERT(tp != QMetaType::Void); // void parameter => metaobject is corrupt
+ qWarning("Don't know how to handle '%s', use qRegisterMetaType to register it.",
+ signal.parameterNames().at(i).constData());
+ }
+ args << tp;
+ }
+
+ m_signalArgumentTypes[metaObject][signal.methodIndex()] = args;
+ }
+}
+
+template<class Receiver>
+void SignalHandler<Receiver>::dispatch(const QObject *object, const int signalIdx, void **argumentData)
+{
+ Q_ASSERT(m_signalArgumentTypes.contains(object->metaObject()));
+ const QHash<int, QVector<int> > &objectSignalArgumentTypes = m_signalArgumentTypes.value(object->metaObject());
+ QHash<int, QVector<int> >::const_iterator signalIt = objectSignalArgumentTypes.constFind(signalIdx);
+ if (signalIt == objectSignalArgumentTypes.constEnd()) {
+ // not connected to this signal, skip
+ return;
+ }
+ const QVector<int> &argumentTypes = *signalIt;
+ QVariantList arguments;
+ arguments.reserve(argumentTypes.count());
+ // TODO: basic overload resolution based on number of arguments?
+ for (int i = 0; i < argumentTypes.count(); ++i) {
+ const QMetaType::Type type = static_cast<QMetaType::Type>(argumentTypes.at(i));
+ QVariant arg;
+ if (type == QMetaType::QVariant) {
+ arg = *reinterpret_cast<QVariant *>(argumentData[i + 1]);
+ } else {
+ arg = QVariant(type, argumentData[i + 1]);
+ }
+ arguments.append(arg);
+ }
+ m_receiver->signalEmitted(object, signalIdx, arguments);
+}
+
+template<class Receiver>
+void SignalHandler<Receiver>::disconnectFrom(const QObject *object, const int signalIndex)
+{
+ Q_ASSERT(m_connectionsCounter.value(object).contains(signalIndex));
+ ConnectionPair &connection = m_connectionsCounter[object][signalIndex];
+ --connection.second;
+ if (!connection.second || !connection.first) {
+ QObject::disconnect(connection.first);
+ m_connectionsCounter[object].remove(signalIndex);
+ if (m_connectionsCounter[object].isEmpty()) {
+ m_connectionsCounter.remove(object);
+ }
+ }
+}
+
+template<class Receiver>
+int SignalHandler<Receiver>::qt_metacall(QMetaObject::Call call, int methodId, void **args)
+{
+ methodId = QObject::qt_metacall(call, methodId, args);
+ if (methodId < 0)
+ return methodId;
+
+ if (call == QMetaObject::InvokeMetaMethod) {
+ if (methodId == 0) {
+ Q_ASSERT(sender());
+ Q_ASSERT(senderSignalIndex() != -1);
+ dispatch(sender(), senderSignalIndex(), args);
+ static const int destroyedIndex = metaObject()->indexOfSignal("destroyed(QObject*)");
+ if (senderSignalIndex() == destroyedIndex) {
+ ConnectionHash::iterator it = m_connectionsCounter.find(sender());
+ Q_ASSERT(it != m_connectionsCounter.end());
+ foreach (const ConnectionPair &connection, *it) {
+ QObject::disconnect(connection.first);
+ }
+ m_connectionsCounter.erase(it);
+ }
+ }
+ --methodId;
+ }
+ return methodId;
+}
+
+template<class Receiver>
+void SignalHandler<Receiver>::clear()
+{
+ foreach (const SignalConnectionHash &connections, m_connectionsCounter) {
+ foreach (const ConnectionPair &connection, connections) {
+ QObject::disconnect(connection.first);
+ }
+ }
+ m_connectionsCounter.clear();
+ m_signalArgumentTypes.clear();
+}
+
+#endif // SIGNALHANDLER_H
diff --git a/src/src.pri b/src/src.pri
index 408b430..5ae01f3 100644
--- a/src/src.pri
+++ b/src/src.pri
@@ -6,4 +6,6 @@ SOURCES += \
HEADERS += \
$$PWD/qwebchannel.h \
$$PWD/qtmetaobjectpublisher.h \
- $$PWD/qwebsocketserver.h
+ $$PWD/qwebsocketserver.h \
+ $$PWD/variantargument.h \
+ $$PWD/signalhandler.h
diff --git a/src/src.pro b/src/src.pro
index 5b860ba..bd2e3c7 100644
--- a/src/src.pro
+++ b/src/src.pro
@@ -17,15 +17,13 @@ RESOURCES += \
OTHER_FILES = qmldir \
webchannel.js \
- qobject.js \
- MetaObjectPublisher.qml
+ qobject.js
target.path = $$[QT_INSTALL_QML]/$$TARGETPATH
# extra files that need to be deployed to $$TARGETPATH
DEPLOY_FILES = \
- qmldir \
- MetaObjectPublisher.qml
+ qmldir
for(FILE, DEPLOY_FILES): qmldir.files += $$PWD/$$FILE
qmldir.path += $$[QT_INSTALL_QML]/$$TARGETPATH
diff --git a/src/variantargument.h b/src/variantargument.h
new file mode 100644
index 0000000..d5382bd
--- /dev/null
+++ b/src/variantargument.h
@@ -0,0 +1,103 @@
+/****************************************************************************
+**
+** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies).
+** Copyright (C) 2013 Klarälvdalens Datakonsult AB, a KDAB Group company, info@kdab.com, author Milian Wolff <milian.wolff@kdab.com>
+** Contact: http://www.qt-project.org/legal
+*
+** This file is part of the QtCore 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 Digia. For licensing terms and
+** conditions see http://qt.digia.com/licensing. For further information
+** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software
+** Foundation and appearing in the file LICENSE.LGPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU Lesser General Public License version 2.1 requirements
+** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
+**
+** In addition, as a special exception, Digia gives you certain additional
+** rights. These rights are described in the Digia Qt LGPL Exception
+** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
+**
+** GNU General Public License Usage
+** Alternatively, this file may be used under the terms of the GNU
+** General Public License version 3.0 as published by the Free Software
+** Foundation and appearing in the file LICENSE.GPL included in the
+** packaging of this file. Please review the following information to
+** ensure the GNU General Public License version 3.0 requirements will be
+** met: http://www.gnu.org/copyleft/gpl.html.
+**
+**
+** $QT_END_LICENSE$
+**
+****************************************************************************/
+
+#ifndef VARIANTARGUMENT_H
+#define VARIANTARGUMENT_H
+
+#include <QVariant>
+#include <QMetaType>
+
+/**
+ * RAII QVariant to Q[Generic]Argument conversion
+ */
+class VariantArgument
+{
+public:
+ explicit VariantArgument()
+ : m_data(0)
+ , m_paramType(0)
+ {
+ }
+
+ /// TODO: test with C++ methods that don't take a QVariant as arg
+ /// also test conversions
+ void setValue(const QVariant &value, int paramType)
+ {
+ if (m_data) {
+ QMetaType::destroy(m_paramType, m_data);
+ m_name.clear();
+ m_data = 0;
+ }
+
+ m_paramType = paramType;
+
+ if (value.isValid()) {
+ m_name = value.typeName();
+ m_data = QMetaType::create(m_paramType, value.constData());
+ }
+ }
+
+ ~VariantArgument()
+ {
+ if (m_data) {
+ QMetaType::destroy(m_paramType, m_data);
+ m_data = 0;
+ }
+ }
+
+ operator QGenericArgument() const
+ {
+ if (!m_data) {
+ return QGenericArgument();
+ }
+ return QGenericArgument(m_name.constData(), m_data);
+ }
+
+private:
+ Q_DISABLE_COPY(VariantArgument)
+
+ QByteArray m_name;
+ void* m_data;
+ int m_paramType;
+};
+
+#endif // VARIANTARGUMENT_H