diff options
| author | Mitch Curtis <mitch.curtis@qt.io> | 2019-12-09 15:13:33 +0100 |
|---|---|---|
| committer | Mitch Curtis <mitch.curtis@qt.io> | 2020-03-23 14:01:16 +0100 |
| commit | 619dfbe8d02347cedb93a949cde627ec4424e0ae (patch) | |
| tree | 670edc69d8c746fc5c3d73a7eccc0a84a7ea27d4 | |
| parent | b9c7f8989b979374ab0528827d69050415a424e0 (diff) | |
StackLayout: add attached index, isCurrentItem, and layout properties
These properties are useful for items within StackLayout to get access
to their index within it, especially when those items are
declared in their own QML files. Similar API already exists for e.g.
ListView and SwipeView.
[ChangeLog][StackLayout] Added attached index, isCurrentItem,
and layout properties.
Change-Id: I648d4434ab21573b56edd9a0f8399463946fd571
Fixes: QTBUG-76999
Reviewed-by: Volker Hilsheimer <volker.hilsheimer@qt.io>
Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
| -rw-r--r-- | src/imports/layouts/qquickstacklayout.cpp | 177 | ||||
| -rw-r--r-- | src/imports/layouts/qquickstacklayout_p.h | 37 | ||||
| -rw-r--r-- | tests/auto/quick/qquicklayouts/data/tst_stacklayout.qml | 175 |
3 files changed, 371 insertions, 18 deletions
diff --git a/src/imports/layouts/qquickstacklayout.cpp b/src/imports/layouts/qquickstacklayout.cpp index 4c1d611409..37fa4c999f 100644 --- a/src/imports/layouts/qquickstacklayout.cpp +++ b/src/imports/layouts/qquickstacklayout.cpp @@ -38,8 +38,11 @@ ****************************************************************************/ #include "qquickstacklayout_p.h" + #include <limits> +#include <QtQml/qqmlinfo.h> + /*! \qmltype StackLayout \instantiates QQuickStackLayout @@ -99,6 +102,12 @@ QT_BEGIN_NAMESPACE +static QQuickStackLayoutAttached *attachedStackLayoutObject(QQuickItem *item, bool create = false) +{ + return static_cast<QQuickStackLayoutAttached*>( + qmlAttachedPropertiesObject<QQuickStackLayout>(item, create)); +} + QQuickStackLayout::QQuickStackLayout(QQuickItem *parent) : QQuickLayout(*new QQuickStackLayoutPrivate, parent) { @@ -132,20 +141,34 @@ int QQuickStackLayout::currentIndex() const void QQuickStackLayout::setCurrentIndex(int index) { Q_D(QQuickStackLayout); - if (index != d->currentIndex) { - QQuickItem *prev = itemAt(d->currentIndex); - QQuickItem *next = itemAt(index); - d->currentIndex = index; - d->explicitCurrentIndex = true; - if (prev) - prev->setVisible(false); - if (next) - next->setVisible(true); - - if (isComponentComplete()) { - rearrange(QSizeF(width(), height())); - emit currentIndexChanged(); - } + if (index == d->currentIndex) + return; + + QQuickItem *prev = itemAt(d->currentIndex); + QQuickItem *next = itemAt(index); + d->currentIndex = index; + d->explicitCurrentIndex = true; + if (prev) + prev->setVisible(false); + if (next) + next->setVisible(true); + + if (isComponentComplete()) { + rearrange(QSizeF(width(), height())); + emit currentIndexChanged(); + } + + // Update attached properties after emitting currentIndexChanged() + // to maintain a more sensible emission order. + if (prev) { + auto stackLayoutAttached = attachedStackLayoutObject(prev); + if (stackLayoutAttached) + stackLayoutAttached->setIsCurrentItem(false); + } + if (next) { + auto stackLayoutAttached = attachedStackLayoutObject(next); + if (stackLayoutAttached) + stackLayoutAttached->setIsCurrentItem(true); } } @@ -162,6 +185,21 @@ void QQuickStackLayout::componentComplete() rearrange(QSizeF(width(), height())); } +void QQuickStackLayout::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &value) +{ + QQuickLayout::itemChange(change, value); + + if (change == ItemChildRemovedChange) { + QQuickItem *item = value.item; + auto stackLayoutAttached = attachedStackLayoutObject(item); + if (stackLayoutAttached) { + stackLayoutAttached->setLayout(nullptr); + stackLayoutAttached->setIndex(-1); + stackLayoutAttached->setIsCurrentItem(false); + } + } +} + QSizeF QQuickStackLayout::sizeHint(Qt::SizeHint whichSizeHint) const { QSizeF &askingFor = m_cachedSizeHints[whichSizeHint]; @@ -205,6 +243,11 @@ int QQuickStackLayout::indexOf(QQuickItem *childItem) const return -1; } +QQuickStackLayoutAttached *QQuickStackLayout::qmlAttachedProperties(QObject *object) +{ + return new QQuickStackLayoutAttached(object); +} + QQuickItem *QQuickStackLayout::itemAt(int index) const { const auto items = childItems(); @@ -294,6 +337,13 @@ void QQuickStackLayout::updateLayoutItems() QQuickItem *child = itemAt(i); checkAnchors(child); child->setVisible(d->currentIndex == i); + + auto stackLayoutAttached = attachedStackLayoutObject(child); + if (stackLayoutAttached) { + stackLayoutAttached->setLayout(this); + stackLayoutAttached->setIndex(i); + stackLayoutAttached->setIsCurrentItem(d->currentIndex == i); + } } invalidate(); @@ -347,6 +397,105 @@ bool QQuickStackLayout::shouldIgnoreItem(QQuickItem *item) const return ignored; } +QQuickStackLayoutAttached::QQuickStackLayoutAttached(QObject *object) +{ + auto item = qobject_cast<QQuickItem*>(object); + if (!item) { + qmlWarning(object) << "StackLayout must be attached to an Item"; + return; + } + + auto stackLayout = qobject_cast<QQuickStackLayout*>(item->parentItem()); + if (!stackLayout) { + // It might not be a child of a StackLayout yet, and that's OK. + // The index will get set by updateLayoutItems() when it's reparented. + return; + } + + if (!stackLayout->isComponentComplete()) { + // Don't try to get the index if the StackLayout itself hasn't loaded yet. + return; + } + + // If we got this far, the item was added as a child to the StackLayout after it loaded. + const int index = stackLayout->indexOf(item); + setLayout(stackLayout); + setIndex(index); + setIsCurrentItem(stackLayout->currentIndex() == index); +} + +/*! + \qmlattachedproperty int StackLayout::index + + This attached property holds the index of each child item in the + \l StackLayout. + + \sa isCurrentItem, layout + + \since QtQuick.Layouts 1.15 +*/ +int QQuickStackLayoutAttached::index() const +{ + return m_index; +} + +void QQuickStackLayoutAttached::setIndex(int index) +{ + if (index == m_index) + return; + + m_index = index; + emit indexChanged(); +} + +/*! + \qmlattachedproperty bool StackLayout::isCurrentItem + + This attached property is \c true if this child is the current item + in the \l StackLayout. + + \sa index, layout + + \since QtQuick.Layouts 1.15 +*/ +bool QQuickStackLayoutAttached::isCurrentItem() const +{ + return m_isCurrentItem; +} + +void QQuickStackLayoutAttached::setIsCurrentItem(bool isCurrentItem) +{ + if (isCurrentItem == m_isCurrentItem) + return; + + m_isCurrentItem = isCurrentItem; + emit isCurrentItemChanged(); +} + +/*! + \qmlattachedproperty StackLayout StackLayout::layout + + This attached property holds the \l StackLayout that manages this child + item. + + \sa index, isCurrentItem + + \since QtQuick.Layouts 1.15 +*/ +QQuickStackLayout *QQuickStackLayoutAttached::layout() const +{ + return m_layout; +} + +void QQuickStackLayoutAttached::setLayout(QQuickStackLayout *layout) +{ + if (layout == m_layout) + return; + + m_layout = layout; + emit layoutChanged(); +} + QT_END_NAMESPACE #include "moc_qquickstacklayout_p.cpp" diff --git a/src/imports/layouts/qquickstacklayout_p.h b/src/imports/layouts/qquickstacklayout_p.h index 07f9e48178..b641376ed8 100644 --- a/src/imports/layouts/qquickstacklayout_p.h +++ b/src/imports/layouts/qquickstacklayout_p.h @@ -45,6 +45,7 @@ QT_BEGIN_NAMESPACE class QQuickStackLayoutPrivate; +class QQuickStackLayoutAttached; class QQuickStackLayout : public QQuickLayout { @@ -53,6 +54,7 @@ class QQuickStackLayout : public QQuickLayout Q_PROPERTY(int currentIndex READ currentIndex WRITE setCurrentIndex NOTIFY currentIndexChanged) QML_NAMED_ELEMENT(StackLayout) QML_ADDED_IN_VERSION(1, 3) + QML_ATTACHED(QQuickStackLayoutAttached) public: explicit QQuickStackLayout(QQuickItem *parent = 0); @@ -61,6 +63,7 @@ public: void setCurrentIndex(int index); void componentComplete() override; + void itemChange(ItemChange change, const ItemChangeData &value) override; QSizeF sizeHint(Qt::SizeHint whichSizeHint) const override; void setAlignment(QQuickItem *item, Qt::Alignment align) override; void invalidate(QQuickItem *childItem = 0) override; @@ -72,7 +75,7 @@ public: int itemCount() const override; int indexOf(QQuickItem *item) const; - + static QQuickStackLayoutAttached *qmlAttachedProperties(QObject *object); signals: void currentIndexChanged(); @@ -85,6 +88,8 @@ private: bool shouldIgnoreItem(QQuickItem *item) const; Q_DECLARE_PRIVATE(QQuickStackLayout) + friend class QQuickStackLayoutAttached; + QList<QQuickItem*> m_items; typedef struct { @@ -109,6 +114,36 @@ private: bool explicitCurrentIndex; }; +class QQuickStackLayoutAttached : public QObject +{ + Q_OBJECT + Q_PROPERTY(int index READ index NOTIFY indexChanged) + Q_PROPERTY(bool isCurrentItem READ isCurrentItem NOTIFY isCurrentItemChanged) + Q_PROPERTY(QQuickStackLayout *layout READ layout NOTIFY layoutChanged) + +public: + QQuickStackLayoutAttached(QObject *object); + + int index() const; + void setIndex(int index); + + bool isCurrentItem() const; + void setIsCurrentItem(bool isCurrentItem); + + QQuickStackLayout *layout() const; + void setLayout(QQuickStackLayout *layout); + +signals: + void indexChanged(); + void isCurrentItemChanged(); + void layoutChanged(); + +private: + int m_index = -1; + bool m_isCurrentItem = false; + QQuickStackLayout *m_layout = nullptr; +}; + QT_END_NAMESPACE #endif // QQUICKSTACKLAYOUT_H diff --git a/tests/auto/quick/qquicklayouts/data/tst_stacklayout.qml b/tests/auto/quick/qquicklayouts/data/tst_stacklayout.qml index 8234ac6ef7..9c0b3f8635 100644 --- a/tests/auto/quick/qquicklayouts/data/tst_stacklayout.qml +++ b/tests/auto/quick/qquicklayouts/data/tst_stacklayout.qml @@ -48,9 +48,9 @@ ** ****************************************************************************/ -import QtQuick 2.2 -import QtTest 1.0 -import QtQuick.Layouts 1.3 +import QtQuick 2.15 +import QtTest 1.15 +import QtQuick.Layouts 1.15 Item { id: container @@ -94,6 +94,24 @@ Item { } } + Component { + id: stackLayoutComponent + + StackLayout {} + } + + Component { + id: itemComponent + + Item {} + } + + Component { + id: signalSpyComponent + + SignalSpy {} + } + function test_rearrange() { var layout = layout_rearrange_Component.createObject(container) @@ -103,5 +121,156 @@ Item { layout.destroy() } + + // Test that items added dynamically to the StackLayout + // have valid attached properties when accessed imperatively. + function test_attachedDynamicImperative() { + let layout = createTemporaryObject(stackLayoutComponent, container, { "anchors.fill": parent }) + verify(layout) + + let item1 = itemComponent.createObject(layout, { objectName: "item1" }) + verify(item1) + compare(item1.StackLayout.index, 0) + compare(item1.StackLayout.isCurrentItem, true) + compare(item1.StackLayout.layout, layout) + + let item2 = itemComponent.createObject(layout, { objectName: "item2" }) + verify(item2) + compare(item2.StackLayout.index, 1) + compare(item2.StackLayout.isCurrentItem, false) + compare(item2.StackLayout.layout, layout) + + // Test creating an item without a parent, accessing the attached properties, + // and _then_ add it to the StackLayout and check its attached properties again. + let item3 = itemComponent.createObject(null, { objectName: "item3" }) + verify(item3) + compare(item3.StackLayout.index, -1) + compare(item3.StackLayout.isCurrentItem, false) + compare(item3.StackLayout.layout, null) + + let signalSpy = signalSpyComponent.createObject(item3, + { target: item3.StackLayout, signalName: "indexChanged" }) + verify(signalSpy) + verify(signalSpy.valid) + + item3.parent = layout + compare(item3.StackLayout.index, 2) + compare(item3.StackLayout.isCurrentItem, false) + compare(item3.StackLayout.layout, layout) + compare(signalSpy.count, 1) + } + + Component { + id: attachedPropertiesItemComponent + + Item { + readonly property int index: StackLayout.index + readonly property bool isCurrentItem: StackLayout.isCurrentItem + readonly property StackLayout layout: StackLayout.layout + } + } + + // Test that items added dynamically to the StackLayout + // have valid attached properties when accessed declaratively. + function test_attachedDynamicDeclarative() { + let layout = createTemporaryObject(stackLayoutComponent, container, { "anchors.fill": parent }) + verify(layout) + + let item1 = attachedPropertiesItemComponent.createObject(layout, { objectName: "item1" }) + verify(item1) + compare(item1.index, 0) + compare(item1.isCurrentItem, true) + compare(item1.layout, layout) + + let item2 = attachedPropertiesItemComponent.createObject(layout, { objectName: "item2" }) + verify(item2) + compare(item2.index, 1) + compare(item2.isCurrentItem, false) + compare(item2.layout, layout) + } + + Component { + id: attachedStackLayoutComponent + + StackLayout { + anchors.fill: parent + + property alias item1: item1 + property alias item2: item2 + + Item { + id: item1 + readonly property int index: StackLayout.index + readonly property bool isCurrentItem: StackLayout.isCurrentItem + readonly property StackLayout layout: StackLayout.layout + } + Item { + id: item2 + readonly property int index: StackLayout.index + readonly property bool isCurrentItem: StackLayout.isCurrentItem + readonly property StackLayout layout: StackLayout.layout + } + } + } + + // Test that items that are declared statically within StackLayout + // have valid attached properties when accessed declaratively. + function test_attachedStaticDeclarative() { + let layout = createTemporaryObject(attachedStackLayoutComponent, container) + verify(layout) + compare(layout.item1.index, 0) + compare(layout.item1.isCurrentItem, true) + compare(layout.item1.layout, layout) + compare(layout.item2.index, 1) + compare(layout.item2.isCurrentItem, false) + compare(layout.item2.layout, layout) + } + + // Tests attached properties after adding and removing items. + function test_attachedAddAndRemove() { + let layout = createTemporaryObject(attachedStackLayoutComponent, container) + verify(layout) + compare(layout.item1.index, 0) + compare(layout.item1.isCurrentItem, true) + compare(layout.item1.layout, layout) + compare(layout.item2.index, 1) + compare(layout.item2.isCurrentItem, false) + compare(layout.item2.layout, layout) + + // Remove item1. It's index should become -1. + layout.item1.parent = null + compare(layout.item1.index, -1) + compare(layout.item1.isCurrentItem, false) + compare(layout.item1.layout, null) + compare(layout.item2.index, 0) + compare(layout.item2.isCurrentItem, true) + compare(layout.item2.layout, layout) + + // Add it back. Since it's appended, and item2 took index 0, its index should now be 1. + layout.item1.parent = layout + compare(layout.item1.index, 1) + compare(layout.item1.isCurrentItem, false) + compare(layout.item1.layout, layout) + compare(layout.item2.index, 0) + compare(layout.item2.isCurrentItem, true) + compare(layout.item2.layout, layout) + + // Do the same with item2. + layout.item2.parent = null + compare(layout.item1.index, 0) + compare(layout.item1.isCurrentItem, true) + compare(layout.item1.layout, layout) + compare(layout.item2.index, -1) + compare(layout.item2.isCurrentItem, false) + compare(layout.item2.layout, null) + + layout.item2.parent = layout + compare(layout.item1.index, 0) + compare(layout.item1.isCurrentItem, true) + compare(layout.item1.layout, layout) + compare(layout.item2.index, 1) + compare(layout.item2.isCurrentItem, false) + compare(layout.item2.layout, layout) + } } } |
