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

Provided by KDAB

Privacy Policy
Learn to use CMake with our Intro Training
Find out more

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