| 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 | |