1// Copyright (C) 2023 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#include "qquicklayoutitemproxy_p.h"
4#include "qquicklayout_p.h"
5
6QT_BEGIN_NAMESPACE
7
8/*!
9 \qmltype LayoutItemProxy
10 \nativetype QQuickLayoutItemProxy
11 \inherits Item
12 \inqmlmodule QtQuick.Layouts
13 \ingroup layouts
14 \since QtQuick.Layouts 6.6
15 \brief The LayoutItemProxy class provides a placeholder for \l{QQuickItem}s
16 in layouts.
17
18 Some responsive layouts require different layout hierarchies for different
19 screen sizes, but the layout hierarchy is the same as the QML structure and
20 can therefore not be changed at runtime. LayoutItemProxy overcomes this
21 limitation by representing a \l{target} item within the layout. The
22 \l{target} item itself can be defined anywhere in the QML hierarchy. This
23 allows declaration of multiple layouts with the same content items. The
24 layouts can be shown and hidden to switch between them.
25
26 The LayoutItemProxy will try to take control of the \l{target} item if it
27 is \l [QML] {Item::}{visible}. Taking control will position and resize the
28 \l{target} item to match the position and size of the LayoutItemProxy.
29 Further, the LayoutItemProxy will set itself as the parent of the
30 \l{target} (to ensure event delivery and useful drawing order) and set the
31 visibility to \c true. Multiple LayoutItemProxies can \l{target} the same
32 item, but only one LayoutItemProxy can control an item at a time. Therefore
33 only one of the proxies targeting the same item should be visible at a
34 time. If multiple proxies target the same item but \e visible is set to
35 false for each proxy, the item will also be invisible.
36
37 All \l{Layout} attached properties of the \l {target}, as well as the
38 \l{QQuickItem::implicitWidth} and \l{QQuickItem::implicitHeight} of the
39 \l{target} are forwarded by the LayoutItemProxy. The LayoutItemProxy will
40 mimic the \l{target} as closely as possible in terms of \l{Layout}
41 properties and size. \l{Layout} attached properties can also be set
42 explicitly on the LayoutItemProxy which will stop the forwarding of the
43 \l {target} properties.
44
45 \section1 Example Usage
46
47 This is a minimalistic example, changing between two layouts using proxies
48 to use the same items in both layouts. The items that populate the layouts
49 can be defined at an arbitrary point in the QML structure.
50
51 \snippet layouts/simpleProxy.qml item definition
52
53 Then we can define the Layouts with LayoutItemProxys
54
55 \snippet layouts/simpleProxy.qml layout definition
56
57 We can switch now between the layouts, depending on a criterion of our
58 choice by toggling the visibility of the layouts on and off.
59
60 \snippet layouts/simpleProxy.qml layout choice
61
62 The two resulting layouts look like this:
63
64 \div {class="float-right"}
65 \inlineimage simpleProxy.png
66 \enddiv
67
68 The LayoutItemProxy can also be used without layouts, e.g. by anchoring it
69 to different items. A mix of real \l {Item}{Items} and proxy items is
70 equally possible, as well as nested structures of layouts and items.
71
72 \warning The LayoutItemProxy will set the parent of its target to itself.
73 Keep this in mind when referring to the parent of the target item.
74
75 \sa Item, GridLayout, RowLayout, ColumnLayout
76*/
77
78Q_STATIC_LOGGING_CATEGORY(lcLayouts, "qt.quick.layouts")
79
80
81QQuickLayoutItemProxy::QQuickLayoutItemProxy(QQuickItem *parent)
82 : QQuickItem(*new QQuickLayoutItemProxyPrivate, parent)
83{
84
85}
86
87QQuickLayoutItemProxy::~QQuickLayoutItemProxy()
88{
89 Q_D(QQuickLayoutItemProxy);
90
91 if (!d->target)
92 return;
93
94 QQuickLayoutItemProxyAttachedData * attachedData = d->target->property(name: "QQuickLayoutItemProxyAttachedData").value<QQuickLayoutItemProxyAttachedData*>();
95 // De-register this proxy from the proxies controlling the target
96 if (attachedData) {
97 if (attachedData->getControllingProxy() == this) {
98 attachedData->releaseControl(proxy: this);
99 d->target->setParentItem(nullptr);
100 }
101 attachedData->releaseProxy(proxy: this);
102 }
103 // The target item still has a QObject parent that takes care of its destrctuion.
104 // No need to invoke destruction of the target tiem from here.
105}
106
107/*! \internal
108 \brief QQuickLayoutItemProxy::geometryChange Reimplementation of
109 QQuickItem::geometryChange to update the target geometry too.
110*/
111void QQuickLayoutItemProxy::geometryChange(const QRectF &newGeom, const QRectF &oldGeom)
112{
113 QQuickItem::geometryChange(newGeometry: newGeom, oldGeometry: oldGeom);
114 if (!isVisible())
115 return;
116
117 const QSizeF sz = newGeom.size();
118 QPointF pos(0., 0.);
119
120 if (QQuickItem *t = effectiveTarget()) {
121 if (QQuickLayoutItemProxyAttachedData * attachedData = target()->property(name: "QQuickLayoutItemProxyAttachedData").value<QQuickLayoutItemProxyAttachedData*>()) {
122 if (attachedData->getControllingProxy() != this)
123 return;
124 }
125
126 // Should normally not be the case, except the user resets the parent
127 // This is a failsave for this case and positions the item correctly
128 if (t->parentItem() != this)
129 pos = t->parentItem()->mapFromGlobal(point: mapToGlobal(x: 0, y: 0));
130
131 if (t->size() == sz && t->position() == pos && newGeom == oldGeom)
132 return;
133
134 t->setSize(sz);
135 t->setPosition(pos);
136 }
137}
138
139/*! \internal
140 \brief QQuickLayoutItemProxy::itemChange is a reimplementation of
141 QQuickItem::itemChange to react to changes in visibility.
142*/
143void QQuickLayoutItemProxy::itemChange(ItemChange c, const ItemChangeData &d)
144{
145 if (c == QQuickItem::ItemVisibleHasChanged)
146 {
147 maybeTakeControl();
148 }
149 QQuickItem::itemChange(c, d);
150}
151
152// Implementation of the slots to react to changes of the Layout attached properties.
153// If the target Layout propertie change, we change the proxy Layout properties accordingly
154// If the proxy Layout properties have been changed externally, we want to remove this binding.
155// The member variables m_expectProxy##Property##Change help us keep track about who invokes
156// the change of the parameter. If it is invoked by the target we expect a proxy property
157// change and will not remove the connection.
158#define propertyForwarding(property, Property) \
159 void QQuickLayoutItemProxy::target##Property##Changed() { \
160 Q_D(QQuickLayoutItemProxy); \
161 QQuickLayoutAttached *attTarget = attachedLayoutObject(target(), false); \
162 QQuickLayoutAttached *attProxy = attachedLayoutObject(this, false); \
163 if (!attTarget) return; \
164 if (attProxy->property() == attTarget->property()) \
165 return; \
166 d->m_expectProxy##Property##Change = true; \
167 attProxy->set##Property(attTarget->property()); \
168 } \
169 void QQuickLayoutItemProxy::proxy##Property##Changed() { \
170 Q_D(QQuickLayoutItemProxy); \
171 if (d->m_expectProxy##Property##Change) { \
172 d->m_expectProxy##Property##Change = false; \
173 return; \
174 } \
175 QQuickLayoutAttached *attTarget = attachedLayoutObject(target(), false); \
176 if (!attTarget) return; \
177 disconnect(attTarget, &QQuickLayoutAttached::property##Changed, this, &QQuickLayoutItemProxy::target##Property##Changed); \
178 }
179
180propertyForwarding(minimumWidth, MinimumWidth)
181propertyForwarding(minimumHeight, MinimumHeight)
182propertyForwarding(preferredWidth, PreferredWidth)
183propertyForwarding(preferredHeight, PreferredHeight)
184propertyForwarding(maximumWidth, MaximumWidth)
185propertyForwarding(maximumHeight, MaximumHeight)
186propertyForwarding(fillWidth, FillWidth)
187propertyForwarding(fillHeight, FillHeight)
188propertyForwarding(alignment, Alignment)
189propertyForwarding(horizontalStretchFactor, HorizontalStretchFactor)
190propertyForwarding(verticalStretchFactor, VerticalStretchFactor)
191propertyForwarding(margins, Margins)
192propertyForwarding(leftMargin, LeftMargin)
193propertyForwarding(topMargin, TopMargin)
194propertyForwarding(rightMargin, RightMargin)
195propertyForwarding(bottomMargin, BottomMargin)
196
197#undef propertyForwarding
198
199/*!
200 \qmlproperty Item LayoutItemProxy::target
201
202 This property holds the \l Item that the proxy should represent in a
203 \l {Layout} hierarchy.
204*/
205
206/*! \internal
207 \brief QQuickLayoutItemProxy::target
208 \return The target item of the proxy
209*/
210QQuickItem *QQuickLayoutItemProxy::target() const
211{
212 Q_D(const QQuickLayoutItemProxy);
213 return d->target;
214}
215
216/*! \internal
217 \brief QQuickLayoutItemProxy::setTarget sets the target
218 \param newTarget The item that the proxy stands in place for.
219
220 All layout properties of the target are connected to the layout properties
221 of the LayoutItemProxy. It the LayoutItemProxy is visible, it will try to
222 take control of the target.
223*/
224void QQuickLayoutItemProxy::setTarget(QQuickItem *newTarget)
225{
226 Q_D(QQuickLayoutItemProxy);
227
228 if (newTarget == d->target)
229 return;
230
231 if (d->target && d->target->property(name: "QQuickLayoutItemProxyAttachedData").isValid()) {
232 QQuickLayoutItemProxyAttachedData *attachedData = d->target->property(name: "QQuickLayoutItemProxyAttachedData").value<QQuickLayoutItemProxyAttachedData*>();
233 attachedData->releaseProxy(proxy: this);
234 }
235 d->target = newTarget;
236
237 if (newTarget) {
238
239 QQuickLayoutItemProxyAttachedData *attachedData;
240 if (newTarget->property(name: "QQuickLayoutItemProxyAttachedData").isValid()) {
241 attachedData = newTarget->property(name: "QQuickLayoutItemProxyAttachedData").value<QQuickLayoutItemProxyAttachedData*>();
242 } else {
243 attachedData = new QQuickLayoutItemProxyAttachedData(newTarget);
244 QVariant v;
245 v.setValue(attachedData);
246 newTarget->setProperty(name: "QQuickLayoutItemProxyAttachedData", value: v);
247 }
248 attachedData->registerProxy(proxy: this);
249
250 // If there is no other controlling proxy, we will hide the target
251 if (!attachedData->proxyHasControl())
252 newTarget->setVisible(false);
253 // We are calling maybeTakeControl at the end to eventually take
254 // responsibility of showing the target.
255
256 if (QQuickLayoutAttached *attTarget = attachedLayoutObject(item: newTarget)) {
257 QQuickLayoutAttached *attProxy = attachedLayoutObject(item: this, create: true);
258
259 disconnect(sender: attTarget, signal: nullptr, receiver: attProxy, member: nullptr);
260
261 // bind item-specific layout properties:
262
263#define connectPropertyForwarding(property, Property) \
264 if (!attProxy->is##Property##Set()) { \
265 connect(attTarget, &QQuickLayoutAttached::property##Changed, this, &QQuickLayoutItemProxy::target##Property##Changed); \
266 connect(attProxy, &QQuickLayoutAttached::property##Changed, this, &QQuickLayoutItemProxy::proxy##Property##Changed); \
267 target##Property##Changed(); \
268 }
269 connectPropertyForwarding(minimumWidth, MinimumWidth)
270 connectPropertyForwarding(minimumHeight, MinimumHeight)
271 connectPropertyForwarding(preferredWidth, PreferredWidth)
272 connectPropertyForwarding(preferredHeight, PreferredHeight)
273 connectPropertyForwarding(maximumWidth, MaximumWidth)
274 connectPropertyForwarding(maximumHeight, MaximumHeight)
275 connectPropertyForwarding(fillWidth, FillWidth)
276 connectPropertyForwarding(fillHeight, FillHeight)
277 connectPropertyForwarding(alignment, Alignment)
278 connectPropertyForwarding(horizontalStretchFactor, HorizontalStretchFactor)
279 connectPropertyForwarding(verticalStretchFactor, VerticalStretchFactor)
280 connectPropertyForwarding(margins, Margins)
281 connectPropertyForwarding(leftMargin, LeftMargin)
282 connectPropertyForwarding(topMargin, TopMargin)
283 connectPropertyForwarding(rightMargin, RightMargin)
284 connectPropertyForwarding(bottomMargin, BottomMargin)
285#undef connectPropertyForwarding
286
287 // proxy.implicitWidth: target.implicitWidth
288 auto fnBindImplW = [newTarget, this](){ this->setImplicitWidth(newTarget->implicitWidth()); };
289 fnBindImplW();
290 connect(sender: newTarget, signal: &QQuickItem::implicitWidthChanged, slot&: fnBindImplW);
291
292 // proxy.implicitHeight: target.implicitHeight
293 auto fnBindImplH = [newTarget, this](){ this->setImplicitHeight(newTarget->implicitHeight()); };
294 fnBindImplH();
295 connect(sender: newTarget, signal: &QQuickItem::implicitHeightChanged, slot&: fnBindImplH);
296 }
297 }
298
299 if (isVisible())
300 maybeTakeControl();
301
302 emit targetChanged();
303}
304
305/*! \internal
306 \brief QQuickLayoutItemProxy::effectiveTarget
307 \return The target item of the proxy if it is in control, \c null otherwise.
308*/
309QQuickItem *QQuickLayoutItemProxy::effectiveTarget() const
310{
311 if (target() == nullptr)
312 return nullptr;
313
314 QQuickLayoutItemProxyAttachedData * attachedData = target()->property(name: "QQuickLayoutItemProxyAttachedData").value<QQuickLayoutItemProxyAttachedData*>();
315 return (attachedData->getControllingProxy() == this) ? target() : nullptr;
316}
317
318/*! \internal
319 \brief QQuickLayoutItemProxy::clearTarget sets the target to null.
320
321 This function is called if the target is destroyed to make sure we do not
322 try to access a non-existing object.
323*/
324void QQuickLayoutItemProxy::clearTarget()
325{
326 setTarget(nullptr);
327}
328
329/*! \internal
330 \brief QQuickLayoutItemProxy::maybeTakeControl checks and takes over control
331 of the item.
332
333 If the proxy is visible it will try to take control over the target and set
334 its visibility to true. If the proxy is hidden it will also hide the target
335 and another LayoutItemProxy has to set the visibility to \c true or the
336 target will stay invisible.
337*/
338void QQuickLayoutItemProxy::maybeTakeControl()
339{
340 Q_D(QQuickLayoutItemProxy);
341 if (!d->target)
342 return;
343
344 QQuickLayoutItemProxyAttachedData * attachedData = d->target->property(name: "QQuickLayoutItemProxyAttachedData").value<QQuickLayoutItemProxyAttachedData*>();
345 if (isVisible() && attachedData->getControllingProxy() != this) {
346 if (attachedData->takeControl(proxy: this)) {
347 d->target->setVisible(true);
348 d->target->setParentItem(this);
349 updatePos();
350 }
351 }
352 if (!isVisible() && attachedData->getControllingProxy() == this){
353 if (d->target->parentItem() == this) {
354 d->target->setParentItem(nullptr);
355 } else
356 qCDebug(lcLayouts) << "Parent was changed to" << d->target->parentItem() << "while an ItemProxy had control";
357 d->target->setVisible(false);
358 attachedData->releaseControl(proxy: this);
359 }
360}
361
362/*! \internal
363 \brief QQuickLayoutItemProxy::updatePos sets the geometry of the target to
364 the geometry of the proxy
365*/
366void QQuickLayoutItemProxy::updatePos()
367{
368 if (!isVisible())
369 return;
370 if (target()) {
371 if (QQuickLayoutItemProxyAttachedData * attachedData = target()->property(name: "QQuickLayoutItemProxyAttachedData").value<QQuickLayoutItemProxyAttachedData*>()) {
372 if (attachedData->getControllingProxy() == this)
373 geometryChange(newGeom: boundingRect(), oldGeom: boundingRect());
374 }
375 }
376}
377
378QQuickLayoutItemProxyPrivate::QQuickLayoutItemProxyPrivate()
379 : QQuickItemPrivate(),
380 m_expectProxyMinimumWidthChange(false),
381 m_expectProxyMinimumHeightChange(false),
382 m_expectProxyPreferredWidthChange(false),
383 m_expectProxyPreferredHeightChange(false),
384 m_expectProxyMaximumWidthChange(false),
385 m_expectProxyMaximumHeightChange(false),
386 m_expectProxyFillWidthChange(false),
387 m_expectProxyFillHeightChange(false),
388 m_expectProxyAlignmentChange(false),
389 m_expectProxyHorizontalStretchFactorChange(false),
390 m_expectProxyVerticalStretchFactorChange(false),
391 m_expectProxyMarginsChange(false),
392 m_expectProxyLeftMarginChange(false),
393 m_expectProxyTopMarginChange(false),
394 m_expectProxyRightMarginChange(false),
395 m_expectProxyBottomMarginChange(false)
396{
397
398}
399
400/*! \internal
401 \class QQuickLayoutItemProxyAttachedData
402 \brief Provides attached properties for items that are managed by one or
403 more LayoutItemProxy.
404
405 It stores all proxies that target the item, and will emit signals when the
406 proxies or the controlling proxy changes. Proxies can listen to the signal
407 and pick up control if they wish to.
408*/
409QQuickLayoutItemProxyAttachedData::QQuickLayoutItemProxyAttachedData(QObject *parent)
410 : QObject(parent), controllingProxy(nullptr)
411{
412
413}
414
415QQuickLayoutItemProxyAttachedData::~QQuickLayoutItemProxyAttachedData()
416{
417 if (QObject *par = parent())
418 par->setProperty(name: "QQuickLayoutItemProxyAttachedData", value: QVariant());
419
420 // If this is destroyed, so is the target. Clear the target from the
421 // proxies so they do not try to access a destroyed object
422 for (auto &proxy: std::as_const(t&: proxies))
423 proxy->clearTarget();
424}
425
426/*! \internal
427 \brief QQuickLayoutItemProxyAttachedData::registerProxy registers a proxy
428 that manages the item this data is attached to.
429
430 This is required to easily notify proxies when the target is destroyed or
431 when it is free to take over control.
432*/
433void QQuickLayoutItemProxyAttachedData::registerProxy(QQuickLayoutItemProxy *proxy)
434{
435 if (proxies.contains(t: proxy))
436 return;
437
438 proxies.append(t: proxy);
439 emit proxiesChanged();
440}
441
442/*! \internal
443 \brief QQuickLayoutItemProxyAttachedData::releaseProxy removes a proxy from
444 a list of known proxies that manage the item this data is attached to.
445*/
446void QQuickLayoutItemProxyAttachedData::releaseProxy(QQuickLayoutItemProxy *proxy)
447{
448 if (proxy == controllingProxy)
449 releaseControl(proxy);
450
451 proxies.removeAll(t: proxy);
452
453 if (proxies.isEmpty())
454 deleteLater();
455
456 emit proxiesChanged();
457}
458
459/*! \internal
460 \brief QQuickLayoutItemProxyAttachedData::takeControl is called by
461 LayoutItemProxies when they try to take control over the item this data is
462 attached to.
463 \return \c true if no other proxy controls the item and if control is
464 granted to the proxy, \c false otherwise.
465
466 \param proxy The proxy that tries to take control.
467*/
468bool QQuickLayoutItemProxyAttachedData::takeControl(QQuickLayoutItemProxy *proxy)
469{
470 if (controllingProxy || !proxies.contains(t: proxy))
471 return false;
472
473 qCDebug(lcLayouts) << proxy
474 << "takes control of"
475 << parent();
476
477 controllingProxy = proxy;
478 emit controlTaken();
479 emit controllingProxyChanged();
480 return true;
481}
482
483/*! \internal
484 \brief QQuickLayoutItemProxyAttachedData::releaseControl is called by
485 LayoutItemProxies when they try no longer control the item
486
487 \param proxy The proxy that gives up control.
488*/
489void QQuickLayoutItemProxyAttachedData::releaseControl(QQuickLayoutItemProxy *proxy)
490{
491 if (controllingProxy != proxy)
492 return;
493
494 qCDebug(lcLayouts) << proxy
495 << "no longer controls"
496 << parent();
497
498 controllingProxy = nullptr;
499 emit controlReleased();
500 emit controllingProxyChanged();
501
502 for (auto &otherProxy: std::as_const(t&: proxies)) {
503 if (proxy != otherProxy)
504 otherProxy->maybeTakeControl();
505 }
506}
507
508/*! \internal
509 \brief QQuickLayoutItemProxyAttachedData::getControllingProxy
510 \return the proxy that currently controls the item this data is attached to.
511 Returns \c null if no proxy controls the item.
512*/
513QQuickLayoutItemProxy *QQuickLayoutItemProxyAttachedData::getControllingProxy() const
514{
515 return controllingProxy;
516}
517
518/*! \internal
519 \brief QQuickLayoutItemProxyAttachedData::getProxies
520 \return a list of all proxies that target the item this data is attached to.
521*/
522QQmlListProperty<QQuickLayoutItemProxy> QQuickLayoutItemProxyAttachedData::getProxies()
523{
524 using Type = QQuickLayoutItemProxy;
525 using Property = QQmlListProperty<Type>;
526
527 return Property(
528 this, &proxies,
529 [](Property *p) { return static_cast<QList<Type *> *>(p->data)->size(); },
530 [](Property *p, qsizetype i) { return static_cast<QList<Type *> *>(p->data)->at(i); }
531 );
532}
533
534/*! \internal
535 \brief QQuickLayoutItemProxyAttachedData::proxyHasControl
536 \return \c true if a proxy is controlling the item, \c false otherwise.
537*/
538bool QQuickLayoutItemProxyAttachedData::proxyHasControl() const
539{
540 return controllingProxy != nullptr;
541}
542
543QT_END_NAMESPACE
544

source code of qtdeclarative/src/quicklayouts/qquicklayoutitemproxy.cpp