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 d->target = newTarget;
230
231 if (newTarget) {
232
233 QQuickLayoutItemProxyAttachedData *attachedData;
234 if (newTarget->property(name: "QQuickLayoutItemProxyAttachedData").isValid()) {
235 attachedData = newTarget->property(name: "QQuickLayoutItemProxyAttachedData").value<QQuickLayoutItemProxyAttachedData*>();
236 } else {
237 attachedData = new QQuickLayoutItemProxyAttachedData(newTarget);
238 QVariant v;
239 v.setValue(attachedData);
240 newTarget->setProperty(name: "QQuickLayoutItemProxyAttachedData", value: v);
241 }
242 attachedData->registerProxy(proxy: this);
243
244 // If there is no other controlling proxy, we will hide the target
245 if (!attachedData->proxyHasControl())
246 newTarget->setVisible(false);
247 // We are calling maybeTakeControl at the end to eventually take
248 // responsibility of showing the target.
249
250 if (QQuickLayoutAttached *attTarget = attachedLayoutObject(item: newTarget)) {
251 QQuickLayoutAttached *attProxy = attachedLayoutObject(item: this, create: true);
252
253 disconnect(sender: attTarget, signal: nullptr, receiver: attProxy, member: nullptr);
254
255 // bind item-specific layout properties:
256
257#define connectPropertyForwarding(property, Property) \
258 if (!attProxy->is##Property##Set()) { \
259 connect(attTarget, &QQuickLayoutAttached::property##Changed, this, &QQuickLayoutItemProxy::target##Property##Changed); \
260 connect(attProxy, &QQuickLayoutAttached::property##Changed, this, &QQuickLayoutItemProxy::proxy##Property##Changed); \
261 target##Property##Changed(); \
262 }
263 connectPropertyForwarding(minimumWidth, MinimumWidth)
264 connectPropertyForwarding(minimumHeight, MinimumHeight)
265 connectPropertyForwarding(preferredWidth, PreferredWidth)
266 connectPropertyForwarding(preferredHeight, PreferredHeight)
267 connectPropertyForwarding(maximumWidth, MaximumWidth)
268 connectPropertyForwarding(maximumHeight, MaximumHeight)
269 connectPropertyForwarding(fillWidth, FillWidth)
270 connectPropertyForwarding(fillHeight, FillHeight)
271 connectPropertyForwarding(alignment, Alignment)
272 connectPropertyForwarding(horizontalStretchFactor, HorizontalStretchFactor)
273 connectPropertyForwarding(verticalStretchFactor, VerticalStretchFactor)
274 connectPropertyForwarding(margins, Margins)
275 connectPropertyForwarding(leftMargin, LeftMargin)
276 connectPropertyForwarding(topMargin, TopMargin)
277 connectPropertyForwarding(rightMargin, RightMargin)
278 connectPropertyForwarding(bottomMargin, BottomMargin)
279#undef connectPropertyForwarding
280
281 // proxy.implicitWidth: target.implicitWidth
282 auto fnBindImplW = [newTarget, this](){ this->setImplicitWidth(newTarget->implicitWidth()); };
283 fnBindImplW();
284 connect(sender: newTarget, signal: &QQuickItem::implicitWidthChanged, slot&: fnBindImplW);
285
286 // proxy.implicitHeight: target.implicitHeight
287 auto fnBindImplH = [newTarget, this](){ this->setImplicitHeight(newTarget->implicitHeight()); };
288 fnBindImplH();
289 connect(sender: newTarget, signal: &QQuickItem::implicitHeightChanged, slot&: fnBindImplH);
290 }
291 }
292
293 if (isVisible())
294 maybeTakeControl();
295
296 emit targetChanged();
297}
298
299/*! \internal
300 \brief QQuickLayoutItemProxy::effectiveTarget
301 \return The target item of the proxy if it is in control, \c null otherwise.
302*/
303QQuickItem *QQuickLayoutItemProxy::effectiveTarget() const
304{
305 if (target() == nullptr)
306 return nullptr;
307
308 QQuickLayoutItemProxyAttachedData * attachedData = target()->property(name: "QQuickLayoutItemProxyAttachedData").value<QQuickLayoutItemProxyAttachedData*>();
309 return (attachedData->getControllingProxy() == this) ? target() : nullptr;
310}
311
312/*! \internal
313 \brief QQuickLayoutItemProxy::clearTarget sets the target to null.
314
315 This function is called if the target is destroyed to make sure we do not
316 try to access a non-existing object.
317*/
318void QQuickLayoutItemProxy::clearTarget()
319{
320 setTarget(nullptr);
321}
322
323/*! \internal
324 \brief QQuickLayoutItemProxy::maybeTakeControl checks and takes over control
325 of the item.
326
327 If the proxy is visible it will try to take control over the target and set
328 its visibility to true. If the proxy is hidden it will also hide the target
329 and another LayoutItemProxy has to set the visibility to \c true or the
330 target will stay invisible.
331*/
332void QQuickLayoutItemProxy::maybeTakeControl()
333{
334 Q_D(QQuickLayoutItemProxy);
335 if (!d->target)
336 return;
337
338 QQuickLayoutItemProxyAttachedData * attachedData = d->target->property(name: "QQuickLayoutItemProxyAttachedData").value<QQuickLayoutItemProxyAttachedData*>();
339 if (isVisible() && attachedData->getControllingProxy() != this) {
340 if (attachedData->takeControl(proxy: this)) {
341 d->target->setVisible(true);
342 d->target->setParentItem(this);
343 updatePos();
344 }
345 }
346 if (!isVisible() && attachedData->getControllingProxy() == this){
347 if (d->target->parentItem() == this) {
348 d->target->setParentItem(nullptr);
349 } else
350 qCDebug(lcLayouts) << "Parent was changed to" << d->target->parentItem() << "while an ItemProxy had control";
351 d->target->setVisible(false);
352 attachedData->releaseControl(proxy: this);
353 }
354}
355
356/*! \internal
357 \brief QQuickLayoutItemProxy::updatePos sets the geometry of the target to
358 the geometry of the proxy
359*/
360void QQuickLayoutItemProxy::updatePos()
361{
362 if (!isVisible())
363 return;
364 if (target()) {
365 if (QQuickLayoutItemProxyAttachedData * attachedData = target()->property(name: "QQuickLayoutItemProxyAttachedData").value<QQuickLayoutItemProxyAttachedData*>()) {
366 if (attachedData->getControllingProxy() == this)
367 geometryChange(newGeom: boundingRect(), oldGeom: boundingRect());
368 }
369 }
370}
371
372QQuickLayoutItemProxyPrivate::QQuickLayoutItemProxyPrivate()
373 : QQuickItemPrivate(),
374 m_expectProxyMinimumWidthChange(false),
375 m_expectProxyMinimumHeightChange(false),
376 m_expectProxyPreferredWidthChange(false),
377 m_expectProxyPreferredHeightChange(false),
378 m_expectProxyMaximumWidthChange(false),
379 m_expectProxyMaximumHeightChange(false),
380 m_expectProxyFillWidthChange(false),
381 m_expectProxyFillHeightChange(false),
382 m_expectProxyAlignmentChange(false),
383 m_expectProxyHorizontalStretchFactorChange(false),
384 m_expectProxyVerticalStretchFactorChange(false),
385 m_expectProxyMarginsChange(false),
386 m_expectProxyLeftMarginChange(false),
387 m_expectProxyTopMarginChange(false),
388 m_expectProxyRightMarginChange(false),
389 m_expectProxyBottomMarginChange(false)
390{
391
392}
393
394/*! \internal
395 \class QQuickLayoutItemProxyAttachedData
396 \brief Provides attached properties for items that are managed by one or
397 more LayoutItemProxy.
398
399 It stores all proxies that target the item, and will emit signals when the
400 proxies or the controlling proxy changes. Proxies can listen to the signal
401 and pick up control if they wish to.
402*/
403QQuickLayoutItemProxyAttachedData::QQuickLayoutItemProxyAttachedData(QObject *parent)
404 : QObject(parent), controllingProxy(nullptr)
405{
406
407}
408
409QQuickLayoutItemProxyAttachedData::~QQuickLayoutItemProxyAttachedData()
410{
411 // If this is destroyed, so is the target. Clear the target from the
412 // proxies so they do not try to access a destroyed object
413 for (auto &proxy: std::as_const(t&: proxies))
414 proxy->clearTarget();
415}
416
417/*! \internal
418 \brief QQuickLayoutItemProxyAttachedData::registerProxy registers a proxy
419 that manages the item this data is attached to.
420
421 This is required to easily notify proxies when the target is destroyed or
422 when it is free to take over control.
423*/
424void QQuickLayoutItemProxyAttachedData::registerProxy(QQuickLayoutItemProxy *proxy)
425{
426 if (proxies.contains(t: proxy))
427 return;
428
429 proxies.append(t: proxy);
430 emit proxiesChanged();
431}
432
433/*! \internal
434 \brief QQuickLayoutItemProxyAttachedData::releaseProxy removes a proxy from
435 a list of known proxies that manage the item this data is attached to.
436*/
437void QQuickLayoutItemProxyAttachedData::releaseProxy(QQuickLayoutItemProxy *proxy)
438{
439 if (proxy == controllingProxy)
440 releaseControl(proxy);
441
442 proxies.removeAll(t: proxy);
443
444 if (proxies.isEmpty())
445 deleteLater();
446
447 emit proxiesChanged();
448}
449
450/*! \internal
451 \brief QQuickLayoutItemProxyAttachedData::takeControl is called by
452 LayoutItemProxies when they try to take control over the item this data is
453 attached to.
454 \return \c true if no other proxy controls the item and if control is
455 granted to the proxy, \c false otherwise.
456
457 \param proxy The proxy that tries to take control.
458*/
459bool QQuickLayoutItemProxyAttachedData::takeControl(QQuickLayoutItemProxy *proxy)
460{
461 if (controllingProxy || !proxies.contains(t: proxy))
462 return false;
463
464 qCDebug(lcLayouts) << proxy
465 << "takes control of"
466 << parent();
467
468 controllingProxy = proxy;
469 emit controlTaken();
470 emit controllingProxyChanged();
471 return true;
472}
473
474/*! \internal
475 \brief QQuickLayoutItemProxyAttachedData::releaseControl is called by
476 LayoutItemProxies when they try no longer control the item
477
478 \param proxy The proxy that gives up control.
479*/
480void QQuickLayoutItemProxyAttachedData::releaseControl(QQuickLayoutItemProxy *proxy)
481{
482 if (controllingProxy != proxy)
483 return;
484
485 qCDebug(lcLayouts) << proxy
486 << "no longer controls"
487 << parent();
488
489 controllingProxy = nullptr;
490 emit controlReleased();
491 emit controllingProxyChanged();
492
493 for (auto &otherProxy: std::as_const(t&: proxies)) {
494 if (proxy != otherProxy)
495 otherProxy->maybeTakeControl();
496 }
497}
498
499/*! \internal
500 \brief QQuickLayoutItemProxyAttachedData::getControllingProxy
501 \return the proxy that currently controls the item this data is attached to.
502 Returns \c null if no proxy controls the item.
503*/
504QQuickLayoutItemProxy *QQuickLayoutItemProxyAttachedData::getControllingProxy() const
505{
506 return controllingProxy;
507}
508
509/*! \internal
510 \brief QQuickLayoutItemProxyAttachedData::getProxies
511 \return a list of all proxies that target the item this data is attached to.
512*/
513QQmlListProperty<QQuickLayoutItemProxy> QQuickLayoutItemProxyAttachedData::getProxies()
514{
515 using Type = QQuickLayoutItemProxy;
516 using Property = QQmlListProperty<Type>;
517
518 return Property(
519 this, &proxies,
520 [](Property *p) { return static_cast<QList<Type *> *>(p->data)->size(); },
521 [](Property *p, qsizetype i) { return static_cast<QList<Type *> *>(p->data)->at(i); }
522 );
523}
524
525/*! \internal
526 \brief QQuickLayoutItemProxyAttachedData::proxyHasControl
527 \return \c true if a proxy is controlling the item, \c false otherwise.
528*/
529bool QQuickLayoutItemProxyAttachedData::proxyHasControl() const
530{
531 return controllingProxy != nullptr;
532}
533

Provided by KDAB

Privacy Policy
Start learning QML with our Intro Training
Find out more

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