Enable gadget wrapping for custom value types
authorSimon Hausmann <simon.hausmann@theqtcompany.com>
Thu, 20 Nov 2014 09:42:34 +0000 (10:42 +0100)
committerSimon Hausmann <simon.hausmann@digia.com>
Fri, 2 Jan 2015 14:06:46 +0000 (15:06 +0100)
[ChangeLog][QtQml] Custom C++ value types annotated with Q_GADGET are now fully
accessible in the QML and QJSEngine JavaScript environment.

QJSEngine::toScriptValue can be used for injection and fromScriptValue to
extraction. The QML "built-in" gadget wrappers for QPoint and the gui types are
not exposed this way, toScriptValue(point) will still return an opaque QVariant
wrapper. We could expose the core types right away, but then we would be
lacking an API to enable use of the Gui types that are registered in QtQuick.

It would be better to make the core types in qtbase gadgets and thus enable
them without the need for hooks and init functions to be called by the user.

Task-number: QTBUG-29769
Change-Id: I8179cd599bdc1209ff61cfdbdda419cb400296bb
Reviewed-by: Lars Knoll <lars.knoll@digia.com>
src/qml/doc/src/cppintegration/data.qdoc
src/qml/jsapi/qjsengine.cpp
src/qml/jsapi/qjsengine_p.h
src/qml/qml/qqmlvaluetype.cpp
src/qml/qml/qqmlvaluetypewrapper.cpp
src/qml/qml/qqmlvaluetypewrapper_p.h
src/qml/qml/v8/qv8engine.cpp
tests/auto/qml/qqmlvaluetypes/data/customvaluetype.qml [new file with mode: 0644]
tests/auto/qml/qqmlvaluetypes/qqmlvaluetypes.pro
tests/auto/qml/qqmlvaluetypes/tst_qqmlvaluetypes.cpp

index c083d63..26d3060 100644 (file)
@@ -307,6 +307,33 @@ them with default constructed values, do not use the indexed delete operator
 ("delete sequence[i]") but instead use the \c {splice} function
 ("sequence.splice(startIndex, deleteCount)").
 
+\section2 Value types
+
+Some value types in Qt such as QPoint are represented in JavaScript as objects
+that have the same properties and functions like in the C++ API. The same
+representation is possible with custom C++ value types. To enable a custom
+value type with the QML engine, the class declaration needs to be annotated
+with \c{Q_GADGET}. Properties that are intended to be visible in the JavaScript
+representation need to be declared with \c Q_PROPERTY. Similarly functions need
+to be marked with \c Q_INVOKABLE. This is the same with QObject based C++ APIs.
+For example, the \c Actor class below is annotated as gadget and has
+properties:
+
+\code
+ class Actor
+ {
+     Q_GADGET
+     Q_PROPERTY(QString name READ name WRITE setName)
+ public:
+     QString name() const { return m_name; }
+     void setName(const QString &name) { m_name = name; }
+
+ private:
+     QString m_name;
+ }
+
+ Q_DECLARE_METATYPE(Actor)
+\endcode
 
 \section1 Enumeration Types
 
index 0989101..8525aed 100644 (file)
@@ -530,7 +530,7 @@ bool QJSEngine::convertV2(const QJSValue &value, int type, void *ptr)
 
     Creates a QJSValue with the given \a value.
 
-    \sa fromScriptValue()
+    \sa fromScriptValue(), newVariant()
 */
 
 /*! \fn T QJSEngine::fromScriptValue(const QJSValue &value)
@@ -541,6 +541,11 @@ bool QJSEngine::convertV2(const QJSValue &value, int type, void *ptr)
 */
 
 
+QJSEnginePrivate *QJSEnginePrivate::get(QV4::ExecutionEngine *e)
+{
+    return e->v8Engine->publicEngine()->d_func();
+}
+
 QJSEnginePrivate::~QJSEnginePrivate()
 {
     for (QHash<const QMetaObject *, QQmlPropertyCache *>::Iterator iter = propertyCache.begin(); iter != propertyCache.end(); ++iter)
index eba8ffb..3f7e91b 100644 (file)
@@ -54,6 +54,10 @@ QT_BEGIN_NAMESPACE
 
 class QQmlPropertyCache;
 
+namespace QV4 {
+struct ExecutionEngine;
+}
+
 class QJSEnginePrivate : public QObjectPrivate
 {
     Q_DECLARE_PUBLIC(QJSEngine)
@@ -61,6 +65,7 @@ class QJSEnginePrivate : public QObjectPrivate
 public:
     static QJSEnginePrivate* get(QJSEngine*e) { return e->d_func(); }
     static const QJSEnginePrivate* get(const QJSEngine*e) { return e->d_func(); }
+    static QJSEnginePrivate* get(QV4::ExecutionEngine *e);
 
     QJSEnginePrivate() : mutex(QMutex::Recursive) {}
     ~QJSEnginePrivate();
index e22e0f8..e901a9c 100644 (file)
@@ -107,6 +107,9 @@ const QMetaObject *QQmlValueTypeFactoryImpl::metaObjectForMetaType(int t)
         break;
     }
 
+    QMetaType metaType(t);
+    if (metaType.flags() & QMetaType::IsGadget)
+        return metaType.metaObject();
     return 0;
 }
 
index 8ed1169..00b206f 100644 (file)
@@ -133,7 +133,7 @@ bool QQmlValueTypeReference::readReferenceValue() const
             if (QQmlValueTypeFactory::isValueType(variantReferenceType)) {
                 QQmlPropertyCache *cache = 0;
                 if (const QMetaObject *mo = QQmlValueTypeFactory::metaObjectForMetaType(variantReferenceType))
-                    cache = QQmlEnginePrivate::get(engine())->cache(mo);
+                    cache = QJSEnginePrivate::get(engine())->cache(mo);
                 if (d()->gadgetPtr)
                     QMetaType::destroy(d()->metaType, d()->gadgetPtr);
                 d()->gadgetPtr = 0;
@@ -177,7 +177,7 @@ ReturnedValue QQmlValueTypeWrapper::create(ExecutionEngine *engine, QObject *obj
     ScopedObject proto(scope, engine->qmlExtensions()->valueTypeWrapperPrototype);
     r->setPrototype(proto);
     r->d()->object = object; r->d()->property = property;
-    r->d()->propertyCache = QQmlEnginePrivate::get(engine)->cache(metaObject);
+    r->d()->propertyCache = QJSEnginePrivate::get(engine)->cache(metaObject);
     r->d()->metaType = typeId;
     r->d()->gadgetPtr = QMetaType::create(r->d()->metaType);
     return r->asReturnedValue();
@@ -191,7 +191,7 @@ ReturnedValue QQmlValueTypeWrapper::create(ExecutionEngine *engine, const QVaria
     Scoped<QQmlValueTypeWrapper> r(scope, engine->memoryManager->alloc<QQmlValueTypeWrapper>(engine));
     ScopedObject proto(scope, engine->qmlExtensions()->valueTypeWrapperPrototype);
     r->setPrototype(proto);
-    r->d()->propertyCache = QQmlEnginePrivate::get(engine)->cache(metaObject);
+    r->d()->propertyCache = QJSEnginePrivate::get(engine)->cache(metaObject);
     r->d()->metaType = typeId;
     r->d()->gadgetPtr = QMetaType::create(r->d()->metaType);
     r->d()->setValue(value);
@@ -206,6 +206,13 @@ QVariant QQmlValueTypeWrapper::toVariant() const
     return d()->toVariant();
 }
 
+void QQmlValueTypeWrapper::toGadget(void *data) const
+{
+    int typeId = d()->metaType;
+    QMetaType::destruct(typeId, data);
+    QMetaType::construct(typeId, data, d()->gadget());
+}
+
 void QQmlValueTypeWrapper::destroy(Heap::Base *that)
 {
     Heap::QQmlValueTypeWrapper *w = static_cast<Heap::QQmlValueTypeWrapper *>(that);
index ad88326..2d0fbcb 100644 (file)
@@ -84,6 +84,7 @@ public:
     static ReturnedValue create(ExecutionEngine *engine, const QVariant &, const QMetaObject *metaObject, int typeId);
 
     QVariant toVariant() const;
+    void toGadget(void *data) const;
     bool isEqual(const QVariant& value);
 
     static ReturnedValue get(Managed *m, String *name, bool *hasProperty);
index cf66c5c..0a309c2 100644 (file)
@@ -700,10 +700,14 @@ QV4::ReturnedValue QV8Engine::metaTypeToJS(int type, const void *data)
             QByteArray typeName = QMetaType::typeName(type);
             if (typeName.endsWith('*') && !*reinterpret_cast<void* const *>(data)) {
                 return QV4::Encode::null();
-            } else {
-                // Fall back to wrapping in a QVariant.
-                return QV4::Encode(m_v4Engine->newVariantObject(QVariant(type, data)));
             }
+            QMetaType mt(type);
+            if (mt.flags() & QMetaType::IsGadget) {
+                Q_ASSERT(mt.metaObject());
+                return QV4::QQmlValueTypeWrapper::create(m_v4Engine, QVariant(type, data), mt.metaObject(), type);
+            }
+            // Fall back to wrapping in a QVariant.
+            return QV4::Encode(m_v4Engine->newVariantObject(QVariant(type, data)));
         }
     }
     Q_UNREACHABLE();
@@ -835,6 +839,14 @@ bool QV8Engine::metaTypeFromJS(const QV4::ValueRef value, int type, void *data)
     ;
     }
 
+    {
+        QV4::Scoped<QV4::QQmlValueTypeWrapper> vtw(scope, value);
+        if (vtw && vtw->d()->metaType == type) {
+            vtw->toGadget(data);
+            return true;
+        }
+    }
+
 #if 0
     if (isQtVariant(value)) {
         const QVariant &var = variantValue(value);
diff --git a/tests/auto/qml/qqmlvaluetypes/data/customvaluetype.qml b/tests/auto/qml/qqmlvaluetypes/data/customvaluetype.qml
new file mode 100644 (file)
index 0000000..7ae7347
--- /dev/null
@@ -0,0 +1,8 @@
+import QtQuick 2.0
+import Test 1.0
+
+TypeWithCustomValueType {
+    desk {
+        monitorCount: 3
+    }
+}
index e318966..e36be45 100644 (file)
@@ -15,3 +15,6 @@ CONFIG += parallel_test
 
 QT += core-private gui-private  qml-private quick-private gui testlib
 DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0
+
+DISTFILES += \
+    data/customvaluetype.qml
index e3703ea..f07a34b 100644 (file)
@@ -88,6 +88,8 @@ private slots:
     void initializeByWrite();
     void groupedInterceptors();
     void groupedInterceptors_data();
+    void customValueType();
+    void customValueTypeInQml();
 
 private:
     QQmlEngine engine;
@@ -1422,6 +1424,87 @@ void tst_qqmlvaluetypes::groupedInterceptors()
     delete object;
 }
 
+struct MyDesk
+{
+    Q_PROPERTY(int monitorCount MEMBER monitorCount)
+    Q_GADGET
+public:
+    MyDesk() : monitorCount(1) {}
+
+    int monitorCount;
+};
+
+bool operator==(const MyDesk &lhs, const MyDesk &rhs)
+{ return lhs.monitorCount == rhs.monitorCount; }
+bool operator!=(const MyDesk &lhs, const MyDesk &rhs)
+{ return lhs.monitorCount != rhs.monitorCount; }
+
+Q_DECLARE_METATYPE(MyDesk)
+
+struct MyOffice
+{
+    Q_PROPERTY(int chairs MEMBER m_chairs)
+    Q_PROPERTY(MyDesk desk READ desk WRITE setDesk)
+    Q_GADGET
+public:
+    MyOffice() : m_chairs(0) {}
+
+    MyDesk desk() const { return m_desk; }
+    void setDesk(const MyDesk &d) { m_desk = d; }
+
+    int m_chairs;
+    MyDesk m_desk;
+};
+
+Q_DECLARE_METATYPE(MyOffice)
+
+void tst_qqmlvaluetypes::customValueType()
+{
+    QJSEngine engine;
+
+    MyOffice cppOffice;
+    cppOffice.m_chairs = 2;
+
+    QJSValue office = engine.toScriptValue(cppOffice);
+    QCOMPARE(office.property("chairs").toInt(), 2);
+    office.setProperty("chairs", 1);
+    QCOMPARE(office.property("chairs").toInt(), 1);
+    QCOMPARE(cppOffice.m_chairs, 2);
+
+    QJSValue jsDesk = office.property("desk");
+    QCOMPARE(jsDesk.property("monitorCount").toInt(), 1);
+    jsDesk.setProperty("monitorCount", 2);
+    QCOMPARE(jsDesk.property("monitorCount").toInt(), 2);
+
+    QCOMPARE(cppOffice.desk().monitorCount, 1);
+
+    office.setProperty("desk", jsDesk);
+    cppOffice = engine.fromScriptValue<MyOffice>(office);
+    QCOMPARE(cppOffice.m_chairs, 1);
+    QCOMPARE(cppOffice.desk().monitorCount, 2);
+}
+
+class TypeWithCustomValueType : public QObject
+{
+    Q_OBJECT
+    Q_PROPERTY(MyDesk desk MEMBER m_desk)
+public:
+
+    MyDesk m_desk;
+};
+
+void tst_qqmlvaluetypes::customValueTypeInQml()
+{
+    qmlRegisterType<TypeWithCustomValueType>("Test", 1, 0, "TypeWithCustomValueType");
+    QQmlComponent component(&engine, testFileUrl("customvaluetype.qml"));
+    QScopedPointer<QObject> object(component.create());
+    QVERIFY(!object.isNull());
+
+    TypeWithCustomValueType *t = qobject_cast<TypeWithCustomValueType*>(object.data());
+    Q_ASSERT(t);
+    QCOMPARE(t->m_desk.monitorCount, 3);
+}
+
 QTEST_MAIN(tst_qqmlvaluetypes)
 
 #include "tst_qqmlvaluetypes.moc"