| 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_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. |
| 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 | \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 | */ |
| 206 | QQmlConnections::QQmlConnections(QObject *parent) : |
| 207 | QObject(*(new QQmlConnectionsPrivate), parent) |
| 208 | { |
| 209 | } |
| 210 | |
| 211 | QQmlConnections::~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 | */ |
| 236 | QObject *QQmlConnections::target() const |
| 237 | { |
| 238 | Q_D(const QQmlConnections); |
| 239 | return d->targetSet ? d->target.data() : parent(); |
| 240 | } |
| 241 | |
| 242 | class QQmlBoundSignalDeleter : public QObject |
| 243 | { |
| 244 | public: |
| 245 | QQmlBoundSignalDeleter(QQmlBoundSignal *signal) : m_signal(signal) { m_signal->removeFromObject(); } |
| 246 | ~QQmlBoundSignalDeleter() { delete m_signal; } |
| 247 | |
| 248 | private: |
| 249 | QQmlBoundSignal *m_signal; |
| 250 | }; |
| 251 | |
| 252 | void 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 | */ |
| 286 | bool QQmlConnections::isEnabled() const |
| 287 | { |
| 288 | Q_D(const QQmlConnections); |
| 289 | return d->enabled; |
| 290 | } |
| 291 | |
| 292 | void 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 | */ |
| 319 | bool QQmlConnections::ignoreUnknownSignals() const |
| 320 | { |
| 321 | Q_D(const QQmlConnections); |
| 322 | return d->ignoreUnknownSignals; |
| 323 | } |
| 324 | |
| 325 | void QQmlConnections::setIgnoreUnknownSignals(bool ignore) |
| 326 | { |
| 327 | Q_D(QQmlConnections); |
| 328 | d->ignoreUnknownSignals = ignore; |
| 329 | } |
| 330 | |
| 331 | void 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 | |
| 361 | void 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 | |
| 369 | void 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 | |
| 386 | void 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 |
| 449 | void 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 | |
| 480 | void QQmlConnections::classBegin() |
| 481 | { |
| 482 | Q_D(QQmlConnections); |
| 483 | d->componentcomplete=false; |
| 484 | } |
| 485 | |
| 486 | void QQmlConnections::componentComplete() |
| 487 | { |
| 488 | Q_D(QQmlConnections); |
| 489 | d->componentcomplete=true; |
| 490 | connectSignals(); |
| 491 | } |
| 492 | |
| 493 | QT_END_NAMESPACE |
| 494 | |
| 495 | #include "moc_qqmlconnections_p.cpp" |
| 496 | |