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 | |
24 | QT_BEGIN_NAMESPACE |
25 | |
26 | Q_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. |
32 | struct 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 | |
128 | class QQmlConnectionsPrivate : public QObjectPrivate |
129 | { |
130 | public: |
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 | \nativetype QQmlConnections |
146 | \inqmlmodule QtQml |
147 | \ingroup qtquick-interceptors |
148 | \brief Describes generalized connections to signals. |
149 | |
150 | A Connections object creates a connection to a QML signal. |
151 | |
152 | When connecting to signals in QML, the usual way is to create an |
153 | "on<Signal>" handler that reacts when a signal is received, like this: |
154 | |
155 | \qml |
156 | MouseArea { |
157 | onClicked: (mouse)=> { foo(mouse) } |
158 | } |
159 | \endqml |
160 | |
161 | However, it is not possible to connect to a signal in this way in some |
162 | cases, such as when: |
163 | |
164 | \list |
165 | \li Multiple connections to the same signal are required |
166 | \li Creating connections outside the scope of the signal sender |
167 | \li Connecting to targets not defined in QML |
168 | \endlist |
169 | |
170 | When any of these are needed, the Connections type can be used instead. |
171 | |
172 | For example, the above code can be changed to use a Connections object, |
173 | like this: |
174 | |
175 | \qml |
176 | MouseArea { |
177 | Connections { |
178 | function onClicked(mouse) { foo(mouse) } |
179 | } |
180 | } |
181 | \endqml |
182 | |
183 | More generally, the Connections object can be a child of some object other than |
184 | the sender of the signal: |
185 | |
186 | \qml |
187 | MouseArea { |
188 | id: area |
189 | } |
190 | // ... |
191 | \endqml |
192 | \qml |
193 | Connections { |
194 | target: area |
195 | function onClicked(mouse) { foo(mouse) } |
196 | } |
197 | \endqml |
198 | |
199 | \note For backwards compatibility you can also specify the signal handlers |
200 | without \c{function}, like you would specify them directly in the target |
201 | object. This is not recommended. If you specify one signal handler this way, |
202 | then all signal handlers specified as \c{function} in the same Connections |
203 | object are ignored. |
204 | |
205 | \sa {Qt Qml} |
206 | */ |
207 | QQmlConnections::QQmlConnections(QObject *parent) : |
208 | QObject(*(new QQmlConnectionsPrivate), parent) |
209 | { |
210 | } |
211 | |
212 | QQmlConnections::~QQmlConnections() |
213 | { |
214 | Q_D(QQmlConnections); |
215 | |
216 | // The slot dispatchers hold cyclic references to their connections. Clear them. |
217 | for (const auto &bound : std::as_const(t&: d->boundsignals)) { |
218 | if (QQmlConnectionSlotDispatcher *dispatcher = bound.isT2() ? bound.asT2() : nullptr) { |
219 | // No need to explicitly disconnect anymore since 'this' is the receiver. |
220 | // But to be safe, explicitly break any cyclic references between the connection |
221 | // and the slot object. |
222 | dispatcher->connection = {}; |
223 | dispatcher->destroyIfLastRef(); |
224 | } |
225 | } |
226 | } |
227 | |
228 | /*! |
229 | \qmlproperty QtObject QtQml::Connections::target |
230 | This property holds the object that sends the signal. |
231 | |
232 | If this property is not set, the \c target defaults to the parent of the Connection. |
233 | |
234 | If set to null, no connection is made and any signal handlers are ignored |
235 | until the target is not null. |
236 | */ |
237 | QObject *QQmlConnections::target() const |
238 | { |
239 | Q_D(const QQmlConnections); |
240 | return d->targetSet ? d->target.data() : parent(); |
241 | } |
242 | |
243 | class QQmlBoundSignalDeleter : public QObject |
244 | { |
245 | public: |
246 | QQmlBoundSignalDeleter(QQmlBoundSignal *signal) : m_signal(signal) { m_signal->removeFromObject(); } |
247 | ~QQmlBoundSignalDeleter() { delete m_signal; } |
248 | |
249 | private: |
250 | QQmlBoundSignal *m_signal; |
251 | }; |
252 | |
253 | void QQmlConnections::setTarget(QObject *obj) |
254 | { |
255 | Q_D(QQmlConnections); |
256 | if (d->targetSet && d->target == obj) |
257 | return; |
258 | d->targetSet = true; // even if setting to 0, it is *set* |
259 | for (const auto &bound : std::as_const(t&: d->boundsignals)) { |
260 | // It is possible that target is being changed due to one of our signal |
261 | // handlers -> use deleteLater(). |
262 | if (QQmlBoundSignal *signal = bound.isT1() ? bound.asT1() : nullptr) { |
263 | if (signal->isNotifying()) |
264 | (new QQmlBoundSignalDeleter(signal))->deleteLater(); |
265 | else |
266 | delete signal; |
267 | } else { |
268 | QQmlConnectionSlotDispatcher *dispatcher = bound.asT2(); |
269 | QObject::disconnect(std::exchange(obj&: dispatcher->connection, new_val: {})); |
270 | dispatcher->destroyIfLastRef(); |
271 | } |
272 | } |
273 | d->boundsignals.clear(); |
274 | d->target = obj; |
275 | connectSignals(); |
276 | emit targetChanged(); |
277 | } |
278 | |
279 | /*! |
280 | \qmlproperty bool QtQml::Connections::enabled |
281 | \since 5.7 |
282 | |
283 | This property holds whether the item accepts change events. |
284 | |
285 | By default, this property is \c true. |
286 | */ |
287 | bool QQmlConnections::isEnabled() const |
288 | { |
289 | Q_D(const QQmlConnections); |
290 | return d->enabled; |
291 | } |
292 | |
293 | void QQmlConnections::setEnabled(bool enabled) |
294 | { |
295 | Q_D(QQmlConnections); |
296 | if (d->enabled == enabled) |
297 | return; |
298 | |
299 | d->enabled = enabled; |
300 | |
301 | for (const auto &bound : std::as_const(t&: d->boundsignals)) { |
302 | if (QQmlBoundSignal *signal = bound.isT1() ? bound.asT1() : nullptr) |
303 | signal->setEnabled(d->enabled); |
304 | else |
305 | bound.asT2()->enabled = enabled; |
306 | } |
307 | |
308 | emit enabledChanged(); |
309 | } |
310 | |
311 | /*! |
312 | \qmlproperty bool QtQml::Connections::ignoreUnknownSignals |
313 | |
314 | Normally, a connection to a non-existent signal produces runtime errors. |
315 | |
316 | If this property is set to \c true, such errors are ignored. |
317 | This is useful if you intend to connect to different types of objects, handling |
318 | a different set of signals for each object. |
319 | */ |
320 | bool QQmlConnections::ignoreUnknownSignals() const |
321 | { |
322 | Q_D(const QQmlConnections); |
323 | return d->ignoreUnknownSignals; |
324 | } |
325 | |
326 | void QQmlConnections::setIgnoreUnknownSignals(bool ignore) |
327 | { |
328 | Q_D(QQmlConnections); |
329 | d->ignoreUnknownSignals = ignore; |
330 | } |
331 | |
332 | void QQmlConnectionsParser::verifyBindings( |
333 | const QQmlRefPointer<QV4::CompiledData::CompilationUnit> &compilationUnit, |
334 | const QList<const QV4::CompiledData::Binding *> &props) |
335 | { |
336 | for (int ii = 0; ii < props.size(); ++ii) { |
337 | const QV4::CompiledData::Binding *binding = props.at(i: ii); |
338 | const QString &propName = compilationUnit->stringAt(index: binding->propertyNameIndex); |
339 | |
340 | if (!QQmlSignalNames::isHandlerName(signalName: propName)) { |
341 | error(binding: props.at(i: ii), description: QQmlConnections::tr(s: "Cannot assign to non-existent property \"%1\"").arg(a: propName)); |
342 | return; |
343 | } |
344 | |
345 | if (binding->type() == QV4::CompiledData::Binding::Type_Script) |
346 | continue; |
347 | |
348 | if (binding->type() >= QV4::CompiledData::Binding::Type_Object) { |
349 | const QV4::CompiledData::Object *target = compilationUnit->objectAt(index: binding->value.objectIndex); |
350 | if (!compilationUnit->stringAt(index: target->inheritedTypeNameIndex).isEmpty()) |
351 | error(binding, description: QQmlConnections::tr(s: "Connections: nested objects not allowed")); |
352 | else |
353 | error(binding, description: QQmlConnections::tr(s: "Connections: syntax error")); |
354 | return; |
355 | } |
356 | |
357 | error(binding, description: QQmlConnections::tr(s: "Connections: script expected")); |
358 | return; |
359 | } |
360 | } |
361 | |
362 | void QQmlConnectionsParser::applyBindings(QObject *object, const QQmlRefPointer<QV4::ExecutableCompilationUnit> &compilationUnit, const QList<const QV4::CompiledData::Binding *> &bindings) |
363 | { |
364 | QQmlConnectionsPrivate *p = |
365 | static_cast<QQmlConnectionsPrivate *>(QObjectPrivate::get(o: object)); |
366 | p->compilationUnit = compilationUnit; |
367 | p->bindings = bindings; |
368 | } |
369 | |
370 | void QQmlConnections::connectSignals() |
371 | { |
372 | Q_D(QQmlConnections); |
373 | if (!d->componentcomplete || (d->targetSet && !target())) |
374 | return; |
375 | |
376 | if (d->bindings.isEmpty()) { |
377 | connectSignalsToMethods(); |
378 | } else { |
379 | if (lcQmlConnections().isWarningEnabled()) { |
380 | qmlWarning(me: this) << tr(s: "Implicitly defined onFoo properties in Connections are deprecated. " |
381 | "Use this syntax instead: function onFoo(<arguments>) { ... }"); |
382 | } |
383 | connectSignalsToBindings(); |
384 | } |
385 | } |
386 | |
387 | void QQmlConnections::connectSignalsToMethods() |
388 | { |
389 | Q_D(QQmlConnections); |
390 | |
391 | QObject *target = this->target(); |
392 | QQmlData *ddata = QQmlData::get(object: this); |
393 | if (!ddata) |
394 | return; |
395 | |
396 | QV4::ExecutionEngine *engine = ddata->context->engine()->handle(); |
397 | |
398 | QQmlRefPointer<QQmlContextData> ctxtdata = ddata->outerContext; |
399 | for (int i = ddata->propertyCache->methodOffset(), |
400 | end = ddata->propertyCache->methodOffset() + ddata->propertyCache->methodCount(); |
401 | i < end; |
402 | ++i) { |
403 | |
404 | const QQmlPropertyData *handler = ddata->propertyCache->method(index: i); |
405 | if (!handler) |
406 | continue; |
407 | |
408 | const QString propName = handler->name(object: this); |
409 | |
410 | QQmlProperty prop(target, propName); |
411 | if (prop.isValid() && (prop.type() & QQmlProperty::SignalProperty)) { |
412 | QV4::Scope scope(engine); |
413 | QV4::ScopedContext global(scope, engine->rootContext()); |
414 | |
415 | if (QQmlVMEMetaObject *vmeMetaObject = QQmlVMEMetaObject::get(obj: this)) { |
416 | int signalIndex = QQmlPropertyPrivate::get(p: prop)->signalIndex(); |
417 | auto *signal = new QQmlBoundSignal(target, signalIndex, this, qmlEngine(this)); |
418 | signal->setEnabled(d->enabled); |
419 | |
420 | QV4::Scoped<QV4::JavaScriptFunctionObject> method( |
421 | scope, vmeMetaObject->vmeMethod(index: handler->coreIndex())); |
422 | |
423 | QQmlBoundSignalExpression *expression = ctxtdata |
424 | ? new QQmlBoundSignalExpression( |
425 | target, signalIndex, ctxtdata, this, method->function()) |
426 | : nullptr; |
427 | |
428 | signal->takeExpression(expression); |
429 | d->boundsignals += signal; |
430 | } else { |
431 | QQmlConnectionSlotDispatcher *slot = new QQmlConnectionSlotDispatcher( |
432 | scope.engine, target, prop.index(), |
433 | this, handler->coreIndex(), d->enabled); |
434 | slot->connection = QObjectPrivate::connect( |
435 | sender: target, signal_index: prop.index(), slotObj: slot, type: Qt::AutoConnection); |
436 | slot->ref(); |
437 | d->boundsignals += slot; |
438 | } |
439 | } else if (!d->ignoreUnknownSignals |
440 | && propName.startsWith(s: QLatin1String("on")) && propName.size() > 2 |
441 | && propName.at(i: 2).isUpper()) { |
442 | qmlWarning(me: this) << tr(s: "Detected function \"%1\" in Connections element. " |
443 | "This is probably intended to be a signal handler but no " |
444 | "signal of the target matches the name.").arg(a: propName); |
445 | } |
446 | } |
447 | } |
448 | |
449 | // TODO: Drop this as soon as we can |
450 | void QQmlConnections::connectSignalsToBindings() |
451 | { |
452 | Q_D(QQmlConnections); |
453 | QObject *target = this->target(); |
454 | QQmlData *ddata = QQmlData::get(object: this); |
455 | QQmlRefPointer<QQmlContextData> ctxtdata = ddata ? ddata->outerContext : nullptr; |
456 | |
457 | for (const QV4::CompiledData::Binding *binding : std::as_const(t&: d->bindings)) { |
458 | Q_ASSERT(binding->type() == QV4::CompiledData::Binding::Type_Script); |
459 | QString propName = d->compilationUnit->stringAt(index: binding->propertyNameIndex); |
460 | |
461 | QQmlProperty prop(target, propName); |
462 | if (prop.isValid() && (prop.type() & QQmlProperty::SignalProperty)) { |
463 | int signalIndex = QQmlPropertyPrivate::get(p: prop)->signalIndex(); |
464 | QQmlBoundSignal *signal = |
465 | new QQmlBoundSignal(target, signalIndex, this, qmlEngine(this)); |
466 | signal->setEnabled(d->enabled); |
467 | |
468 | auto f = d->compilationUnit->runtimeFunctions[binding->value.compiledScriptIndex]; |
469 | QQmlBoundSignalExpression *expression = |
470 | ctxtdata ? new QQmlBoundSignalExpression(target, signalIndex, ctxtdata, this, f) |
471 | : nullptr; |
472 | signal->takeExpression(expression); |
473 | d->boundsignals += signal; |
474 | } else { |
475 | if (!d->ignoreUnknownSignals) |
476 | qmlWarning(me: this) << tr(s: "Cannot assign to non-existent property \"%1\"").arg(a: propName); |
477 | } |
478 | } |
479 | } |
480 | |
481 | void QQmlConnections::classBegin() |
482 | { |
483 | Q_D(QQmlConnections); |
484 | d->componentcomplete=false; |
485 | } |
486 | |
487 | void QQmlConnections::componentComplete() |
488 | { |
489 | Q_D(QQmlConnections); |
490 | d->componentcomplete=true; |
491 | connectSignals(); |
492 | } |
493 | |
494 | QT_END_NAMESPACE |
495 | |
496 | #include "moc_qqmlconnections_p.cpp" |
497 |
Definitions
- lcQmlConnections
- QQmlConnectionSlotDispatcher
- QQmlConnectionSlotDispatcher
- TypedFunction
- TypedFunction
- TypedFunction
- TypedFunction
- returnMetaType
- parameterCount
- parameterMetaType
- impl
- QQmlConnectionsPrivate
- QQmlConnections
- ~QQmlConnections
- target
- QQmlBoundSignalDeleter
- QQmlBoundSignalDeleter
- ~QQmlBoundSignalDeleter
- setTarget
- isEnabled
- setEnabled
- ignoreUnknownSignals
- setIgnoreUnknownSignals
- verifyBindings
- applyBindings
- connectSignals
- connectSignalsToMethods
- connectSignalsToBindings
- classBegin
Learn to use CMake with our Intro Training
Find out more