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_STATIC_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 is a 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 is not a popup item";
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,
280 public QSafeQuickItemChangeListener<QQuickAttachedPropertyPropagatorPrivate>
281{
282public:
283 Q_DECLARE_PUBLIC(QQuickAttachedPropertyPropagator)
284
285 static QQuickAttachedPropertyPropagatorPrivate *get(QQuickAttachedPropertyPropagator *attachedObject)
286 {
287 return attachedObject->d_func();
288 }
289
290 void attachTo(QObject *object);
291 void detachFrom(QObject *object);
292 void setAttachedParent(QQuickAttachedPropertyPropagator *parent);
293
294 void itemWindowChanged(QQuickWindow *window);
295 void transientParentWindowChanged(QWindow *newTransientParent);
296 void itemParentChanged(QQuickItem *item, QQuickItem *parent) override;
297
298 QList<QQuickAttachedPropertyPropagator *> attachedChildren;
299 QPointer<QQuickAttachedPropertyPropagator> attachedParent;
300};
301
302void QQuickAttachedPropertyPropagatorPrivate::attachTo(QObject *object)
303{
304 if (QQuickItem *item = findAttachedItem(parent: object)) {
305 connect(sender: item, signal: &QQuickItem::windowChanged, receiverPrivate: this, slot: &QQuickAttachedPropertyPropagatorPrivate::itemWindowChanged);
306 QQuickItemPrivate::get(item)->addItemChangeListener(listener: this, types: QQuickItemPrivate::Parent);
307 } else if (auto *window = qobject_cast<QQuickWindow *>(object)) {
308 QObjectPrivate::connect(sender: window, signal: &QWindow::transientParentChanged, receiverPrivate: this,
309 slot: &QQuickAttachedPropertyPropagatorPrivate::transientParentWindowChanged);
310 }
311}
312
313void QQuickAttachedPropertyPropagatorPrivate::detachFrom(QObject *object)
314{
315 if (QQuickItem *item = findAttachedItem(parent: object)) {
316 disconnect(sender: item, signal: &QQuickItem::windowChanged, receiverPrivate: this, slot: &QQuickAttachedPropertyPropagatorPrivate::itemWindowChanged);
317 QQuickItemPrivate::get(item)->removeItemChangeListener(this, types: QQuickItemPrivate::Parent);
318 } else if (auto *window = qobject_cast<QQuickWindow *>(object)) {
319 QObjectPrivate::disconnect(sender: window, signal: &QWindow::transientParentChanged,
320 receiverPrivate: this, slot: &QQuickAttachedPropertyPropagatorPrivate::transientParentWindowChanged);
321 }
322}
323
324/*!
325 \internal
326
327 This function sets the attached parent of this attached object.
328
329 Currently it is called when:
330 \list
331 \li The target item's parent changes.
332 \li The target item's window changes.
333 \li The attached object is constructed, to set the attached parent
334 and the attached parent of the attached object children.
335 \li The attached object is destructed.
336 \endlist
337
338 \quotefromfile ../../examples/quickcontrols/attachedstyleproperties/MyStyle/mystyle.cpp
339 \skipto MyStyle::resetTheme
340 \printuntil }
341*/
342void QQuickAttachedPropertyPropagatorPrivate::setAttachedParent(QQuickAttachedPropertyPropagator *parent)
343{
344 Q_Q(QQuickAttachedPropertyPropagator);
345 if (attachedParent == parent)
346 return;
347
348 QQuickAttachedPropertyPropagator *oldParent = attachedParent;
349 qCDebug(lcAttached).noquote() << "setAttachedParent called on" << q << "with parent" << parent;
350 if (attachedParent) {
351 qCDebug(lcAttached).noquote() << "- removing ourselves as an attached child of" << attachedParent;
352 QQuickAttachedPropertyPropagatorPrivate::get(attachedObject: attachedParent)->attachedChildren.removeOne(t: q);
353 }
354 attachedParent = parent;
355 if (parent) {
356 qCDebug(lcAttached).noquote() << "- adding ourselves as an attached child of" << parent;
357 QQuickAttachedPropertyPropagatorPrivate::get(attachedObject: parent)->attachedChildren.append(t: q);
358 }
359 q->attachedParentChange(newParent: parent, oldParent);
360}
361
362/*
363 If there's e.g. code like this:
364
365 Behavior on Material.elevation {}
366
367 The meta type will be something like QQuickMaterialStyle_QML_125,
368 whereas QQmlMetaType::attachedPropertiesFunc only has attached
369 property data for QQuickMaterialStyle (i.e. attached property types
370 created from C++). We work around this by finding the first C++
371 meta object, which works even for attached types created in QML.
372*/
373const QMetaObject *firstCppMetaObject(QQuickAttachedPropertyPropagator *propagator)
374{
375 return QQmlData::ensurePropertyCache(object: propagator)->firstCppMetaObject();
376}
377
378void QQuickAttachedPropertyPropagatorPrivate::itemWindowChanged(QQuickWindow *window)
379{
380 Q_Q(QQuickAttachedPropertyPropagator);
381 QQuickAttachedPropertyPropagator *attachedParent = nullptr;
382 qCDebug(lcAttached).noquote() << "window of" << q << "changed to" << window;
383
384 attachedParent = findAttachedParent(ourAttachedType: firstCppMetaObject(propagator: q), objectWeAreAttachedTo: q->parent());
385 if (!attachedParent)
386 attachedParent = attachedObject(type: firstCppMetaObject(propagator: q), object: window);
387 setAttachedParent(attachedParent);
388}
389
390void QQuickAttachedPropertyPropagatorPrivate::transientParentWindowChanged(QWindow *newTransientParent)
391{
392 Q_Q(QQuickAttachedPropertyPropagator);
393 QQuickAttachedPropertyPropagator *attachedParent = nullptr;
394 qCDebug(lcAttached).noquote() << "transient parent window of" << q << "changed to" << newTransientParent;
395 attachedParent = findAttachedParent(ourAttachedType: firstCppMetaObject(propagator: q), objectWeAreAttachedTo: q->parent());
396 if (!attachedParent)
397 attachedParent = attachedObject(type: firstCppMetaObject(propagator: q), object: newTransientParent);
398 setAttachedParent(attachedParent);
399}
400
401void QQuickAttachedPropertyPropagatorPrivate::itemParentChanged(QQuickItem *item, QQuickItem *parent)
402{
403 Q_Q(QQuickAttachedPropertyPropagator);
404 Q_UNUSED(item);
405 Q_UNUSED(parent);
406 setAttachedParent(findAttachedParent(ourAttachedType: firstCppMetaObject(propagator: q), objectWeAreAttachedTo: q->parent()));
407}
408
409/*!
410 Constructs a QQuickAttachedPropertyPropagator with the given \a parent.
411
412 The \c parent will be used to find this object's
413 \l {attachedParent()}{attached parent}.
414
415 Derived classes should call \l initialize() in their constructor.
416*/
417QQuickAttachedPropertyPropagator::QQuickAttachedPropertyPropagator(QObject *parent)
418 : QObject(*(new QQuickAttachedPropertyPropagatorPrivate), parent)
419{
420 Q_D(QQuickAttachedPropertyPropagator);
421 d->attachTo(object: parent);
422}
423
424/*!
425 Destroys the QQuickAttachedPropertyPropagator.
426*/
427QQuickAttachedPropertyPropagator::~QQuickAttachedPropertyPropagator()
428{
429 Q_D(QQuickAttachedPropertyPropagator);
430 d->detachFrom(object: parent());
431 d->setAttachedParent(nullptr);
432}
433
434/*!
435 This function returns the attached children of this attached object.
436
437 The attached children are used when propagating property values:
438
439 \quotefromfile ../../examples/quickcontrols/attachedstyleproperties/MyStyle/mystyle.cpp
440 \skipto MyStyle::propagateTheme
441 \printuntil }
442 \printuntil }
443*/
444QList<QQuickAttachedPropertyPropagator *> QQuickAttachedPropertyPropagator::attachedChildren() const
445{
446 Q_D(const QQuickAttachedPropertyPropagator);
447 return d->attachedChildren;
448}
449
450/*!
451 This function returns the attached parent of this attached object.
452
453 The attached parent is used when inheriting property values:
454
455 \quotefromfile ../../examples/quickcontrols/attachedstyleproperties/MyStyle/mystyle.cpp
456 \skipto MyStyle::resetTheme
457 \printuntil }
458*/
459QQuickAttachedPropertyPropagator *QQuickAttachedPropertyPropagator::attachedParent() const
460{
461 Q_D(const QQuickAttachedPropertyPropagator);
462 return d->attachedParent;
463}
464
465/*!
466 Finds and sets the attached parent for this attached object, and then does
467 the same for its children. This must be called upon construction of the
468 attached object in order for propagation to work.
469
470 It can be useful to read global/default values before calling this
471 function. For example, before calling \c initialize(), the
472 \l {Imagine Style}{Imagine} style checks a static "globalsInitialized" flag
473 to see if it should read default values from \l QSettings. The values from
474 that file form the basis for any attached property values that have not
475 been explicitly set.
476
477 \quotefromfile ../../examples/quickcontrols/attachedstyleproperties/MyStyle/mystyle.cpp
478 \skipto MyStyle::MyStyle
479 \printuntil }
480*/
481void QQuickAttachedPropertyPropagator::initialize()
482{
483 Q_D(QQuickAttachedPropertyPropagator);
484 qCDebug(lcAttached) << "initialize called for" << parent() << "- looking for attached parent...";
485 QQuickAttachedPropertyPropagator *attachedParent = findAttachedParent(ourAttachedType: metaObject(), objectWeAreAttachedTo: parent());
486 if (attachedParent)
487 d->setAttachedParent(attachedParent);
488
489 const QList<QQuickAttachedPropertyPropagator *> attachedChildren = findAttachedChildren(type: metaObject(), object: parent());
490 qCDebug(lcAttached) << "- found" << attachedChildren.size() << "attached children:";
491 for (QQuickAttachedPropertyPropagator *child : attachedChildren) {
492 qCDebug(lcAttached) << " -" << child->parent();
493 QQuickAttachedPropertyPropagatorPrivate::get(attachedObject: child)->setAttachedParent(this);
494 }
495
496 qCDebug(lcAttached) << "... finished initializing";
497}
498
499/*!
500 This function is called whenever the attached parent of this
501 QQuickAttachedPropertyPropagator changes from \a oldParent to \a newParent.
502
503 Subclasses should reimplement this function to inherit attached properties
504 from \c newParent.
505
506 \quotefromfile ../../examples/quickcontrols/attachedstyleproperties/MyStyle/mystyle.cpp
507 \skipto attachedParentChange
508 \printuntil }
509 \printuntil }
510*/
511void QQuickAttachedPropertyPropagator::attachedParentChange(QQuickAttachedPropertyPropagator *newParent, QQuickAttachedPropertyPropagator *oldParent)
512{
513 Q_UNUSED(newParent);
514 Q_UNUSED(oldParent);
515}
516
517#ifndef QT_NO_DEBUG_STREAM
518QDebug operator<<(QDebug debug, const QQuickAttachedPropertyPropagator *propagator)
519{
520 QDebugStateSaver saver(debug);
521 debug.nospace().noquote();
522 if (!propagator) {
523 debug << "QQuickAttachedPropertyPropagator(nullptr)";
524 return debug;
525 }
526
527 // Cast to QObject to avoid recursion.
528 debug << static_cast<const QObject *>(propagator) << " (which is attached to " << propagator->parent() << ')';
529 return debug;
530}
531#endif
532
533QT_END_NAMESPACE
534
535#include "moc_qquickattachedpropertypropagator.cpp"
536

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