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

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