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

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