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 | |
79 | Q_LOGGING_CATEGORY(lcLayouts, "qt.quick.layouts" ) |
80 | |
81 | |
82 | QQuickLayoutItemProxy::QQuickLayoutItemProxy(QQuickItem *parent) |
83 | : QQuickItem(*new QQuickLayoutItemProxyPrivate, parent) |
84 | { |
85 | |
86 | } |
87 | |
88 | QQuickLayoutItemProxy::~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 | */ |
112 | void 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 | */ |
144 | void 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 | |
181 | propertyForwarding(minimumWidth, MinimumWidth) |
182 | propertyForwarding(minimumHeight, MinimumHeight) |
183 | propertyForwarding(preferredWidth, PreferredWidth) |
184 | propertyForwarding(preferredHeight, PreferredHeight) |
185 | propertyForwarding(maximumWidth, MaximumWidth) |
186 | propertyForwarding(maximumHeight, MaximumHeight) |
187 | propertyForwarding(fillWidth, FillWidth) |
188 | propertyForwarding(fillHeight, FillHeight) |
189 | propertyForwarding(alignment, Alignment) |
190 | propertyForwarding(horizontalStretchFactor, HorizontalStretchFactor) |
191 | propertyForwarding(verticalStretchFactor, VerticalStretchFactor) |
192 | propertyForwarding(margins, Margins) |
193 | propertyForwarding(leftMargin, LeftMargin) |
194 | propertyForwarding(topMargin, TopMargin) |
195 | propertyForwarding(rightMargin, RightMargin) |
196 | propertyForwarding(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 | */ |
211 | QQuickItem *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 | */ |
225 | void 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 | */ |
306 | QQuickItem *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 | */ |
321 | void 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 | */ |
335 | void 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 | */ |
363 | void 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 | |
375 | QQuickLayoutItemProxyPrivate::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 | */ |
406 | QQuickLayoutItemProxyAttachedData::QQuickLayoutItemProxyAttachedData(QObject *parent) |
407 | : QObject(parent), controllingProxy(nullptr) |
408 | { |
409 | |
410 | } |
411 | |
412 | QQuickLayoutItemProxyAttachedData::~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 | */ |
427 | void 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 | */ |
440 | void 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 | */ |
462 | bool 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 | */ |
483 | void 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 | */ |
507 | QQuickLayoutItemProxy *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 | */ |
516 | const 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 | */ |
525 | bool QQuickLayoutItemProxyAttachedData::proxyHasControl() const |
526 | { |
527 | return controllingProxy != nullptr; |
528 | } |
529 | |