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 | |
72 | QT_BEGIN_NAMESPACE |
73 | |
74 | static QQuickStackLayoutAttached *attachedStackLayoutObject(QQuickItem *item, bool create = false) |
75 | { |
76 | return static_cast<QQuickStackLayoutAttached*>( |
77 | qmlAttachedPropertiesObject<QQuickStackLayout>(obj: item, create)); |
78 | } |
79 | |
80 | QQuickStackLayout::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 | */ |
92 | int 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 | */ |
107 | int QQuickStackLayout::currentIndex() const |
108 | { |
109 | Q_D(const QQuickStackLayout); |
110 | return d->currentIndex; |
111 | } |
112 | |
113 | void 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 | |
147 | void 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 | |
162 | void 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 | |
185 | QSizeF 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 | |
212 | int 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 | |
228 | QQuickStackLayoutAttached *QQuickStackLayout::qmlAttachedProperties(QObject *object) |
229 | { |
230 | return new QQuickStackLayoutAttached(object); |
231 | } |
232 | |
233 | QQuickItem *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 | |
246 | int 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 | |
258 | void QQuickStackLayout::setAlignment(QQuickItem * /*item*/, Qt::Alignment /*align*/) |
259 | { |
260 | // ### Do we have to respect alignment? |
261 | } |
262 | |
263 | void 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 | |
280 | void 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 | |
331 | QQuickStackLayout::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 | |
342 | void 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 | |
364 | void QQuickStackLayout::setStretchFactor(QQuickItem * /*item*/, int /*stretchFactor*/, Qt::Orientation /*orient*/) |
365 | { |
366 | } |
367 | |
368 | void 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 | |
387 | bool QQuickStackLayout::shouldIgnoreItem(QQuickItem *item) const |
388 | { |
389 | return QQuickItemPrivate::get(item)->isTransparentForPositioner(); |
390 | } |
391 | |
392 | void QQuickStackLayout::itemSiblingOrderChanged(QQuickItem *) |
393 | { |
394 | if (!isReady()) |
395 | return; |
396 | childItemsChanged(adjustCurrentIndexPolicy: AdjustCurrentIndex); |
397 | invalidate(); |
398 | } |
399 | |
400 | QQuickStackLayoutAttached::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 | */ |
442 | int QQuickStackLayoutAttached::index() const |
443 | { |
444 | return m_index; |
445 | } |
446 | |
447 | void 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 | */ |
466 | bool QQuickStackLayoutAttached::isCurrentItem() const |
467 | { |
468 | return m_isCurrentItem; |
469 | } |
470 | |
471 | void 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 | */ |
490 | QQuickStackLayout *QQuickStackLayoutAttached::layout() const |
491 | { |
492 | return m_layout; |
493 | } |
494 | |
495 | void QQuickStackLayoutAttached::setLayout(QQuickStackLayout *layout) |
496 | { |
497 | if (layout == m_layout) |
498 | return; |
499 | |
500 | m_layout = layout; |
501 | emit layoutChanged(); |
502 | } |
503 | |
504 | QT_END_NAMESPACE |
505 | |
506 | #include "moc_qquickstacklayout_p.cpp" |
507 | |