diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/MetaObjectPublisher.qml | 384 | ||||
-rw-r--r-- | src/qmldir | 1 | ||||
-rw-r--r-- | src/qobject.js | 53 | ||||
-rw-r--r-- | src/qtmetaobjectpublisher.cpp | 629 | ||||
-rw-r--r-- | src/qtmetaobjectpublisher.h | 73 | ||||
-rw-r--r-- | src/qwebchannel_plugin.cpp | 2 | ||||
-rw-r--r-- | src/signalhandler.h | 278 | ||||
-rw-r--r-- | src/src.pri | 4 | ||||
-rw-r--r-- | src/src.pro | 6 | ||||
-rw-r--r-- | src/variantargument.h | 103 |
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() - } -} @@ -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 |