aboutsummaryrefslogtreecommitdiffstats
path: root/src/qml/qml/qqmlpropertybinding.cpp
blob: c8a7e6256ab14441ae8e9a5f4b1e9ed7bdaf30e0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
// Copyright (C) 2020 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qqmlpropertybinding_p.h"

#include <private/qqmlbinding_p.h>
#include <private/qqmlglobal_p.h>
#include <private/qqmlscriptstring_p.h>
#include <private/qv4functionobject_p.h>
#include <private/qv4jscall_p.h>
#include <private/qv4qmlcontext_p.h>

#include <QtQml/qqmlinfo.h>

#include <QtCore/qloggingcategory.h>

QT_BEGIN_NAMESPACE

using namespace Qt::Literals::StringLiterals;

Q_LOGGING_CATEGORY(lcQQPropertyBinding, "qt.qml.propertybinding");

QUntypedPropertyBinding QQmlPropertyBinding::create(const QQmlPropertyData *pd, QV4::Function *function,
                                                    QObject *obj, const QQmlRefPointer<QQmlContextData> &ctxt,
                                                    QV4::ExecutionContext *scope, QObject *target, QQmlPropertyIndex targetIndex)
{
    Q_ASSERT(pd);
    return create(pd->propType(), function, obj, ctxt, scope, target, targetIndex);
}

QUntypedPropertyBinding QQmlPropertyBinding::create(QMetaType propertyType, QV4::Function *function,
                                                    QObject *obj,
                                                    const QQmlRefPointer<QQmlContextData> &ctxt,
                                                    QV4::ExecutionContext *scope, QObject *target,
                                                    QQmlPropertyIndex targetIndex)
{
    auto buffer = new std::byte[QQmlPropertyBinding::getSizeEnsuringAlignment()
            + sizeof(QQmlPropertyBindingJS)+jsExpressionOffsetLength()]; // QQmlPropertyBinding uses delete[]
    auto binding = new (buffer) QQmlPropertyBinding(propertyType, target, targetIndex,
                                                    TargetData::WithoutBoundFunction);
    auto js = new(buffer + QQmlPropertyBinding::getSizeEnsuringAlignment() + jsExpressionOffsetLength()) QQmlPropertyBindingJS();
    Q_ASSERT(binding->jsExpression() == js);
    Q_ASSERT(js->asBinding() == binding);
    Q_UNUSED(js);
    binding->jsExpression()->setNotifyOnValueChanged(true);
    binding->jsExpression()->setContext(ctxt);
    binding->jsExpression()->setScopeObject(obj);
    binding->jsExpression()->setupFunction(scope, function);
    return QUntypedPropertyBinding(static_cast<QPropertyBindingPrivate *>(QPropertyBindingPrivatePtr(binding).data()));
}

QUntypedPropertyBinding QQmlPropertyBinding::createFromCodeString(const QQmlPropertyData *pd, const QString& str, QObject *obj, const QQmlRefPointer<QQmlContextData> &ctxt, const QString &url, quint16 lineNumber, QObject *target, QQmlPropertyIndex targetIndex)
{
    auto buffer = new std::byte[QQmlPropertyBinding::getSizeEnsuringAlignment()
            + sizeof(QQmlPropertyBindingJS)+jsExpressionOffsetLength()]; // QQmlPropertyBinding uses delete[]
    auto binding = new(buffer) QQmlPropertyBinding(QMetaType(pd->propType()), target, targetIndex, TargetData::WithoutBoundFunction);
    auto js = new(buffer + QQmlPropertyBinding::getSizeEnsuringAlignment() + jsExpressionOffsetLength()) QQmlPropertyBindingJS();
    Q_ASSERT(binding->jsExpression() == js);
    Q_ASSERT(js->asBinding() == binding);
    Q_UNUSED(js);
    binding->jsExpression()->setNotifyOnValueChanged(true);
    binding->jsExpression()->setContext(ctxt);
    binding->jsExpression()->createQmlBinding(ctxt, obj, str, url, lineNumber);
    return QUntypedPropertyBinding(static_cast<QPropertyBindingPrivate *>(QPropertyBindingPrivatePtr(binding).data()));
}

QUntypedPropertyBinding QQmlPropertyBinding::createFromScriptString(const QQmlPropertyData *property, const QQmlScriptString &script, QObject *obj, QQmlContext *ctxt, QObject *target, QQmlPropertyIndex targetIndex)
{
    const QQmlScriptStringPrivate *scriptPrivate = script.d.data();
    // without a valid context, we cannot create anything
    if (!ctxt && (!scriptPrivate->context || !scriptPrivate->context->isValid())) {
        return {};
    }

    auto scopeObject = obj ? obj : scriptPrivate->scope;

    QV4::Function *runtimeFunction = nullptr;
    QString url;
    QQmlRefPointer<QQmlContextData> ctxtdata = QQmlContextData::get(scriptPrivate->context);
    QQmlEnginePrivate *engine = QQmlEnginePrivate::get(scriptPrivate->context->engine());
    if (engine && ctxtdata && !ctxtdata->urlString().isEmpty() && ctxtdata->typeCompilationUnit()) {
        url = ctxtdata->urlString();
        if (scriptPrivate->bindingId != QQmlBinding::Invalid)
            runtimeFunction = ctxtdata->typeCompilationUnit()->runtimeFunctions.at(scriptPrivate->bindingId);
    }
    // Do we actually have a function in the script string? If not, this becomes createCodeFromString
    if (!runtimeFunction)
        return createFromCodeString(property, scriptPrivate->script, obj, ctxtdata, url, scriptPrivate->lineNumber, target, targetIndex);

    auto buffer = new std::byte[QQmlPropertyBinding::getSizeEnsuringAlignment()
            + sizeof(QQmlPropertyBindingJS)+jsExpressionOffsetLength()]; // QQmlPropertyBinding uses delete[]
    auto binding = new(buffer) QQmlPropertyBinding(QMetaType(property->propType()), target, targetIndex, TargetData::WithoutBoundFunction);
    auto js = new(buffer + QQmlPropertyBinding::getSizeEnsuringAlignment() + jsExpressionOffsetLength()) QQmlPropertyBindingJS();
    Q_ASSERT(binding->jsExpression() == js);
    Q_ASSERT(js->asBinding() == binding);
    js->setContext(QQmlContextData::get(ctxt ? ctxt : scriptPrivate->context));

    QV4::ExecutionEngine *v4 = engine->v4engine();
    QV4::Scope scope(v4);
    QV4::Scoped<QV4::QmlContext> qmlContext(scope, QV4::QmlContext::create(v4->rootContext(), ctxtdata, scopeObject));
    js->setupFunction(qmlContext, runtimeFunction);
    return QUntypedPropertyBinding(static_cast<QPropertyBindingPrivate *>(QPropertyBindingPrivatePtr(binding).data()));
}

QUntypedPropertyBinding QQmlPropertyBinding::createFromBoundFunction(const QQmlPropertyData *pd, QV4::BoundFunction *function, QObject *obj, const QQmlRefPointer<QQmlContextData> &ctxt, QV4::ExecutionContext *scope, QObject *target, QQmlPropertyIndex targetIndex)
{
    auto buffer = new std::byte[QQmlPropertyBinding::getSizeEnsuringAlignment()
            + sizeof(QQmlPropertyBindingJSForBoundFunction)+jsExpressionOffsetLength()]; // QQmlPropertyBinding uses delete[]
    auto binding = new(buffer) QQmlPropertyBinding(QMetaType(pd->propType()), target, targetIndex, TargetData::HasBoundFunction);
    auto js = new(buffer + QQmlPropertyBinding::getSizeEnsuringAlignment() + jsExpressionOffsetLength()) QQmlPropertyBindingJSForBoundFunction();
    Q_ASSERT(binding->jsExpression() == js);
    Q_ASSERT(js->asBinding() == binding);
    Q_UNUSED(js);
    binding->jsExpression()->setNotifyOnValueChanged(true);
    binding->jsExpression()->setContext(ctxt);
    binding->jsExpression()->setScopeObject(obj);
    binding->jsExpression()->setupFunction(scope, function->function());
    js->m_boundFunction.set(function->engine(), *function);
    return QUntypedPropertyBinding(static_cast<QPropertyBindingPrivate *>(QPropertyBindingPrivatePtr(binding).data()));
}

/*!
    \fn bool QQmlPropertyBindingJS::hasDependencies()
    \internal

    Returns true if this binding has dependencies.
    Dependencies can be either QProperty dependencies or dependencies of
    the JS expression (aka activeGuards). Translations end up as a QProperty
    dependency, so they do not need any special handling
    Note that a QQmlPropertyBinding never stores qpropertyChangeTriggers.
 */


void QQmlPropertyBindingJS::expressionChanged()
{
    auto binding = asBinding();
    if (!binding->propertyDataPtr)
        return;
    const auto currentTag = m_error.tag();
    if (currentTag == InEvaluationLoop) {
        QQmlError err;
        auto location = QQmlJavaScriptExpression::sourceLocation();
        err.setUrl(QUrl{location.sourceFile});
        err.setLine(location.line);
        err.setColumn(location.column);
        const auto ctxt = context();
        QQmlEngine *engine = ctxt ? ctxt->engine() : nullptr;
        if (engine)
            err.setDescription(asBinding()->createBindingLoopErrorDescription());
        else
            err.setDescription(QString::fromLatin1("Binding loop detected"));
        err.setObject(asBinding()->target());
        qmlWarning(this->scopeObject(), err);
        return;
    }
    m_error.setTag(InEvaluationLoop);
    PendingBindingObserverList bindingObservers;
    binding->evaluateRecursive(bindingObservers);
    binding->notifyNonRecursive(bindingObservers);
    m_error.setTag(NoTag);
}

QQmlPropertyBinding::QQmlPropertyBinding(QMetaType mt, QObject *target, QQmlPropertyIndex targetIndex, TargetData::BoundFunction hasBoundFunction)
    : QPropertyBindingPrivate(mt,
                              bindingFunctionVTableForQQmlPropertyBinding(mt),
                              QPropertyBindingSourceLocation(), true)
{
    static_assert (std::is_trivially_destructible_v<TargetData>);
    static_assert (sizeof(TargetData) + sizeof(DeclarativeErrorCallback) <= sizeof(QPropertyBindingSourceLocation));
    static_assert (alignof(TargetData) <= alignof(QPropertyBindingSourceLocation));
    const auto state = hasBoundFunction ? TargetData::HasBoundFunction : TargetData::WithoutBoundFunction;
    new (&declarativeExtraData) TargetData {target, targetIndex, state};
    errorCallBack = bindingErrorCallback;
}

static QtPrivate::QPropertyBindingData *bindingDataFromPropertyData(QUntypedPropertyData *dataPtr, QMetaType type)
{
    // XXX Qt 7: We need a clean way to access the binding data
    /* This function makes the (dangerous) assumption that if we could not get the binding data
       from the binding storage, we must have been handed a QProperty.
       This does hold for anything a user could write, as there the only ways of providing a bindable property
       are to use the Q_X_BINDABLE macros, or to directly expose a QProperty.
       As long as we can ensure that any "fancier" property we implement is not resettable, we should be fine.
       We procede to calculate the address of the binding data pointer from the address of the data pointer
    */
    Q_ASSERT(dataPtr);
    std::byte *qpropertyPointer = reinterpret_cast<std::byte *>(dataPtr);
    qpropertyPointer += type.sizeOf();
    constexpr auto alignment = alignof(QtPrivate::QPropertyBindingData *);
    auto aligned = (quintptr(qpropertyPointer) + alignment - 1) & ~(alignment - 1); // ensure pointer alignment
    return reinterpret_cast<QtPrivate::QPropertyBindingData *>(aligned);
}

void QQmlPropertyBinding::handleUndefinedAssignment(QQmlEnginePrivate *ep, void *dataPtr)
{
    const QQmlPropertyData *propertyData = nullptr;
    QQmlPropertyData valueTypeData;
    QQmlData *data = QQmlData::get(target(), false);
    Q_ASSERT(data);
    if (Q_UNLIKELY(!data->propertyCache))
        data->propertyCache = QQmlMetaType::propertyCache(target()->metaObject());

    propertyData = data->propertyCache->property(targetIndex().coreIndex());
    Q_ASSERT(propertyData);
    Q_ASSERT(!targetIndex().hasValueTypeIndex());
    QQmlProperty prop = QQmlPropertyPrivate::restore(target(), *propertyData, &valueTypeData, nullptr);
    // helper function for writing back value into dataPtr
    // this is necessary  for QObjectCompatProperty, which doesn't give us the "real" dataPtr
    // if we don't write the correct value, we would otherwise set the default constructed value
    auto writeBackCurrentValue = [&](QVariant &&currentValue) {
        if (currentValue.metaType() != valueMetaType())
            currentValue.convert(valueMetaType());
        auto metaType = valueMetaType();
        metaType.destruct(dataPtr);
        metaType.construct(dataPtr, currentValue.constData());
    };
    if (prop.isResettable()) {
        // Normally a reset would remove any existing binding; but now we need to keep the binding alive
        // to handle the case where this binding becomes defined again
        // We therefore detach the binding, call reset, and reattach again
        const auto storage = qGetBindingStorage(target());
        auto bindingData = storage->bindingData(propertyDataPtr);
        if (!bindingData)
            bindingData = bindingDataFromPropertyData(propertyDataPtr, propertyData->propType());
        QPropertyBindingDataPointer bindingDataPointer{bindingData};
        auto firstObserver = takeObservers();
        bindingData->d_ref() = 0;
        if (firstObserver) {
            bindingDataPointer.setObservers(firstObserver.ptr);
        }
        Q_ASSERT(!bindingData->hasBinding());
        setIsUndefined(true);
        //suspend binding evaluation state for reset and subsequent read
        auto state = QtPrivate::suspendCurrentBindingStatus();
        prop.reset(); // May re-allocate the bindingData
        QVariant currentValue = QVariant(prop.propertyMetaType(), propertyDataPtr);
        QtPrivate::restoreBindingStatus(state);
        writeBackCurrentValue(std::move(currentValue));

        // Re-fetch binding data
        bindingData = storage->bindingData(propertyDataPtr);
        if (!bindingData)
            bindingData = bindingDataFromPropertyData(propertyDataPtr, propertyData->propType());
        bindingDataPointer = QPropertyBindingDataPointer {bindingData};

        // reattach the binding (without causing a new notification)
        if (Q_UNLIKELY(bindingData->d() & QtPrivate::QPropertyBindingData::BindingBit)) {
            qCWarning(lcQQPropertyBinding) << "Resetting " << prop.name() << "due to the binding becoming undefined  caused a new binding to be installed\n"
                       << "The old binding binding will be abandoned";
            deref();
            return;
        }
        // reset might have changed observers (?), so refresh firstObserver
        firstObserver = bindingDataPointer.firstObserver();
        bindingData->d_ref() = reinterpret_cast<quintptr>(this) | QtPrivate::QPropertyBindingData::BindingBit;
        if (firstObserver)
            prependObserver(firstObserver);
    } else {
        QQmlError qmlError;
        auto location = jsExpression()->sourceLocation();
        qmlError.setColumn(location.column);
        qmlError.setLine(location.line);
        qmlError.setUrl(QUrl {location.sourceFile});
        const QString description = QStringLiteral(R"(QML %1: Unable to assign [undefined] to "%2")").arg(QQmlMetaType::prettyTypeName(target()) , prop.name());
        qmlError.setDescription(description);
        qmlError.setObject(target());
        ep->warning(qmlError);
    }
}

QString QQmlPropertyBinding::createBindingLoopErrorDescription()
{
    const QQmlPropertyData *propertyData = nullptr;
    QQmlPropertyData valueTypeData;
    QQmlData *data = QQmlData::get(target(), false);
    Q_ASSERT(data);
    if (Q_UNLIKELY(!data->propertyCache))
        data->propertyCache = QQmlMetaType::propertyCache(target()->metaObject());

    propertyData = data->propertyCache->property(targetIndex().coreIndex());
    Q_ASSERT(propertyData);
    Q_ASSERT(!targetIndex().hasValueTypeIndex());
    QQmlProperty prop = QQmlPropertyPrivate::restore(target(), *propertyData, &valueTypeData, nullptr);
    return R"(QML %1: Binding loop detected for property "%2")"_L1.arg(QQmlMetaType::prettyTypeName(target()) , prop.name());
}

void QQmlPropertyBinding::bindingErrorCallback(QPropertyBindingPrivate *that)
{
    auto This = static_cast<QQmlPropertyBinding *>(that);
    auto target = This->target();
    auto engine = qmlEngine(target);
    if (!engine)
        return;

    auto error = This->bindingError();
    QQmlError qmlError;
    auto location = This->jsExpression()->sourceLocation();
    qmlError.setColumn(location.column);
    qmlError.setLine(location.line);
    qmlError.setUrl(QUrl {location.sourceFile});
    auto description = error.description();
    if (error.type() == QPropertyBindingError::BindingLoop) {
        description = This->createBindingLoopErrorDescription();
    }
    qmlError.setDescription(description);
    qmlError.setObject(target);
    QQmlEnginePrivate::get(engine)->warning(qmlError);
}

template<typename TranslateWithUnit>
auto qQmlTranslationPropertyBindingCreateBinding(
        const QQmlRefPointer<QV4::ExecutableCompilationUnit> &compilationUnit,
        TranslateWithUnit &&translateWithUnit)
{
    return [compilationUnit, translateWithUnit](QMetaType metaType, void *dataPtr) -> bool {
        // Create a dependency to the translationLanguage
        QQmlEnginePrivate::get(compilationUnit->engine)->translationLanguage.value();

        QVariant resultVariant(translateWithUnit(compilationUnit));
        if (metaType != QMetaType::fromType<QString>())
            resultVariant.convert(metaType);

        const bool hasChanged = !metaType.equals(resultVariant.constData(), dataPtr);
        metaType.destruct(dataPtr);
        metaType.construct(dataPtr, resultVariant.constData());
        return hasChanged;
    };
}

QUntypedPropertyBinding QQmlTranslationPropertyBinding::create(
        const QQmlPropertyData *pd,
        const QQmlRefPointer<QV4::ExecutableCompilationUnit> &compilationUnit,
        const QV4::CompiledData::Binding *binding)
{
    auto translationBinding = qQmlTranslationPropertyBindingCreateBinding(
            compilationUnit,
            [binding](const QQmlRefPointer<QV4::ExecutableCompilationUnit> &compilationUnit) {
                return compilationUnit->bindingValueAsString(binding);
            });

    return QUntypedPropertyBinding(QMetaType(pd->propType()), translationBinding,
                                   QPropertyBindingSourceLocation());
}

QUntypedPropertyBinding QQmlTranslationPropertyBinding::create(
        const QMetaType &propertyType,
        const QQmlRefPointer<QV4::ExecutableCompilationUnit> &compilationUnit,
        const QQmlTranslation &translationData)
{
    auto translationBinding = qQmlTranslationPropertyBindingCreateBinding(
            compilationUnit,
            [translationData](
                    const QQmlRefPointer<QV4::ExecutableCompilationUnit> &compilationUnit) {
                Q_UNUSED(compilationUnit);
                return translationData.translate();
            });

    return QUntypedPropertyBinding(propertyType, translationBinding,
                                   QPropertyBindingSourceLocation());
}

QV4::ReturnedValue QQmlPropertyBindingJSForBoundFunction::evaluate(bool *isUndefined)
{
    QV4::ExecutionEngine *v4 = engine()->handle();
    int argc = 0;
    const QV4::Value *argv = nullptr;
    const QV4::Value *thisObject = nullptr;
    QV4::BoundFunction *b = nullptr;
    if ((b = m_boundFunction.as<QV4::BoundFunction>())) {
        QV4::Heap::MemberData *args = b->boundArgs();
        if (args) {
            argc = args->values.size;
            argv = args->values.data();
        }
        thisObject = &b->d()->boundThis;
    }
    QV4::Scope scope(v4);
    QV4::JSCallData jsCall(thisObject, argv, argc);

    return QQmlJavaScriptExpression::evaluate(jsCall.callData(scope), isUndefined);
}

QT_END_NAMESPACE