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 | |
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 | \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 | */ |
93 | int 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 | */ |
108 | int QQuickStackLayout::currentIndex() const |
109 | { |
110 | Q_D(const QQuickStackLayout); |
111 | return d->currentIndex; |
112 | } |
113 | |
114 | void 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 | |
148 | void 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 | |
163 | void 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 | |
186 | QSizeF 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 | |
213 | int 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 | |
229 | QQuickStackLayoutAttached *QQuickStackLayout::qmlAttachedProperties(QObject *object) |
230 | { |
231 | return new QQuickStackLayoutAttached(object); |
232 | } |
233 | |
234 | QQuickItem *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 | |
247 | int 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 | |
259 | void QQuickStackLayout::setAlignment(QQuickItem * /*item*/, Qt::Alignment /*align*/) |
260 | { |
261 | // ### Do we have to respect alignment? |
262 | } |
263 | |
264 | void 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 | |
281 | void 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 | |
332 | QQuickStackLayout::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 | |
343 | void 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 | |
365 | void QQuickStackLayout::setStretchFactor(QQuickItem * /*item*/, int /*stretchFactor*/, Qt::Orientation /*orient*/) |
366 | { |
367 | } |
368 | |
369 | void 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 | |
388 | bool QQuickStackLayout::shouldIgnoreItem(QQuickItem *item) const |
389 | { |
390 | return QQuickItemPrivate::get(item)->isTransparentForPositioner(); |
391 | } |
392 | |
393 | void QQuickStackLayout::itemSiblingOrderChanged(QQuickItem *) |
394 | { |
395 | if (!isReady()) |
396 | return; |
397 | childItemsChanged(adjustCurrentIndexPolicy: AdjustCurrentIndex); |
398 | invalidate(); |
399 | } |
400 | |
401 | QQuickStackLayoutAttached::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 | */ |
444 | int QQuickStackLayoutAttached::index() const |
445 | { |
446 | return m_index; |
447 | } |
448 | |
449 | void 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 | */ |
469 | bool QQuickStackLayoutAttached::isCurrentItem() const |
470 | { |
471 | return m_isCurrentItem; |
472 | } |
473 | |
474 | void 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 | */ |
494 | QQuickStackLayout *QQuickStackLayoutAttached::layout() const |
495 | { |
496 | return m_layout; |
497 | } |
498 | |
499 | void QQuickStackLayoutAttached::setLayout(QQuickStackLayout *layout) |
500 | { |
501 | if (layout == m_layout) |
502 | return; |
503 | |
504 | m_layout = layout; |
505 | emit layoutChanged(); |
506 | } |
507 | |
508 | QT_END_NAMESPACE |
509 | |
510 | #include "moc_qquickstacklayout_p.cpp" |
511 |
Definitions
- attachedStackLayoutObject
- QQuickStackLayout
- count
- currentIndex
- setCurrentIndex
- componentComplete
- itemChange
- sizeHint
- indexOf
- qmlAttachedProperties
- itemAt
- itemCount
- setAlignment
- invalidate
- childItemsChanged
- cachedItemSizeHints
- rearrange
- setStretchFactor
- collectItemSizeHints
- shouldIgnoreItem
- itemSiblingOrderChanged
- QQuickStackLayoutAttached
- index
- setIndex
- isCurrentItem
- setIsCurrentItem
- layout
Learn to use CMake with our Intro Training
Find out more