1// Copyright (C) 2016 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qqmlconnections_p.h"
5
6#include <private/qqmlboundsignal_p.h>
7#include <private/qqmlcontext_p.h>
8#include <private/qqmlexpression_p.h>
9#include <private/qqmlproperty_p.h>
10#include <private/qqmlsignalnames_p.h>
11#include <private/qqmlvmemetaobject_p.h>
12#include <private/qv4jscall_p.h>
13#include <private/qv4qobjectwrapper_p.h>
14
15#include <QtQml/qqmlcontext.h>
16#include <QtQml/qqmlinfo.h>
17
18#include <QtCore/qdebug.h>
19#include <QtCore/qloggingcategory.h>
20#include <QtCore/qstringlist.h>
21
22#include <private/qobject_p.h>
23
24QT_BEGIN_NAMESPACE
25
26Q_STATIC_LOGGING_CATEGORY(lcQmlConnections, "qt.qml.connections")
27
28// This is the equivalent of QQmlBoundSignal for C++ methods as as slots.
29// If a type derived from QQmlConnnections is compiled using qmltc, the
30// JavaScript functions it contains are turned into C++ methods and we cannot
31// use QQmlBoundSignal to connect to those.
32struct QQmlConnectionSlotDispatcher : public QtPrivate::QSlotObjectBase
33{
34 QV4::ExecutionEngine *v4 = nullptr;
35 QObject *receiver = nullptr;
36
37 // Signals rarely have more than one argument.
38 QQmlMetaObject::ArgTypeStorage<2> signalMetaTypes;
39 QQmlMetaObject::ArgTypeStorage<2> slotMetaTypes;
40
41 QMetaObject::Connection connection;
42
43 int slotIndex = -1;
44 bool enabled = true;
45
46 QQmlConnectionSlotDispatcher(
47 QV4::ExecutionEngine *v4, QObject *sender, int signalIndex,
48 QObject *receiver, int slotIndex, bool enabled)
49 : QtPrivate::QSlotObjectBase(&impl)
50 , v4(v4)
51 , receiver(receiver)
52 , slotIndex(slotIndex)
53 , enabled(enabled)
54 {
55 QMetaMethod signal = sender->metaObject()->method(index: signalIndex);
56 QQmlMetaObject::methodReturnAndParameterTypes(method: signal, argStorage: &signalMetaTypes, unknownTypeError: nullptr);
57
58 QMetaMethod slot = receiver->metaObject()->method(index: slotIndex);
59 QQmlMetaObject::methodReturnAndParameterTypes(method: slot, argStorage: &slotMetaTypes, unknownTypeError: nullptr);
60 }
61
62 template<typename ArgTypeStorage>
63 struct TypedFunction
64 {
65 Q_DISABLE_COPY_MOVE(TypedFunction)
66 public:
67 TypedFunction(const ArgTypeStorage *storage) : storage(storage) {}
68
69 QMetaType returnMetaType() const { return storage->at(0); }
70 qsizetype parameterCount() const { return storage->size() - 1; }
71 QMetaType parameterMetaType(qsizetype i) const { return storage->at(i + 1); }
72
73 private:
74 const ArgTypeStorage *storage;
75 };
76
77 static void impl(int which, QSlotObjectBase *base, QObject *, void **metaArgs, bool *ret)
78 {
79 switch (which) {
80 case Destroy: {
81 delete static_cast<QQmlConnectionSlotDispatcher *>(base);
82 break;
83 }
84 case Call: {
85 QQmlConnectionSlotDispatcher *self = static_cast<QQmlConnectionSlotDispatcher *>(base);
86 QV4::ExecutionEngine *v4 = self->v4;
87 if (!v4)
88 break;
89
90 if (!self->enabled)
91 break;
92
93 TypedFunction typedFunction(&self->slotMetaTypes);
94 QV4::coerceAndCall(
95 engine: v4, typedFunction: &typedFunction, argv: metaArgs,
96 types: self->signalMetaTypes.data(), argc: self->signalMetaTypes.size() - 1,
97 call: [&](void **argv, int) {
98 self->receiver->metaObject()->metacall(
99 self->receiver, QMetaObject::InvokeMetaMethod,
100 self->slotIndex, argv);
101 });
102
103 if (v4->hasException) {
104 QQmlError error = v4->catchExceptionAsQmlError();
105 if (QQmlEngine *qmlEngine = v4->qmlEngine()) {
106 QQmlEnginePrivate::get(e: qmlEngine)->warning(error);
107 } else {
108 QMessageLogger(
109 qPrintable(error.url().toString()), error.line(), nullptr)
110 .warning().noquote()
111 << error.toString();
112 }
113 }
114 break;
115 }
116 case Compare:
117 // We're not implementing the Compare protocol here. It's insane.
118 // QQmlConnectionSlotDispatcher compares false to anything. We use
119 // the regular QObject::disconnect with QMetaObject::Connection.
120 *ret = false;
121 break;
122 case NumOperations:
123 break;
124 }
125 };
126};
127
128class QQmlConnectionsPrivate : public QObjectPrivate
129{
130public:
131 QList<QBiPointer<QQmlBoundSignal, QQmlConnectionSlotDispatcher>> boundsignals;
132 QQmlGuard<QObject> target;
133
134 bool enabled = true;
135 bool targetSet = false;
136 bool ignoreUnknownSignals = false;
137 bool componentcomplete = true;
138
139 QQmlRefPointer<QV4::ExecutableCompilationUnit> compilationUnit;
140 QList<const QV4::CompiledData::Binding *> bindings;
141};
142
143/*!
144 \qmltype Connections
145 \inqmlmodule QtQml
146 \ingroup qtquick-interceptors
147 \brief Describes generalized connections to signals.
148
149 A Connections object creates a connection to a QML signal.
150
151 When connecting to signals in QML, the usual way is to create an
152 "on<Signal>" handler that reacts when a signal is received, like this:
153
154 \qml
155 MouseArea {
156 onClicked: (mouse)=> { foo(mouse) }
157 }
158 \endqml
159
160 However, it is not possible to connect to a signal in this way in some
161 cases, such as when:
162
163 \list
164 \li Multiple connections to the same signal are required
165 \li Creating connections outside the scope of the signal sender
166 \li Connecting to targets not defined in QML
167 \endlist
168
169 When any of these are needed, the Connections type can be used instead.
170
171 For example, the above code can be changed to use a Connections object,
172 like this:
173
174 \qml
175 MouseArea {
176 Connections {
177 function onClicked(mouse) { foo(mouse) }
178 }
179 }
180 \endqml
181
182 More generally, the Connections object can be a child of some object other than
183 the sender of the signal:
184
185 \qml
186 MouseArea {
187 id: area
188 }
189 // ...
190 \endqml
191 \qml
192 Connections {
193 target: area
194 function onClicked(mouse) { foo(mouse) }
195 }
196 \endqml
197
198 \note For backwards compatibility you can also specify the signal handlers
199 without \c{function}, like you would specify them directly in the target
200 object. This is not recommended. If you specify one signal handler this way,
201 then all signal handlers specified as \c{function} in the same Connections
202 object are ignored.
203
204 \sa {Qt Qml}
205*/
206QQmlConnections::QQmlConnections(QObject *parent) :
207 QObject(*(new QQmlConnectionsPrivate), parent)
208{
209}
210
211QQmlConnections::~QQmlConnections()
212{
213 Q_D(QQmlConnections);
214
215 // The slot dispatchers hold cyclic references to their connections. Clear them.
216 for (const auto &bound : std::as_const(t&: d->boundsignals)) {
217 if (QQmlConnectionSlotDispatcher *dispatcher = bound.isT2() ? bound.asT2() : nullptr) {
218 // No need to explicitly disconnect anymore since 'this' is the receiver.
219 // But to be safe, explicitly break any cyclic references between the connection
220 // and the slot object.
221 dispatcher->connection = {};
222 dispatcher->destroyIfLastRef();
223 }
224 }
225}
226
227/*!
228 \qmlproperty QtObject QtQml::Connections::target
229 This property holds the object that sends the signal.
230
231 If this property is not set, the \c target defaults to the parent of the Connection.
232
233 If set to null, no connection is made and any signal handlers are ignored
234 until the target is not null.
235*/
236QObject *QQmlConnections::target() const
237{
238 Q_D(const QQmlConnections);
239 return d->targetSet ? d->target.data() : parent();
240}
241
242class QQmlBoundSignalDeleter : public QObject
243{
244public:
245 QQmlBoundSignalDeleter(QQmlBoundSignal *signal) : m_signal(signal) { m_signal->removeFromObject(); }
246 ~QQmlBoundSignalDeleter() { delete m_signal; }
247
248private:
249 QQmlBoundSignal *m_signal;
250};
251
252void QQmlConnections::setTarget(QObject *obj)
253{
254 Q_D(QQmlConnections);
255 if (d->targetSet && d->target == obj)
256 return;
257 d->targetSet = true; // even if setting to 0, it is *set*
258 for (const auto &bound : std::as_const(t&: d->boundsignals)) {
259 // It is possible that target is being changed due to one of our signal
260 // handlers -> use deleteLater().
261 if (QQmlBoundSignal *signal = bound.isT1() ? bound.asT1() : nullptr) {
262 if (signal->isNotifying())
263 (new QQmlBoundSignalDeleter(signal))->deleteLater();
264 else
265 delete signal;
266 } else {
267 QQmlConnectionSlotDispatcher *dispatcher = bound.asT2();
268 QObject::disconnect(std::exchange(obj&: dispatcher->connection, new_val: {}));
269 dispatcher->destroyIfLastRef();
270 }
271 }
272 d->boundsignals.clear();
273 d->target = obj;
274 connectSignals();
275 emit targetChanged();
276}
277
278/*!
279 \qmlproperty bool QtQml::Connections::enabled
280 \since 5.7
281
282 This property holds whether the item accepts change events.
283
284 By default, this property is \c true.
285*/
286bool QQmlConnections::isEnabled() const
287{
288 Q_D(const QQmlConnections);
289 return d->enabled;
290}
291
292void QQmlConnections::setEnabled(bool enabled)
293{
294 Q_D(QQmlConnections);
295 if (d->enabled == enabled)
296 return;
297
298 d->enabled = enabled;
299
300 for (const auto &bound : std::as_const(t&: d->boundsignals)) {
301 if (QQmlBoundSignal *signal = bound.isT1() ? bound.asT1() : nullptr)
302 signal->setEnabled(d->enabled);
303 else
304 bound.asT2()->enabled = enabled;
305 }
306
307 emit enabledChanged();
308}
309
310/*!
311 \qmlproperty bool QtQml::Connections::ignoreUnknownSignals
312
313 Normally, a connection to a non-existent signal produces runtime errors.
314
315 If this property is set to \c true, such errors are ignored.
316 This is useful if you intend to connect to different types of objects, handling
317 a different set of signals for each object.
318*/
319bool QQmlConnections::ignoreUnknownSignals() const
320{
321 Q_D(const QQmlConnections);
322 return d->ignoreUnknownSignals;
323}
324
325void QQmlConnections::setIgnoreUnknownSignals(bool ignore)
326{
327 Q_D(QQmlConnections);
328 d->ignoreUnknownSignals = ignore;
329}
330
331void QQmlConnectionsParser::verifyBindings(
332 const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &compilationUnit,
333 const QList<const QV4::CompiledData::Binding *> &props)
334{
335 for (int ii = 0; ii < props.size(); ++ii) {
336 const QV4::CompiledData::Binding *binding = props.at(i: ii);
337 const QString &propName = compilationUnit->stringAt(index: binding->propertyNameIndex);
338
339 if (!QQmlSignalNames::isHandlerName(signalName: propName)) {
340 error(binding: props.at(i: ii), description: QQmlConnections::tr(s: "Cannot assign to non-existent property \"%1\"").arg(a: propName));
341 return;
342 }
343
344 if (binding->type() == QV4::CompiledData::Binding::Type_Script)
345 continue;
346
347 if (binding->type() >= QV4::CompiledData::Binding::Type_Object) {
348 const QV4::CompiledData::Object *target = compilationUnit->objectAt(index: binding->value.objectIndex);
349 if (!compilationUnit->stringAt(index: target->inheritedTypeNameIndex).isEmpty())
350 error(binding, description: QQmlConnections::tr(s: "Connections: nested objects not allowed"));
351 else
352 error(binding, description: QQmlConnections::tr(s: "Connections: syntax error"));
353 return;
354 }
355
356 error(binding, description: QQmlConnections::tr(s: "Connections: script expected"));
357 return;
358 }
359}
360
361void QQmlConnectionsParser::applyBindings(QObject *object, const QQmlRefPointer<QV4::ExecutableCompilationUnit> &compilationUnit, const QList<const QV4::CompiledData::Binding *> &bindings)
362{
363 QQmlConnectionsPrivate *p =
364 static_cast<QQmlConnectionsPrivate *>(QObjectPrivate::get(o: object));
365 p->compilationUnit = compilationUnit;
366 p->bindings = bindings;
367}
368
369void QQmlConnections::connectSignals()
370{
371 Q_D(QQmlConnections);
372 if (!d->componentcomplete || (d->targetSet && !target()))
373 return;
374
375 if (d->bindings.isEmpty()) {
376 connectSignalsToMethods();
377 } else {
378 if (lcQmlConnections().isWarningEnabled()) {
379 qmlWarning(me: this) << tr(s: "Implicitly defined onFoo properties in Connections are deprecated. "
380 "Use this syntax instead: function onFoo(<arguments>) { ... }");
381 }
382 connectSignalsToBindings();
383 }
384}
385
386void QQmlConnections::connectSignalsToMethods()
387{
388 Q_D(QQmlConnections);
389
390 QObject *target = this->target();
391 QQmlData *ddata = QQmlData::get(object: this);
392 if (!ddata)
393 return;
394
395 QV4::ExecutionEngine *engine = ddata->context->engine()->handle();
396
397 QQmlRefPointer<QQmlContextData> ctxtdata = ddata->outerContext;
398 for (int i = ddata->propertyCache->methodOffset(),
399 end = ddata->propertyCache->methodOffset() + ddata->propertyCache->methodCount();
400 i < end;
401 ++i) {
402
403 const QQmlPropertyData *handler = ddata->propertyCache->method(index: i);
404 if (!handler)
405 continue;
406
407 const QString propName = handler->name(object: this);
408
409 QQmlProperty prop(target, propName);
410 if (prop.isValid() && (prop.type() & QQmlProperty::SignalProperty)) {
411 QV4::Scope scope(engine);
412 QV4::ScopedContext global(scope, engine->rootContext());
413
414 if (QQmlVMEMetaObject *vmeMetaObject = QQmlVMEMetaObject::get(obj: this)) {
415 int signalIndex = QQmlPropertyPrivate::get(p: prop)->signalIndex();
416 auto *signal = new QQmlBoundSignal(target, signalIndex, this, qmlEngine(this));
417 signal->setEnabled(d->enabled);
418
419 QV4::Scoped<QV4::JavaScriptFunctionObject> method(
420 scope, vmeMetaObject->vmeMethod(index: handler->coreIndex()));
421
422 QQmlBoundSignalExpression *expression = ctxtdata
423 ? new QQmlBoundSignalExpression(
424 target, signalIndex, ctxtdata, this, method->function())
425 : nullptr;
426
427 signal->takeExpression(expression);
428 d->boundsignals += signal;
429 } else {
430 QQmlConnectionSlotDispatcher *slot = new QQmlConnectionSlotDispatcher(
431 scope.engine, target, prop.index(),
432 this, handler->coreIndex(), d->enabled);
433 slot->connection = QObjectPrivate::connect(
434 sender: target, signal_index: prop.index(), slotObj: slot, type: Qt::AutoConnection);
435 slot->ref();
436 d->boundsignals += slot;
437 }
438 } else if (!d->ignoreUnknownSignals
439 && propName.startsWith(s: QLatin1String("on")) && propName.size() > 2
440 && propName.at(i: 2).isUpper()) {
441 qmlWarning(me: this) << tr(s: "Detected function \"%1\" in Connections element. "
442 "This is probably intended to be a signal handler but no "
443 "signal of the target matches the name.").arg(a: propName);
444 }
445 }
446}
447
448// TODO: Drop this as soon as we can
449void QQmlConnections::connectSignalsToBindings()
450{
451 Q_D(QQmlConnections);
452 QObject *target = this->target();
453 QQmlData *ddata = QQmlData::get(object: this);
454 QQmlRefPointer<QQmlContextData> ctxtdata = ddata ? ddata->outerContext : nullptr;
455
456 for (const QV4::CompiledData::Binding *binding : std::as_const(t&: d->bindings)) {
457 Q_ASSERT(binding->type() == QV4::CompiledData::Binding::Type_Script);
458 QString propName = d->compilationUnit->stringAt(index: binding->propertyNameIndex);
459
460 QQmlProperty prop(target, propName);
461 if (prop.isValid() && (prop.type() & QQmlProperty::SignalProperty)) {
462 int signalIndex = QQmlPropertyPrivate::get(p: prop)->signalIndex();
463 QQmlBoundSignal *signal =
464 new QQmlBoundSignal(target, signalIndex, this, qmlEngine(this));
465 signal->setEnabled(d->enabled);
466
467 auto f = d->compilationUnit->runtimeFunctions[binding->value.compiledScriptIndex];
468 QQmlBoundSignalExpression *expression =
469 ctxtdata ? new QQmlBoundSignalExpression(target, signalIndex, ctxtdata, this, f)
470 : nullptr;
471 signal->takeExpression(expression);
472 d->boundsignals += signal;
473 } else {
474 if (!d->ignoreUnknownSignals)
475 qmlWarning(me: this) << tr(s: "Cannot assign to non-existent property \"%1\"").arg(a: propName);
476 }
477 }
478}
479
480void QQmlConnections::classBegin()
481{
482 Q_D(QQmlConnections);
483 d->componentcomplete=false;
484}
485
486void QQmlConnections::componentComplete()
487{
488 Q_D(QQmlConnections);
489 d->componentcomplete=true;
490 connectSignals();
491}
492
493QT_END_NAMESPACE
494
495#include "moc_qqmlconnections_p.cpp"
496

source code of qtdeclarative/src/qmlmeta/types/qqmlconnections.cpp