1// Copyright (C) 2022 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 "qquickattachedpropertypropagator.h"
5
6#include <QtCore/qpointer.h>
7#include <QtQuick/qquickwindow.h>
8#include <QtQuick/private/qquickitem_p.h>
9#include <QtQuick/private/qquickitemchangelistener_p.h>
10#include <QtQuickTemplates2/private/qquickpopup_p.h>
11#include <QtQuickTemplates2/private/qquickpopupitem_p_p.h>
12#include <QtQuickTemplates2/private/qquickpopupwindow_p_p.h>
13
14QT_BEGIN_NAMESPACE
15
16Q_LOGGING_CATEGORY(lcAttached, "qt.quick.controls.attachedpropertypropagator")
17
18/*!
19 \class QQuickAttachedPropertyPropagator
20 \brief The QQuickAttachedPropertyPropagator class provides a way to
21 propagate attached properties.
22 \inmodule QtQuickControls2
23 \since 6.5
24
25 In QML, it is possible to
26 \l {Attached Properties and Attached Signal Handlers}{attach properties and
27 signal handlers} to objects. \l {Providing Attached Properties} goes into more
28 detail about how to expose your own C++ attached types.
29
30 QQuickAttachedPropertyPropagator provides an API to propagate attached
31 properties from a parent object to its children, similar to
32 \l {Control::}{font} and \l {Item::}{palette} propagation. It supports
33 propagation through \l {Item}{items}, \l {Popup}{popups}, and
34 \l {Window}{windows}.
35
36 If propagation of properties is not important, consider using a
37 \l {QML_SINGLETON}{C++} or
38 \l {Contents of a Module Definition qmldir File}{QML} singleton instead,
39 as it is better suited for that use case, and is more efficient in that
40 it only requires one QObject.
41
42 To implement a custom attached property:
43
44 \list 1
45 \li Derive a class that exposes the attached property from
46 QQuickAttachedPropertyPropagator.
47
48 For example, to implement an attached \c {MyStyle.theme} property,
49 declare the \c {MyStyle} class:
50
51 \quotefromfile ../../examples/quickcontrols/attachedstyleproperties/MyStyle/mystyle.h
52 \skipto class
53 \printto {
54
55 \li Call \l initialize() in the constructor of your class:
56
57 \quotefromfile ../../examples/quickcontrols/attachedstyleproperties/MyStyle/mystyle.cpp
58 \skipto MyStyle::MyStyle
59 \printuntil }
60
61 \li Define set/inherit/propagate/reset functions for the attached property
62 as needed. For example, to define a \c theme attached property:
63
64 \quotefromfile ../../examples/quickcontrols/attachedstyleproperties/MyStyle/mystyle.cpp
65 \skipto theme()
66 \printto MyStyle::themeChange()
67
68 \li Reimplement \l attachedParentChange() to handle property inheritance:
69
70 \quotefromfile ../../examples/quickcontrols/attachedstyleproperties/MyStyle/mystyle.cpp
71 \skipto attachedParentChange
72 \printuntil /^\}/
73
74 \li Implement a static \c qmlAttachedProperties function and declare the
75 type as an attached QML type with \l QML_ELEMENT and \l QML_ATTACHED,
76 as detailed in \l {Providing Attached Properties}:
77
78 \quotefromfile ../../examples/quickcontrols/attachedstyleproperties/MyStyle/mystyle.cpp
79 \skipto qmlAttachedProperties
80 \printuntil }
81
82 \endlist
83
84 The complete implementation is available in
85 \l {Qt Quick Controls - Attached Style Properties Example}.
86
87 \sa {Styling Qt Quick Controls}
88*/
89
90static QQuickAttachedPropertyPropagator *attachedObject(const QMetaObject *type, QObject *object, bool create = false)
91{
92 if (!object)
93 return nullptr;
94 auto func = qmlAttachedPropertiesFunction(object, type);
95 return qobject_cast<QQuickAttachedPropertyPropagator *>(object: qmlAttachedPropertiesObject(object, func, create));
96}
97
98/*!
99 \internal
100
101 Tries to find a QQuickAttachedPropertyPropagator whose type is \a ourAttachedType
102 and is attached to an ancestor of \a objectWeAreAttachedTo.
103
104 QQuickAttachedPropertyPropagator needs to know who its parent attached object is in
105 order to inherit attached property values from it. This is called when an
106 instance of QQuickAttachedPropertyPropagator is created, and whenever
107 \c {objectWeAreAttachedTo}'s parent changes, for example.
108*/
109static QQuickAttachedPropertyPropagator *findAttachedParent(const QMetaObject *ourAttachedType, QObject *objectWeAreAttachedTo)
110{
111 qCDebug(lcAttached).noquote() << "findAttachedParent called with" << ourAttachedType->className() << objectWeAreAttachedTo;
112 /*
113 In the Material ComboBox.qml, we have code like this:
114
115 popup: T.Popup {
116 // ...
117 Material.theme: control.Material.theme
118 // ...
119
120 background: Rectangle {
121 //...
122 color: parent.Material.dialogColor
123
124 The Material attached object has to be accessed this way due to
125 deferred execution limitations (see 3e87695fb4b1a5d503c744046e6d9f43a2ae18a6).
126 However, since parent here refers to QQuickPopupItem and not the popup,
127 the color will actually come from the window. If a dark theme was set on
128 the ComboBox, it will not be respected in the background if we don't have
129 the check below.
130 */
131 auto popupItem = qobject_cast<QQuickPopupItem *>(object: objectWeAreAttachedTo);
132 if (popupItem) {
133 qCDebug(lcAttached).noquote() << "- attachee belongs to popup item" << popupItem << "- checking if it has an attached object";
134 auto popupItemPrivate = QQuickPopupItemPrivate::get(popupItem);
135 QQuickAttachedPropertyPropagator *popupAttached = attachedObject(type: ourAttachedType, object: popupItemPrivate->popup);
136 if (popupAttached) {
137 qCDebug(lcAttached).noquote() << "- popup item has attached object" << popupAttached << "- returning";
138 return popupAttached;
139 } else {
140 qCDebug(lcAttached).noquote() << "- popup item does not have attached object";
141 }
142 } else {
143 qCDebug(lcAttached).noquote() << "- attachee does not belong to a popup";
144 }
145
146 QQuickItem *item = qobject_cast<QQuickItem *>(o: objectWeAreAttachedTo);
147 if (item) {
148 qCDebug(lcAttached).noquote() << "- attachee is an item; checking its parent items and popups";
149 // lookup parent items and popups
150 QQuickItem *parent = item->parentItem();
151 while (parent) {
152 qCDebug(lcAttached).noquote() << " - checking parent item" << parent;
153 QQuickAttachedPropertyPropagator *attached = attachedObject(type: ourAttachedType, object: parent);
154 if (attached) {
155 qCDebug(lcAttached).noquote() << " - parent item has attached object" << attached << "- returning";
156 return attached;
157 }
158
159 QQuickPopup *popup = qobject_cast<QQuickPopup *>(object: parent->parent());
160 if (popup) {
161 qCDebug(lcAttached).noquote() << " - parent popup has attached object" << attached << "- returning";
162 return attachedObject(type: ourAttachedType, object: popup);
163 }
164
165 parent = parent->parentItem();
166 }
167
168 // fallback to item's window
169 qCDebug(lcAttached).noquote() << "- checking parent window" << item->window();
170 QQuickAttachedPropertyPropagator *attached = attachedObject(type: ourAttachedType, object: item->window());
171 if (attached) {
172 qCDebug(lcAttached).noquote() << "- parent window has attached object" << attached << "- returning";
173 return attached;
174 }
175 } else {
176 // lookup popup's window
177 QQuickPopup *popup = qobject_cast<QQuickPopup *>(object: objectWeAreAttachedTo);
178 if (popup) {
179 qCDebug(lcAttached).noquote() << "- attachee is a popup; checking its window";
180 auto* popupWindow = popup->popupItem()->window();
181 auto *object = attachedObject(type: ourAttachedType, object: popupWindow);
182 // Check if the attached object exists for the popup window. If it
183 // doesn't, use transient parent attached object
184 if (!object && qobject_cast<QQuickPopupWindow *>(object: popupWindow))
185 return attachedObject(type: ourAttachedType, object: popupWindow->transientParent());
186 return object;
187 }
188 }
189
190 // lookup parent window
191 QQuickWindow *window = qobject_cast<QQuickWindow *>(object: objectWeAreAttachedTo);
192 if (window) {
193 // It doesn't seem like a parent window can be anything but transient in Qt Quick.
194 QQuickWindow *parentWindow = qobject_cast<QQuickWindow *>(object: window->transientParent());
195 qCDebug(lcAttached).noquote() << "- attachee is a window; checking its parent window" << parentWindow;
196 if (parentWindow) {
197 QQuickAttachedPropertyPropagator *attached = attachedObject(type: ourAttachedType, object: parentWindow);
198 if (attached) {
199 qCDebug(lcAttached).noquote() << "- parent window has attached object" << attached << "- returning";
200 return attached;
201 }
202 }
203 }
204
205 // fallback to engine (global)
206 if (objectWeAreAttachedTo) {
207 QQmlEngine *engine = qmlEngine(objectWeAreAttachedTo);
208 qCDebug(lcAttached).noquote() << "- falling back to engine" << engine;
209 if (engine) {
210 QByteArray name = QByteArray("_q_") + ourAttachedType->className();
211 QQuickAttachedPropertyPropagator *attached = engine->property(name).value<QQuickAttachedPropertyPropagator *>();
212 if (!attached) {
213 attached = attachedObject(type: ourAttachedType, object: engine, create: true);
214 engine->setProperty(name, value: QVariant::fromValue(value: attached));
215 }
216 return attached;
217 }
218 }
219
220 return nullptr;
221}
222
223static QList<QQuickAttachedPropertyPropagator *> findAttachedChildren(const QMetaObject *type, QObject *object)
224{
225 QList<QQuickAttachedPropertyPropagator *> children;
226
227 QQuickItem *item = qobject_cast<QQuickItem *>(o: object);
228 if (!item) {
229 QQuickWindow *window = qobject_cast<QQuickWindow *>(object);
230 if (window)
231 item = window->contentItem();
232 }
233
234 if (!item)
235 return children;
236
237 // At this point, "item" could either be an item that the attached object is
238 // attached to directly, or the contentItem of a window that the attached object
239 // is attached to.
240
241 // Look for attached properties on items.
242 const auto childItems = item->childItems();
243 for (QQuickItem *child : childItems) {
244 QQuickAttachedPropertyPropagator *attached = attachedObject(type, object: child);
245 if (attached)
246 children += attached;
247 else
248 children += findAttachedChildren(type, object: child);
249 }
250
251 // Look for attached properties on windows. Windows declared in QML
252 // as children of a Window are QObject-parented to the contentItem (see
253 // QQuickWindowPrivate::data_append()). Windows declared as children
254 // of items are QObject-parented to those items.
255 const auto &windowChildren = item->children();
256 for (QObject *child : windowChildren) {
257 QQuickWindow *childWindow = qobject_cast<QQuickWindow *>(object: child);
258 if (childWindow) {
259 QQuickAttachedPropertyPropagator *attached = attachedObject(type, object: childWindow);
260 if (attached)
261 children += attached;
262 }
263 }
264
265 return children;
266}
267
268static QQuickItem *findAttachedItem(QObject *parent)
269{
270 QQuickItem *item = qobject_cast<QQuickItem *>(o: parent);
271 if (!item) {
272 QQuickPopup *popup = qobject_cast<QQuickPopup *>(object: parent);
273 if (popup)
274 item = popup->popupItem();
275 }
276 return item;
277}
278
279class QQuickAttachedPropertyPropagatorPrivate : public QObjectPrivate, public QQuickItemChangeListener
280{
281public:
282 Q_DECLARE_PUBLIC(QQuickAttachedPropertyPropagator)
283
284 static QQuickAttachedPropertyPropagatorPrivate *get(QQuickAttachedPropertyPropagator *attachedObject)
285 {
286 return attachedObject->d_func();
287 }
288
289 void attachTo(QObject *object);
290 void detachFrom(QObject *object);
291 void setAttachedParent(QQuickAttachedPropertyPropagator *parent);
292
293 void itemWindowChanged(QQuickWindow *window);
294 void transientParentWindowChanged(QWindow *newTransientParent);
295 void itemParentChanged(QQuickItem *item, QQuickItem *parent) override;
296
297 QList<QQuickAttachedPropertyPropagator *> attachedChildren;
298 QPointer<QQuickAttachedPropertyPropagator> attachedParent;
299};
300
301void QQuickAttachedPropertyPropagatorPrivate::attachTo(QObject *object)
302{
303 if (QQuickItem *item = findAttachedItem(parent: object)) {
304 connect(sender: item, signal: &QQuickItem::windowChanged, receiverPrivate: this, slot: &QQuickAttachedPropertyPropagatorPrivate::itemWindowChanged);
305 QQuickItemPrivate::get(item)->addItemChangeListener(listener: this, types: QQuickItemPrivate::Parent);
306 } else if (auto *window = qobject_cast<QQuickWindow *>(object)) {
307 QObjectPrivate::connect(sender: window, signal: &QWindow::transientParentChanged, receiverPrivate: this,
308 slot: &QQuickAttachedPropertyPropagatorPrivate::transientParentWindowChanged);
309 }
310}
311
312void QQuickAttachedPropertyPropagatorPrivate::detachFrom(QObject *object)
313{
314 if (QQuickItem *item = findAttachedItem(parent: object)) {
315 disconnect(sender: item, signal: &QQuickItem::windowChanged, receiverPrivate: this, slot: &QQuickAttachedPropertyPropagatorPrivate::itemWindowChanged);
316 QQuickItemPrivate::get(item)->removeItemChangeListener(this, types: QQuickItemPrivate::Parent);
317 } else if (auto *window = qobject_cast<QQuickWindow *>(object)) {
318 QObjectPrivate::disconnect(sender: window, signal: &QWindow::transientParentChanged,
319 receiverPrivate: this, slot: &QQuickAttachedPropertyPropagatorPrivate::transientParentWindowChanged);
320 }
321}
322
323/*!
324 \internal
325
326 This function sets the attached parent of this attached object.
327
328 Currently it is called when:
329 \list
330 \li The target item's parent changes.
331 \li The target item's window changes.
332 \li The attached object is constructed, to set the attached parent
333 and the attached parent of the attached object children.
334 \li The attached object is destructed.
335 \endlist
336
337 \quotefromfile ../../examples/quickcontrols/attachedstyleproperties/MyStyle/mystyle.cpp
338 \skipto MyStyle::resetTheme
339 \printuntil }
340*/
341void QQuickAttachedPropertyPropagatorPrivate::setAttachedParent(QQuickAttachedPropertyPropagator *parent)
342{
343 Q_Q(QQuickAttachedPropertyPropagator);
344 if (attachedParent == parent)
345 return;
346
347 QQuickAttachedPropertyPropagator *oldParent = attachedParent;
348 qCDebug(lcAttached).noquote() << "setAttachedParent called on" << q << "with parent" << parent;
349 if (attachedParent) {
350 qCDebug(lcAttached).noquote() << "- removing ourselves as an attached child of" << attachedParent;
351 QQuickAttachedPropertyPropagatorPrivate::get(attachedObject: attachedParent)->attachedChildren.removeOne(t: q);
352 }
353 attachedParent = parent;
354 if (parent) {
355 qCDebug(lcAttached).noquote() << "- adding ourselves as an attached child of" << parent;
356 QQuickAttachedPropertyPropagatorPrivate::get(attachedObject: parent)->attachedChildren.append(t: q);
357 }
358 q->attachedParentChange(newParent: parent, oldParent);
359}
360
361/*
362 If there's e.g. code like this:
363
364 Behavior on Material.elevation {}
365
366 The meta type will be something like QQuickMaterialStyle_QML_125,
367 whereas QQmlMetaType::attachedPropertiesFunc only has attached
368 property data for QQuickMaterialStyle (i.e. attached property types
369 created from C++). We work around this by finding the first C++
370 meta object, which works even for attached types created in QML.
371*/
372const QMetaObject *firstCppMetaObject(QQuickAttachedPropertyPropagator *propagator)
373{
374 return QQmlData::ensurePropertyCache(object: propagator)->firstCppMetaObject();
375}
376
377void QQuickAttachedPropertyPropagatorPrivate::itemWindowChanged(QQuickWindow *window)
378{
379 Q_Q(QQuickAttachedPropertyPropagator);
380 QQuickAttachedPropertyPropagator *attachedParent = nullptr;
381 qCDebug(lcAttached).noquote() << "window of" << q << "changed to" << window;
382
383 attachedParent = findAttachedParent(ourAttachedType: firstCppMetaObject(propagator: q), objectWeAreAttachedTo: q->parent());
384 if (!attachedParent)
385 attachedParent = attachedObject(type: firstCppMetaObject(propagator: q), object: window);
386 setAttachedParent(attachedParent);
387}
388
389void QQuickAttachedPropertyPropagatorPrivate::transientParentWindowChanged(QWindow *newTransientParent)
390{
391 Q_Q(QQuickAttachedPropertyPropagator);
392 QQuickAttachedPropertyPropagator *attachedParent = nullptr;
393 qCDebug(lcAttached).noquote() << "transient parent window of" << q << "changed to" << newTransientParent;
394 attachedParent = findAttachedParent(ourAttachedType: firstCppMetaObject(propagator: q), objectWeAreAttachedTo: q->parent());
395 if (!attachedParent)
396 attachedParent = attachedObject(type: firstCppMetaObject(propagator: q), object: newTransientParent);
397 setAttachedParent(attachedParent);
398}
399
400void QQuickAttachedPropertyPropagatorPrivate::itemParentChanged(QQuickItem *item, QQuickItem *parent)
401{
402 Q_Q(QQuickAttachedPropertyPropagator);
403 Q_UNUSED(item);
404 Q_UNUSED(parent);
405 setAttachedParent(findAttachedParent(ourAttachedType: firstCppMetaObject(propagator: q), objectWeAreAttachedTo: q->parent()));
406}
407
408/*!
409 Constructs a QQuickAttachedPropertyPropagator with the given \a parent.
410
411 The \c parent will be used to find this object's
412 \l {attachedParent()}{attached parent}.
413
414 Derived classes should call \l initialize() in their constructor.
415*/
416QQuickAttachedPropertyPropagator::QQuickAttachedPropertyPropagator(QObject *parent)
417 : QObject(*(new QQuickAttachedPropertyPropagatorPrivate), parent)
418{
419 Q_D(QQuickAttachedPropertyPropagator);
420 d->attachTo(object: parent);
421}
422
423/*!
424 Destroys the QQuickAttachedPropertyPropagator.
425*/
426QQuickAttachedPropertyPropagator::~QQuickAttachedPropertyPropagator()
427{
428 Q_D(QQuickAttachedPropertyPropagator);
429 d->detachFrom(object: parent());
430 d->setAttachedParent(nullptr);
431}
432
433/*!
434 This function returns the attached children of this attached object.
435
436 The attached children are used when propagating property values:
437
438 \quotefromfile ../../examples/quickcontrols/attachedstyleproperties/MyStyle/mystyle.cpp
439 \skipto MyStyle::propagateTheme
440 \printuntil }
441 \printuntil }
442*/
443QList<QQuickAttachedPropertyPropagator *> QQuickAttachedPropertyPropagator::attachedChildren() const
444{
445 Q_D(const QQuickAttachedPropertyPropagator);
446 return d->attachedChildren;
447}
448
449/*!
450 This function returns the attached parent of this attached object.
451
452 The attached parent is used when inheriting property values:
453
454 \quotefromfile ../../examples/quickcontrols/attachedstyleproperties/MyStyle/mystyle.cpp
455 \skipto MyStyle::resetTheme
456 \printuntil }
457*/
458QQuickAttachedPropertyPropagator *QQuickAttachedPropertyPropagator::attachedParent() const
459{
460 Q_D(const QQuickAttachedPropertyPropagator);
461 return d->attachedParent;
462}
463
464/*!
465 Finds and sets the attached parent for this attached object, and then does
466 the same for its children. This must be called upon construction of the
467 attached object in order for propagation to work.
468
469 It can be useful to read global/default values before calling this
470 function. For example, before calling \c initialize(), the
471 \l {Imagine Style}{Imagine} style checks a static "globalsInitialized" flag
472 to see if it should read default values from \l QSettings. The values from
473 that file form the basis for any attached property values that have not
474 been explicitly set.
475
476 \quotefromfile ../../examples/quickcontrols/attachedstyleproperties/MyStyle/mystyle.cpp
477 \skipto MyStyle::MyStyle
478 \printuntil }
479*/
480void QQuickAttachedPropertyPropagator::initialize()
481{
482 Q_D(QQuickAttachedPropertyPropagator);
483 qCDebug(lcAttached) << "initialize called for" << parent() << "- looking for attached parent...";
484 QQuickAttachedPropertyPropagator *attachedParent = findAttachedParent(ourAttachedType: metaObject(), objectWeAreAttachedTo: parent());
485 if (attachedParent)
486 d->setAttachedParent(attachedParent);
487
488 const QList<QQuickAttachedPropertyPropagator *> attachedChildren = findAttachedChildren(type: metaObject(), object: parent());
489 qCDebug(lcAttached) << "- found" << attachedChildren.size() << "attached children:";
490 for (QQuickAttachedPropertyPropagator *child : attachedChildren) {
491 qCDebug(lcAttached) << " -" << child->parent();
492 QQuickAttachedPropertyPropagatorPrivate::get(attachedObject: child)->setAttachedParent(this);
493 }
494
495 qCDebug(lcAttached) << "... finished initializing";
496}
497
498/*!
499 This function is called whenever the attached parent of this
500 QQuickAttachedPropertyPropagator changes from \a oldParent to \a newParent.
501
502 Subclasses should reimplement this function to inherit attached properties
503 from \c newParent.
504
505 \quotefromfile ../../examples/quickcontrols/attachedstyleproperties/MyStyle/mystyle.cpp
506 \skipto attachedParentChange
507 \printuntil }
508 \printuntil }
509*/
510void QQuickAttachedPropertyPropagator::attachedParentChange(QQuickAttachedPropertyPropagator *newParent, QQuickAttachedPropertyPropagator *oldParent)
511{
512 Q_UNUSED(newParent);
513 Q_UNUSED(oldParent);
514}
515
516#ifndef QT_NO_DEBUG_STREAM
517QDebug operator<<(QDebug debug, const QQuickAttachedPropertyPropagator *propagator)
518{
519 QDebugStateSaver saver(debug);
520 debug.nospace().noquote();
521 if (!propagator) {
522 debug << "QQuickAttachedPropertyPropagator(nullptr)";
523 return debug;
524 }
525
526 // Cast to QObject to avoid recursion.
527 debug << static_cast<const QObject *>(propagator) << " (which is attached to " << propagator->parent() << ')';
528 return debug;
529}
530#endif
531
532QT_END_NAMESPACE
533
534#include "moc_qquickattachedpropertypropagator.cpp"
535

source code of qtdeclarative/src/quickcontrols/qquickattachedpropertypropagator.cpp