1// Copyright (C) 2016 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
4#include "qquickstacklayout_p.h"
5
6#include <limits>
7
8#include <QtQml/qqmlinfo.h>
9
10/*!
11 \qmltype StackLayout
12 //! \instantiates QQuickStackLayout
13 \inherits Item
14 \inqmlmodule QtQuick.Layouts
15 \ingroup layouts
16 \brief The StackLayout class provides a stack of items where
17 only one item is visible at a time.
18
19 To be able to use this type more efficiently, it is recommended that you
20 understand the general mechanism of the Qt Quick Layouts module. Refer to
21 \l{Qt Quick Layouts Overview} for more information.
22
23 The current visible item can be modified by setting the \l currentIndex property.
24 The index corresponds to the order of the StackLayout's children.
25
26 In contrast to most other layouts, child Items' \l{Layout::fillWidth}{Layout.fillWidth} and \l{Layout::fillHeight}{Layout.fillHeight} properties
27 default to \c true. As a consequence, child items are by default filled to match the size of the StackLayout as long as their
28 \l{Layout::maximumWidth}{Layout.maximumWidth} or \l{Layout::maximumHeight}{Layout.maximumHeight} does not prevent it.
29
30 Items are added to the layout by reparenting the item to the layout. Similarly, removal is done by reparenting the item from the layout.
31 Both of these operations will affect the layout's \l count property.
32
33 The following code will create a StackLayout where only the 'plum' rectangle is visible.
34 \code
35 StackLayout {
36 id: layout
37 anchors.fill: parent
38 currentIndex: 1
39 Rectangle {
40 color: 'teal'
41 implicitWidth: 200
42 implicitHeight: 200
43 }
44 Rectangle {
45 color: 'plum'
46 implicitWidth: 300
47 implicitHeight: 200
48 }
49 }
50 \endcode
51
52 Items in a StackLayout support these attached properties:
53 \list
54 \li \l{Layout::minimumWidth}{Layout.minimumWidth}
55 \li \l{Layout::minimumHeight}{Layout.minimumHeight}
56 \li \l{Layout::preferredWidth}{Layout.preferredWidth}
57 \li \l{Layout::preferredHeight}{Layout.preferredHeight}
58 \li \l{Layout::maximumWidth}{Layout.maximumWidth}
59 \li \l{Layout::maximumHeight}{Layout.maximumHeight}
60 \li \l{Layout::fillWidth}{Layout.fillWidth}
61 \li \l{Layout::fillHeight}{Layout.fillHeight}
62 \endlist
63
64 Read more about attached properties \l{QML Object Attributes}{here}.
65 \sa ColumnLayout
66 \sa GridLayout
67 \sa RowLayout
68 \sa {QtQuick.Controls::StackView}{StackView}
69 \sa {Qt Quick Layouts Overview}
70*/
71
72QT_BEGIN_NAMESPACE
73
74static QQuickStackLayoutAttached *attachedStackLayoutObject(QQuickItem *item, bool create = false)
75{
76 return static_cast<QQuickStackLayoutAttached*>(
77 qmlAttachedPropertiesObject<QQuickStackLayout>(obj: item, create));
78}
79
80QQuickStackLayout::QQuickStackLayout(QQuickItem *parent) :
81 QQuickLayout(*new QQuickStackLayoutPrivate, parent)
82{
83}
84
85/*!
86 \qmlproperty int StackLayout::count
87
88 This property holds the number of items that belong to the layout.
89
90 Only items that are children of the StackLayout will be candidates for layouting.
91*/
92int QQuickStackLayout::count() const
93{
94 Q_D(const QQuickStackLayout);
95 return d->count;
96}
97
98/*!
99 \qmlproperty int StackLayout::currentIndex
100
101 This property holds the index of the child item that is currently visible in the StackLayout.
102 By default it will be \c -1 for an empty layout, otherwise the default is \c 0 (referring to the first item).
103
104 Since 6.5, inserting/removing a new Item at an index less than or equal to the current index
105 will increment/decrement the current index, but keep the current Item.
106*/
107int QQuickStackLayout::currentIndex() const
108{
109 Q_D(const QQuickStackLayout);
110 return d->currentIndex;
111}
112
113void QQuickStackLayout::setCurrentIndex(int index)
114{
115 Q_D(QQuickStackLayout);
116 if (index == d->currentIndex)
117 return;
118
119 QQuickItem *prev = itemAt(index: d->currentIndex);
120 QQuickItem *next = itemAt(index);
121 d->currentIndex = index;
122 d->explicitCurrentIndex = true;
123 if (prev)
124 prev->setVisible(false);
125 if (next)
126 next->setVisible(true);
127
128 if (isComponentComplete()) {
129 rearrange(QSizeF(width(), height()));
130 emit currentIndexChanged();
131 }
132
133 // Update attached properties after emitting currentIndexChanged()
134 // to maintain a more sensible emission order.
135 if (prev) {
136 auto stackLayoutAttached = attachedStackLayoutObject(item: prev);
137 if (stackLayoutAttached)
138 stackLayoutAttached->setIsCurrentItem(false);
139 }
140 if (next) {
141 auto stackLayoutAttached = attachedStackLayoutObject(item: next);
142 if (stackLayoutAttached)
143 stackLayoutAttached->setIsCurrentItem(true);
144 }
145}
146
147void QQuickStackLayout::componentComplete()
148{
149 QQuickLayout::componentComplete(); // will call our geometryChange(), (where isComponentComplete() == true)
150
151 childItemsChanged();
152 invalidate();
153 ensureLayoutItemsUpdated(options: ApplySizeHints);
154
155 QQuickItem *par = parentItem();
156 if (qobject_cast<QQuickLayout*>(object: par))
157 return;
158
159 rearrange(QSizeF(width(), height()));
160}
161
162void QQuickStackLayout::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value)
163{
164 QQuickLayout::itemChange(change, value);
165 if (!isReady())
166 return;
167
168 if (change == ItemChildRemovedChange) {
169 QQuickItem *item = value.item;
170 auto stackLayoutAttached = attachedStackLayoutObject(item);
171 if (stackLayoutAttached) {
172 stackLayoutAttached->setLayout(nullptr);
173 stackLayoutAttached->setIndex(-1);
174 stackLayoutAttached->setIsCurrentItem(false);
175 }
176 m_cachedItemSizeHints.remove(key: item);
177 childItemsChanged(adjustCurrentIndexPolicy: AdjustCurrentIndex); // removal; might have to adjust currentIndex
178 invalidate();
179 } else if (change == ItemChildAddedChange) {
180 childItemsChanged();
181 invalidate();
182 }
183}
184
185QSizeF QQuickStackLayout::sizeHint(Qt::SizeHint whichSizeHint) const
186{
187 Q_D(const QQuickStackLayout);
188 QSizeF &askingFor = m_cachedSizeHints[whichSizeHint];
189 if (!askingFor.isValid()) {
190 QSizeF &minS = m_cachedSizeHints[Qt::MinimumSize];
191 QSizeF &prefS = m_cachedSizeHints[Qt::PreferredSize];
192 QSizeF &maxS = m_cachedSizeHints[Qt::MaximumSize];
193
194 minS = QSizeF(0,0);
195 prefS = QSizeF(0,0);
196 maxS = QSizeF(std::numeric_limits<qreal>::infinity(), std::numeric_limits<qreal>::infinity());
197
198 const int count = itemCount();
199 for (int i = 0; i < count; ++i) {
200 SizeHints &hints = cachedItemSizeHints(index: i);
201 minS = minS.expandedTo(otherSize: hints.min());
202 prefS = prefS.expandedTo(otherSize: hints.pref());
203 //maxS = maxS.boundedTo(hints.max()); // Can be resized to be larger than any of its items.
204 // This is the same as QStackLayout does it.
205 // Not sure how descent makes sense here...
206 }
207 }
208 d->m_dirty = false;
209 return askingFor;
210}
211
212int QQuickStackLayout::indexOf(QQuickItem *childItem) const
213{
214 if (childItem) {
215 int indexOfItem = 0;
216 const auto items = childItems();
217 for (QQuickItem *item : items) {
218 if (shouldIgnoreItem(item))
219 continue;
220 if (childItem == item)
221 return indexOfItem;
222 ++indexOfItem;
223 }
224 }
225 return -1;
226}
227
228QQuickStackLayoutAttached *QQuickStackLayout::qmlAttachedProperties(QObject *object)
229{
230 return new QQuickStackLayoutAttached(object);
231}
232
233QQuickItem *QQuickStackLayout::itemAt(int index) const
234{
235 const auto items = childItems();
236 for (QQuickItem *item : items) {
237 if (shouldIgnoreItem(item))
238 continue;
239 if (index == 0)
240 return item;
241 --index;
242 }
243 return nullptr;
244}
245
246int QQuickStackLayout::itemCount() const
247{
248 int count = 0;
249 const auto items = childItems();
250 for (QQuickItem *item : items) {
251 if (shouldIgnoreItem(item))
252 continue;
253 ++count;
254 }
255 return count;
256}
257
258void QQuickStackLayout::setAlignment(QQuickItem * /*item*/, Qt::Alignment /*align*/)
259{
260 // ### Do we have to respect alignment?
261}
262
263void QQuickStackLayout::invalidate(QQuickItem *childItem)
264{
265 if (childItem) {
266 SizeHints &hints = m_cachedItemSizeHints[childItem];
267 hints.min() = QSizeF();
268 hints.pref() = QSizeF();
269 hints.max() = QSizeF();
270 }
271
272 for (int i = 0; i < Qt::NSizeHints; ++i)
273 m_cachedSizeHints[i] = QSizeF();
274 QQuickLayout::invalidate(childItem: this);
275
276 if (QQuickLayout *parentLayout = qobject_cast<QQuickLayout *>(object: parentItem()))
277 parentLayout->invalidate(childItem: this);
278}
279
280void QQuickStackLayout::childItemsChanged(AdjustCurrentIndexPolicy adjustCurrentIndexPolicy)
281{
282 Q_D(QQuickStackLayout);
283 const int count = itemCount();
284 const int oldIndex = d->currentIndex;
285 if (!d->explicitCurrentIndex)
286 d->currentIndex = (count > 0 ? 0 : -1);
287
288 if (adjustCurrentIndexPolicy == AdjustCurrentIndex) {
289 /*
290 * If an item is inserted or deleted at an index less than or equal to the current index it
291 * will affect the current index, but keep the current item. This is consistent with
292 * QStackedLayout, QStackedWidget and TabBar
293 *
294 * Unless the caller is componentComplete(), we can assume that only one of the children
295 * are visible, and we should keep that visible even if the stacking order has changed.
296 * This means that if the sibling order has changed (or an item stacked before the current
297 * item is added/removed), we must update the currentIndex so that it corresponds with the
298 * current visible item.
299 */
300 if (d->currentIndex < d->count) {
301 for (int i = 0; i < count; ++i) {
302 QQuickItem *child = itemAt(index: i);
303 if (child->isVisible()) {
304 d->currentIndex = i;
305 break;
306 }
307 }
308 }
309 }
310 if (d->currentIndex != oldIndex)
311 emit currentIndexChanged();
312
313 if (count != d->count) {
314 d->count = count;
315 emit countChanged();
316 }
317 for (int i = 0; i < count; ++i) {
318 QQuickItem *child = itemAt(index: i);
319 checkAnchors(item: child);
320 child->setVisible(d->currentIndex == i);
321
322 auto stackLayoutAttached = attachedStackLayoutObject(item: child);
323 if (stackLayoutAttached) {
324 stackLayoutAttached->setLayout(this);
325 stackLayoutAttached->setIndex(i);
326 stackLayoutAttached->setIsCurrentItem(d->currentIndex == i);
327 }
328 }
329}
330
331QQuickStackLayout::SizeHints &QQuickStackLayout::cachedItemSizeHints(int index) const
332{
333 QQuickItem *item = itemAt(index);
334 Q_ASSERT(item);
335 SizeHints &hints = m_cachedItemSizeHints[item]; // will create an entry if it doesn't exist
336 if (!hints.min().isValid())
337 QQuickStackLayout::collectItemSizeHints(item, sizeHints: hints.array);
338 return hints;
339}
340
341
342void QQuickStackLayout::rearrange(const QSizeF &newSize)
343{
344 Q_D(QQuickStackLayout);
345 if (newSize.isNull() || !newSize.isValid())
346 return;
347
348 qCDebug(lcQuickLayouts) << "QQuickStackLayout::rearrange";
349
350 if (d->currentIndex == -1 || d->currentIndex >= m_cachedItemSizeHints.size())
351 return;
352 QQuickStackLayout::SizeHints &hints = cachedItemSizeHints(index: d->currentIndex);
353 QQuickItem *item = itemAt(index: d->currentIndex);
354 Q_ASSERT(item);
355 item->setPosition(QPointF(0,0)); // ### respect alignment?
356 const QSizeF oldSize(item->width(), item->height());
357 const QSizeF effectiveNewSize = newSize.expandedTo(otherSize: hints.min()).boundedTo(otherSize: hints.max());
358 item->setSize(effectiveNewSize);
359 if (effectiveNewSize == oldSize)
360 item->polish();
361 QQuickLayout::rearrange(newSize);
362}
363
364void QQuickStackLayout::setStretchFactor(QQuickItem * /*item*/, int /*stretchFactor*/, Qt::Orientation /*orient*/)
365{
366}
367
368void QQuickStackLayout::collectItemSizeHints(QQuickItem *item, QSizeF *sizeHints)
369{
370 QQuickLayoutAttached *info = nullptr;
371 QQuickLayout::effectiveSizeHints_helper(item, cachedSizeHints: sizeHints, info: &info, useFallbackToWidthOrHeight: true);
372 if (!info)
373 return;
374 if (info->isFillWidthSet() && !info->fillWidth()) {
375 const qreal pref = sizeHints[Qt::PreferredSize].width();
376 sizeHints[Qt::MinimumSize].setWidth(pref);
377 sizeHints[Qt::MaximumSize].setWidth(pref);
378 }
379
380 if (info->isFillHeightSet() && !info->fillHeight()) {
381 const qreal pref = sizeHints[Qt::PreferredSize].height();
382 sizeHints[Qt::MinimumSize].setHeight(pref);
383 sizeHints[Qt::MaximumSize].setHeight(pref);
384 }
385}
386
387bool QQuickStackLayout::shouldIgnoreItem(QQuickItem *item) const
388{
389 return QQuickItemPrivate::get(item)->isTransparentForPositioner();
390}
391
392void QQuickStackLayout::itemSiblingOrderChanged(QQuickItem *)
393{
394 if (!isReady())
395 return;
396 childItemsChanged(adjustCurrentIndexPolicy: AdjustCurrentIndex);
397 invalidate();
398}
399
400QQuickStackLayoutAttached::QQuickStackLayoutAttached(QObject *object)
401{
402 auto item = qobject_cast<QQuickItem*>(o: object);
403 if (!item) {
404 qmlWarning(me: object) << "StackLayout must be attached to an Item";
405 return;
406 }
407
408 auto stackLayout = qobject_cast<QQuickStackLayout*>(object: item->parentItem());
409 if (!stackLayout) {
410 // It might not be a child of a StackLayout yet, and that's OK.
411 // The index will get set by updateLayoutItems() when it's reparented.
412 return;
413 }
414
415 if (!stackLayout->isComponentComplete()) {
416 // Don't try to get the index if the StackLayout itself hasn't loaded yet.
417 return;
418 }
419
420 // If we got this far, the item was added as a child to the StackLayout after it loaded.
421 const int index = stackLayout->indexOf(childItem: item);
422 setLayout(stackLayout);
423 setIndex(index);
424 setIsCurrentItem(stackLayout->currentIndex() == index);
425
426 // In case of lazy loading in loader, attachedProperties are created and updated for the
427 // object after adding the child object to the stack layout, which leads to entries with
428 // same index. Triggering childItemsChanged() resets to right index in the stack layout.
429 stackLayout->childItemsChanged();
430}
431
432/*!
433 \qmlattachedproperty int StackLayout::index
434
435 This attached property holds the index of each child item in the
436 \l StackLayout.
437
438 \sa isCurrentItem, layout
439
440 \since QtQuick.Layouts 1.15
441*/
442int QQuickStackLayoutAttached::index() const
443{
444 return m_index;
445}
446
447void QQuickStackLayoutAttached::setIndex(int index)
448{
449 if (index == m_index)
450 return;
451
452 m_index = index;
453 emit indexChanged();
454}
455
456/*!
457 \qmlattachedproperty bool StackLayout::isCurrentItem
458
459 This attached property is \c true if this child is the current item
460 in the \l StackLayout.
461
462 \sa index, layout
463
464 \since QtQuick.Layouts 1.15
465*/
466bool QQuickStackLayoutAttached::isCurrentItem() const
467{
468 return m_isCurrentItem;
469}
470
471void QQuickStackLayoutAttached::setIsCurrentItem(bool isCurrentItem)
472{
473 if (isCurrentItem == m_isCurrentItem)
474 return;
475
476 m_isCurrentItem = isCurrentItem;
477 emit isCurrentItemChanged();
478}
479
480/*!
481 \qmlattachedproperty StackLayout StackLayout::layout
482
483 This attached property holds the \l StackLayout that manages this child
484 item.
485
486 \sa index, isCurrentItem
487
488 \since QtQuick.Layouts 1.15
489*/
490QQuickStackLayout *QQuickStackLayoutAttached::layout() const
491{
492 return m_layout;
493}
494
495void QQuickStackLayoutAttached::setLayout(QQuickStackLayout *layout)
496{
497 if (layout == m_layout)
498 return;
499
500 m_layout = layout;
501 emit layoutChanged();
502}
503
504QT_END_NAMESPACE
505
506#include "moc_qquickstacklayout_p.cpp"
507

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