From 7ecce2cc0e33bc6b1aa052163f9d6c1365b93b27 Mon Sep 17 00:00:00 2001 From: Martin Jones Date: Tue, 20 Sep 2011 15:58:05 +1000 Subject: [PATCH] Additional ListView section header placement options Add a section.labelPositioning property which can be a combination of: - ViewSection.InlineLabels - section labels are shown inline between the item delegates separating sections (default). - ViewSection.CurrentLabelAtStart - the current section label sticks to the start of the view as it is moved. - ViewSection.NextLabelAtEnd - the next section label (beyond all visible sections) sticks to the end of the view as it is moved. Task-number: QTBUG-12880 Change-Id: I4601828337412bd3a83769c9b8df3f6d4d7474b8 Reviewed-on: http://codereview.qt-project.org/5192 Reviewed-by: Qt Sanity Bot Reviewed-by: Bea Lam --- doc/src/declarative/whatsnew.qdoc | 4 + .../modelviews/listview/content/ToggleButton.qml | 18 + .../declarative/modelviews/listview/sections.qml | 54 ++- src/declarative/items/qsglistview.cpp | 512 ++++++++++++++------ src/declarative/items/qsglistview_p.h | 14 +- .../data/listview-sections_delegate.qml | 4 +- .../declarative/qsglistview/tst_qsglistview.cpp | 143 ++++++ 7 files changed, 591 insertions(+), 158 deletions(-) create mode 100644 examples/declarative/modelviews/listview/content/ToggleButton.qml diff --git a/doc/src/declarative/whatsnew.qdoc b/doc/src/declarative/whatsnew.qdoc index d4addf8..da00160 100644 --- a/doc/src/declarative/whatsnew.qdoc +++ b/doc/src/declarative/whatsnew.qdoc @@ -114,6 +114,10 @@ PathView now has a \c currentItem property ListView and GridView now have headerItem and footerItem properties (the instantiated header and footer items). +ListView section.labelPositioning property added to allow keeping the current section label +at the start and/or next section label at the end of the view. + + \section2 QtQuick 1 is now a separate library and module Writing C++ applications using QtQuick 1 specific API, i.e. QDeclarativeView or QDeclarativeItem diff --git a/examples/declarative/modelviews/listview/content/ToggleButton.qml b/examples/declarative/modelviews/listview/content/ToggleButton.qml new file mode 100644 index 0000000..2d8221e --- /dev/null +++ b/examples/declarative/modelviews/listview/content/ToggleButton.qml @@ -0,0 +1,18 @@ +import QtQuick 2.0 + +Rectangle { + id: root + property alias label: text.text + property bool active: false + signal toggled + width: 149 + height: 30 + radius: 3 + color: active ? "green" : "lightgray" + border.width: 1 + Text { id: text; anchors.centerIn: parent; font.pixelSize: 14 } + MouseArea { + anchors.fill: parent + onClicked: { active = !active; root.toggled() } + } +} diff --git a/examples/declarative/modelviews/listview/sections.qml b/examples/declarative/modelviews/listview/sections.qml index 09e58be..4496567 100644 --- a/examples/declarative/modelviews/listview/sections.qml +++ b/examples/declarative/modelviews/listview/sections.qml @@ -42,22 +42,33 @@ // the ListView.section attached property. import QtQuick 2.0 +import "content" -//! [0] Rectangle { id: container - width: 200 - height: 250 + width: 300 + height: 360 ListModel { id: animalsModel + ListElement { name: "Ant"; size: "Tiny" } + ListElement { name: "Flea"; size: "Tiny" } ListElement { name: "Parrot"; size: "Small" } ListElement { name: "Guinea pig"; size: "Small" } + ListElement { name: "Rat"; size: "Small" } + ListElement { name: "Butterfly"; size: "Small" } ListElement { name: "Dog"; size: "Medium" } ListElement { name: "Cat"; size: "Medium" } - ListElement { name: "Elephant"; size: "Large" } + ListElement { name: "Pony"; size: "Medium" } + ListElement { name: "Koala"; size: "Medium" } + ListElement { name: "Horse"; size: "Large" } + ListElement { name: "Tiger"; size: "Large" } + ListElement { name: "Giraffe"; size: "Large" } + ListElement { name: "Elephant"; size: "Huge" } + ListElement { name: "Whale"; size: "Huge" } } +//! [0] // The delegate for each section header Component { id: sectionHeading @@ -69,19 +80,48 @@ Rectangle { Text { text: section font.bold: true + font.pixelSize: 20 } } } ListView { - anchors.fill: parent + id: view + anchors.top: parent.top + anchors.bottom: buttonBar.top + width: parent.width model: animalsModel - delegate: Text { text: name } + delegate: Text { text: name; font.pixelSize: 18 } section.property: "size" section.criteria: ViewSection.FullString section.delegate: sectionHeading } -} //! [0] + Row { + id: buttonBar + anchors.bottom: parent.bottom + anchors.bottomMargin: 1 + spacing: 1 + ToggleButton { + label: "CurrentLabelAtStart" + onToggled: { + if (active) + view.section.labelPositioning |= ViewSection.CurrentLabelAtStart + else + view.section.labelPositioning &= ~ViewSection.CurrentLabelAtStart + } + } + ToggleButton { + label: "NextLabelAtEnd" + onToggled: { + if (active) + view.section.labelPositioning |= ViewSection.NextLabelAtEnd + else + view.section.labelPositioning &= ~ViewSection.NextLabelAtEnd + } + } + } +} + diff --git a/src/declarative/items/qsglistview.cpp b/src/declarative/items/qsglistview.cpp index e8a6bf2..f4d1392 100644 --- a/src/declarative/items/qsglistview.cpp +++ b/src/declarative/items/qsglistview.cpp @@ -55,11 +55,132 @@ QT_BEGIN_NAMESPACE +class FxListItemSG; + +class QSGListViewPrivate : public QSGItemViewPrivate +{ + Q_DECLARE_PUBLIC(QSGListView) +public: + static QSGListViewPrivate* get(QSGListView *item) { return item->d_func(); } + + virtual Qt::Orientation layoutOrientation() const; + virtual bool isContentFlowReversed() const; + bool isRightToLeft() const; + + virtual qreal positionAt(int index) const; + virtual qreal endPositionAt(int index) const; + virtual qreal originPosition() const; + virtual qreal lastPosition() const; + + FxViewItem *nextVisibleItem() const; + FxViewItem *itemBefore(int modelIndex) const; + QString sectionAt(int modelIndex); + qreal snapPosAt(qreal pos); + FxViewItem *snapItemAt(qreal pos); + + virtual void init(); + virtual void clear(); + + virtual bool addVisibleItems(qreal fillFrom, qreal fillTo, bool doBuffer); + virtual bool removeNonVisibleItems(qreal bufferFrom, qreal bufferTo); + virtual void visibleItemsChanged(); + + virtual FxViewItem *newViewItem(int index, QSGItem *item); + virtual void initializeViewItem(FxViewItem *item); + virtual void releaseItem(FxViewItem *item); + virtual void repositionPackageItemAt(QSGItem *item, int index); + virtual void resetItemPosition(FxViewItem *item, FxViewItem *toItem); + virtual void resetFirstItemPosition(); + virtual void moveItemBy(FxViewItem *item, const QList &items, const QList &movedBackwards); + + virtual void createHighlight(); + virtual void updateHighlight(); + virtual void resetHighlightPosition(); + + virtual void setPosition(qreal pos); + virtual void layoutVisibleItems(); + bool applyInsertionChange(const QDeclarativeChangeSet::Insert &, QList *, QList *, FxViewItem *firstVisible); + + virtual void updateSections(); + QSGItem *getSectionItem(const QString §ion); + void releaseSectionItem(QSGItem *item); + void updateInlineSection(FxListItemSG *); + void updateCurrentSection(); + void updateStickySections(); + + virtual qreal headerSize() const; + virtual qreal footerSize() const; + virtual bool showHeaderForIndex(int index) const; + virtual bool showFooterForIndex(int index) const; + virtual void updateHeader(); + virtual void updateFooter(); + + virtual void changedVisibleIndex(int newIndex); + virtual void initializeCurrentItem(); + + void updateAverage(); + + void itemGeometryChanged(QSGItem *item, const QRectF &newGeometry, const QRectF &oldGeometry); + virtual void fixupPosition(); + virtual void fixup(AxisData &data, qreal minExtent, qreal maxExtent); + virtual void flick(QSGItemViewPrivate::AxisData &data, qreal minExtent, qreal maxExtent, qreal vSize, + QDeclarativeTimeLineCallback::Callback fixupCallback, qreal velocity); + + QSGListView::Orientation orient; + qreal visiblePos; + qreal averageSize; + qreal spacing; + QSGListView::SnapMode snapMode; + + QSmoothedAnimation *highlightPosAnimator; + QSmoothedAnimation *highlightSizeAnimator; + qreal highlightMoveSpeed; + qreal highlightResizeSpeed; + int highlightResizeDuration; + + QSGViewSection *sectionCriteria; + QString currentSection; + static const int sectionCacheSize = 5; + QSGItem *sectionCache[sectionCacheSize]; + QSGItem *currentSectionItem; + QString currentStickySection; + QSGItem *nextSectionItem; + QString nextStickySection; + QString lastVisibleSection; + QString nextSection; + + qreal overshootDist; + bool correctFlick : 1; + bool inFlickCorrection : 1; + + QSGListViewPrivate() + : orient(QSGListView::Vertical) + , visiblePos(0) + , averageSize(100.0), spacing(0.0) + , snapMode(QSGListView::NoSnap) + , highlightPosAnimator(0), highlightSizeAnimator(0) + , highlightMoveSpeed(400), highlightResizeSpeed(400), highlightResizeDuration(-1) + , sectionCriteria(0), currentSectionItem(0), nextSectionItem(0) + , overshootDist(0.0), correctFlick(false), inFlickCorrection(false) + {} + + friend class QSGViewSection; +}; + +//---------------------------------------------------------------------------- + +QSGViewSection::QSGViewSection(QSGListView *parent) + : QObject(parent), m_criteria(FullString), m_delegate(0), m_labelPositioning(InlineLabels) + , m_view(QSGListViewPrivate::get(parent)) +{ +} + void QSGViewSection::setProperty(const QString &property) { if (property != m_property) { m_property = property; emit propertyChanged(); + m_view->updateSections(); } } @@ -68,6 +189,7 @@ void QSGViewSection::setCriteria(QSGViewSection::SectionCriteria criteria) if (criteria != m_criteria) { m_criteria = criteria; emit criteriaChanged(); + m_view->updateSections(); } } @@ -76,6 +198,7 @@ void QSGViewSection::setDelegate(QDeclarativeComponent *delegate) if (delegate != m_delegate) { m_delegate = delegate; emit delegateChanged(); + m_view->updateSections(); } } @@ -87,6 +210,15 @@ QString QSGViewSection::sectionString(const QString &value) return value; } +void QSGViewSection::setLabelPositioning(int l) +{ + if (m_labelPositioning != l) { + m_labelPositioning = l; + emit labelPositioningChanged(); + m_view->updateSections(); + } +} + //---------------------------------------------------------------------------- class FxListItemSG : public FxViewItem @@ -179,103 +311,6 @@ public: //---------------------------------------------------------------------------- -class QSGListViewPrivate : public QSGItemViewPrivate -{ - Q_DECLARE_PUBLIC(QSGListView) -public: - virtual Qt::Orientation layoutOrientation() const; - virtual bool isContentFlowReversed() const; - bool isRightToLeft() const; - - virtual qreal positionAt(int index) const; - virtual qreal endPositionAt(int index) const; - virtual qreal originPosition() const; - virtual qreal lastPosition() const; - - FxViewItem *nextVisibleItem() const; - FxViewItem *itemBefore(int modelIndex) const; - QString sectionAt(int modelIndex); - qreal snapPosAt(qreal pos); - FxViewItem *snapItemAt(qreal pos); - - virtual void init(); - virtual void clear(); - - virtual bool addVisibleItems(qreal fillFrom, qreal fillTo, bool doBuffer); - virtual bool removeNonVisibleItems(qreal bufferFrom, qreal bufferTo); - virtual void visibleItemsChanged(); - - virtual FxViewItem *newViewItem(int index, QSGItem *item); - virtual void initializeViewItem(FxViewItem *item); - virtual void releaseItem(FxViewItem *item); - virtual void repositionPackageItemAt(QSGItem *item, int index); - virtual void resetItemPosition(FxViewItem *item, FxViewItem *toItem); - virtual void resetFirstItemPosition(); - virtual void moveItemBy(FxViewItem *item, const QList &items, const QList &movedBackwards); - - virtual void createHighlight(); - virtual void updateHighlight(); - virtual void resetHighlightPosition(); - - virtual void setPosition(qreal pos); - virtual void layoutVisibleItems(); - bool applyInsertionChange(const QDeclarativeChangeSet::Insert &, QList *, QList *, FxViewItem *firstVisible); - - virtual void updateSections(); - void createSection(FxListItemSG *); - void updateCurrentSection(); - - virtual qreal headerSize() const; - virtual qreal footerSize() const; - virtual bool showHeaderForIndex(int index) const; - virtual bool showFooterForIndex(int index) const; - virtual void updateHeader(); - virtual void updateFooter(); - - virtual void changedVisibleIndex(int newIndex); - virtual void initializeCurrentItem(); - - void updateAverage(); - - void itemGeometryChanged(QSGItem *item, const QRectF &newGeometry, const QRectF &oldGeometry); - virtual void fixupPosition(); - virtual void fixup(AxisData &data, qreal minExtent, qreal maxExtent); - virtual void flick(QSGItemViewPrivate::AxisData &data, qreal minExtent, qreal maxExtent, qreal vSize, - QDeclarativeTimeLineCallback::Callback fixupCallback, qreal velocity); - - QSGListView::Orientation orient; - qreal visiblePos; - qreal averageSize; - qreal spacing; - QSGListView::SnapMode snapMode; - - QSmoothedAnimation *highlightPosAnimator; - QSmoothedAnimation *highlightSizeAnimator; - qreal highlightMoveSpeed; - qreal highlightResizeSpeed; - int highlightResizeDuration; - - QSGViewSection *sectionCriteria; - QString currentSection; - static const int sectionCacheSize = 4; - QSGItem *sectionCache[sectionCacheSize]; - - qreal overshootDist; - bool correctFlick : 1; - bool inFlickCorrection : 1; - - QSGListViewPrivate() - : orient(QSGListView::Vertical) - , visiblePos(0) - , averageSize(100.0), spacing(0.0) - , snapMode(QSGListView::NoSnap) - , highlightPosAnimator(0), highlightSizeAnimator(0) - , highlightMoveSpeed(400), highlightResizeSpeed(400), highlightResizeDuration(-1) - , sectionCriteria(0) - , overshootDist(0.0), correctFlick(false), inFlickCorrection(false) - {} -}; - bool QSGListViewPrivate::isContentFlowReversed() const { return isRightToLeft(); @@ -474,6 +509,9 @@ void QSGListViewPrivate::clear() sectionCache[i] = 0; } visiblePos = 0; + currentSectionItem = 0; + nextSectionItem = 0; + lastVisibleSection = QString(); QSGItemViewPrivate::clear(); } @@ -514,7 +552,7 @@ void QSGListViewPrivate::initializeViewItem(FxViewItem *item) if (sectionCriteria && sectionCriteria->delegate()) { if (item->attached->m_prevSection != item->attached->m_section) - createSection(static_cast(item)); + updateInlineSection(static_cast(item)); } } @@ -790,42 +828,66 @@ void QSGListViewPrivate::resetHighlightPosition() static_cast(highlight)->setPosition(static_cast(currentItem)->itemPosition()); } -void QSGListViewPrivate::createSection(FxListItemSG *listItem) +QSGItem * QSGListViewPrivate::getSectionItem(const QString §ion) { Q_Q(QSGListView); + QSGItem *sectionItem = 0; + int i = sectionCacheSize-1; + while (i >= 0 && !sectionCache[i]) + --i; + if (i >= 0) { + sectionItem = sectionCache[i]; + sectionCache[i] = 0; + sectionItem->setVisible(true); + QDeclarativeContext *context = QDeclarativeEngine::contextForObject(sectionItem)->parentContext(); + context->setContextProperty(QLatin1String("section"), section); + } else { + QDeclarativeContext *context = new QDeclarativeContext(qmlContext(q)); + context->setContextProperty(QLatin1String("section"), section); + QObject *nobj = sectionCriteria->delegate()->beginCreate(context); + if (nobj) { + QDeclarative_setParent_noEvent(context, nobj); + sectionItem = qobject_cast(nobj); + if (!sectionItem) { + delete nobj; + } else { + sectionItem->setZ(2); + QDeclarative_setParent_noEvent(sectionItem, contentItem); + sectionItem->setParentItem(contentItem); + } + } else { + delete context; + } + sectionCriteria->delegate()->completeCreate(); + } + + return sectionItem; +} + +void QSGListViewPrivate::releaseSectionItem(QSGItem *item) +{ + int i = 0; + do { + if (!sectionCache[i]) { + sectionCache[i] = item; + sectionCache[i]->setVisible(false); + return; + } + ++i; + } while (i < sectionCacheSize); + delete item; +} + +void QSGListViewPrivate::updateInlineSection(FxListItemSG *listItem) +{ if (!sectionCriteria || !sectionCriteria->delegate()) return; - if (listItem->attached->m_prevSection != listItem->attached->m_section) { + if (listItem->attached->m_prevSection != listItem->attached->m_section + && (sectionCriteria->labelPositioning() & QSGViewSection::InlineLabels + || (listItem->index == 0 && sectionCriteria->labelPositioning() & QSGViewSection::CurrentLabelAtStart))) { if (!listItem->section) { qreal pos = listItem->position(); - int i = sectionCacheSize-1; - while (i >= 0 && !sectionCache[i]) - --i; - if (i >= 0) { - listItem->section = sectionCache[i]; - sectionCache[i] = 0; - listItem->section->setVisible(true); - QDeclarativeContext *context = QDeclarativeEngine::contextForObject(listItem->section)->parentContext(); - context->setContextProperty(QLatin1String("section"), listItem->attached->m_section); - } else { - QDeclarativeContext *context = new QDeclarativeContext(qmlContext(q)); - context->setContextProperty(QLatin1String("section"), listItem->attached->m_section); - QObject *nobj = sectionCriteria->delegate()->beginCreate(context); - if (nobj) { - QDeclarative_setParent_noEvent(context, nobj); - listItem->section = qobject_cast(nobj); - if (!listItem->section) { - delete nobj; - } else { - listItem->section->setZ(1); - QDeclarative_setParent_noEvent(listItem->section, q->contentItem()); - listItem->section->setParentItem(q->contentItem()); - } - } else { - delete context; - } - sectionCriteria->delegate()->completeCreate(); - } + listItem->section = getSectionItem(listItem->attached->m_section); listItem->setPosition(pos); } else { QDeclarativeContext *context = QDeclarativeEngine::contextForObject(listItem->section)->parentContext(); @@ -833,24 +895,119 @@ void QSGListViewPrivate::createSection(FxListItemSG *listItem) } } else if (listItem->section) { qreal pos = listItem->position(); - int i = 0; - do { - if (!sectionCache[i]) { - sectionCache[i] = listItem->section; - sectionCache[i]->setVisible(false); - listItem->section = 0; - return; - } - ++i; - } while (i < sectionCacheSize); - delete listItem->section; + releaseSectionItem(listItem->section); listItem->section = 0; listItem->setPosition(pos); } } +void QSGListViewPrivate::updateStickySections() +{ + if (!sectionCriteria || visibleItems.isEmpty() + || (!sectionCriteria->labelPositioning() && !currentSectionItem && !nextSectionItem)) + return; + + bool isRtl = isRightToLeft(); + qreal viewPos = isRightToLeft() ? -position()-size() : position(); + QSGItem *sectionItem = 0; + QSGItem *lastSectionItem = 0; + int index = 0; + while (index < visibleItems.count()) { + if (QSGItem *section = static_cast(visibleItems.at(index))->section) { + // Find the current section header and last visible section header + // and hide them if they will overlap a static section header. + qreal sectionPos = orient == QSGListView::Vertical ? section->y() : section->x(); + qreal sectionSize = orient == QSGListView::Vertical ? section->height() : section->width(); + bool visTop = true; + if (sectionCriteria->labelPositioning() & QSGViewSection::CurrentLabelAtStart) + visTop = isRtl ? -sectionPos-sectionSize >= viewPos : sectionPos >= viewPos; + bool visBot = true; + if (sectionCriteria->labelPositioning() & QSGViewSection::NextLabelAtEnd) + visBot = isRtl ? -sectionPos <= viewPos + size() : sectionPos + sectionSize < viewPos + size(); + section->setVisible(visBot && visTop); + if (visTop && !sectionItem) + sectionItem = section; + if (isRtl) { + if (-sectionPos <= viewPos + size()) + lastSectionItem = section; + } else { + if (sectionPos + sectionSize < viewPos + size()) + lastSectionItem = section; + } + } + ++index; + } + + // Current section header + if (sectionCriteria->labelPositioning() & QSGViewSection::CurrentLabelAtStart) { + if (!currentSectionItem) { + currentSectionItem = getSectionItem(currentSection); + } else if (currentStickySection != currentSection) { + QDeclarativeContext *context = QDeclarativeEngine::contextForObject(currentSectionItem)->parentContext(); + context->setContextProperty(QLatin1String("section"), currentSection); + } + currentStickySection = currentSection; + if (!currentSectionItem) + return; + + qreal sectionSize = orient == QSGListView::Vertical ? currentSectionItem->height() : currentSectionItem->width(); + bool atBeginning = orient == QSGListView::Vertical ? vData.atBeginning : (isRightToLeft() ? hData.atEnd : hData.atBeginning); + currentSectionItem->setVisible(!atBeginning && (!header || header->endPosition() < viewPos)); + qreal pos = isRtl ? position() + size() - sectionSize : viewPos; + if (sectionItem) { + qreal sectionPos = orient == QSGListView::Vertical ? sectionItem->y() : sectionItem->x(); + pos = isRtl ? qMax(pos, sectionPos + sectionSize) : qMin(pos, sectionPos - sectionSize); + } + if (header) + pos = isRtl ? qMin(header->endPosition(), pos) : qMax(header->endPosition(), pos); + if (footer) + pos = isRtl ? qMax(-footer->position(), pos) : qMin(footer->position() - sectionSize, pos); + if (orient == QSGListView::Vertical) + currentSectionItem->setY(pos); + else + currentSectionItem->setX(pos); + } else if (currentSectionItem) { + releaseSectionItem(currentSectionItem); + currentSectionItem = 0; + } + + // Next section footer + if (sectionCriteria->labelPositioning() & QSGViewSection::NextLabelAtEnd) { + if (!nextSectionItem) { + nextSectionItem = getSectionItem(nextSection); + } else if (nextStickySection != nextSection) { + QDeclarativeContext *context = QDeclarativeEngine::contextForObject(nextSectionItem)->parentContext(); + context->setContextProperty(QLatin1String("section"), nextSection); + } + nextStickySection = nextSection; + if (!nextSectionItem) + return; + + qreal sectionSize = orient == QSGListView::Vertical ? nextSectionItem->height() : nextSectionItem->width(); + nextSectionItem->setVisible(!nextSection.isEmpty()); + qreal pos = isRtl ? position() : viewPos + size() - sectionSize; + if (lastSectionItem) { + qreal sectionPos = orient == QSGListView::Vertical ? lastSectionItem->y() : lastSectionItem->x(); + pos = isRtl ? qMin(pos, sectionPos - sectionSize) : qMax(pos, sectionPos + sectionSize); + } + if (header) + pos = isRtl ? qMin(header->endPosition() - sectionSize, pos) : qMax(header->endPosition(), pos); + if (orient == QSGListView::Vertical) + nextSectionItem->setY(pos); + else + nextSectionItem->setX(pos); + } else if (nextSectionItem) { + releaseSectionItem(nextSectionItem); + nextSectionItem = 0; + } +} + void QSGListViewPrivate::updateSections() { + Q_Q(QSGListView); + if (!q->isComponentComplete()) + return; + QSGItemViewPrivate::updateSections(); if (sectionCriteria && !visibleItems.isEmpty()) { @@ -867,7 +1024,7 @@ void QSGListViewPrivate::updateSections() attached->setSection(sectionCriteria->sectionString(propValue)); idx = visibleItems.at(i)->index; } - createSection(static_cast(visibleItems.at(i))); + updateInlineSection(static_cast(visibleItems.at(i))); if (prevAtt) prevAtt->setNextSection(attached->section()); prevSection = attached->section(); @@ -880,6 +1037,10 @@ void QSGListViewPrivate::updateSections() prevAtt->setNextSection(QString()); } } + + lastVisibleSection = QString(); + updateCurrentSection(); + updateStickySections(); } void QSGListViewPrivate::updateCurrentSection() @@ -892,9 +1053,17 @@ void QSGListViewPrivate::updateCurrentSection() } return; } + bool inlineSections = sectionCriteria->labelPositioning() & QSGViewSection::InlineLabels; + qreal sectionThreshold = position(); + if (currentSectionItem && !inlineSections) + sectionThreshold += orient == QSGListView::Vertical ? currentSectionItem->height() : currentSectionItem->width(); int index = 0; - while (index < visibleItems.count() && visibleItems.at(index)->endPosition() <= position()) + int modelIndex = visibleIndex; + while (index < visibleItems.count() && visibleItems.at(index)->endPosition() <= sectionThreshold) { + if (visibleItems.at(index)->index != -1) + modelIndex = visibleItems.at(index)->index; ++index; + } QString newSection = currentSection; if (index < visibleItems.count()) @@ -903,8 +1072,39 @@ void QSGListViewPrivate::updateCurrentSection() newSection = (*visibleItems.constBegin())->attached->section(); if (newSection != currentSection) { currentSection = newSection; + updateStickySections(); emit q->currentSectionChanged(); } + + if (sectionCriteria->labelPositioning() & QSGViewSection::NextLabelAtEnd) { + // Don't want to scan for next section on every movement, so remember + // the last section in the visible area and only scan for the next + // section when that changes. Clearing lastVisibleSection will also + // force searching. + QString lastSection = currentSection; + qreal endPos = isRightToLeft() ? -position() : position() + size(); + if (nextSectionItem && !inlineSections) + endPos -= orient == QSGListView::Vertical ? nextSectionItem->height() : nextSectionItem->width(); + while (index < visibleItems.count() && static_cast(visibleItems.at(index))->itemPosition() < endPos) { + if (visibleItems.at(index)->index != -1) + modelIndex = visibleItems.at(index)->index; + lastSection = visibleItems.at(index)->attached->section(); + ++index; + } + + if (lastVisibleSection != lastSection) { + nextSection = QString(); + lastVisibleSection = lastSection; + for (int i = modelIndex; i < itemCount; ++i) { + QString section = sectionAt(i); + if (section != lastSection) { + nextSection = section; + updateStickySections(); + break; + } + } + } + } } void QSGListViewPrivate::initializeCurrentItem() @@ -1708,12 +1908,10 @@ void QSGListView::setOrientation(QSGListView::Orientation orientation) \qmlproperty string QtQuick2::ListView::section.property \qmlproperty enumeration QtQuick2::ListView::section.criteria \qmlproperty Component QtQuick2::ListView::section.delegate + \qmlproperty enumeration QtQuick2::ListView::section.labelPositioning - These properties hold the expression to be evaluated for the \l section attached property. - - The \l section attached property enables a ListView to be visually - separated into different parts. These properties determine how sections - are created. + These properties determine the expression to be evaluated and appearance + of the section labels. \c section.property holds the name of the property that is the basis of each section. @@ -1731,9 +1929,23 @@ void QSGListView::setOrientation(QSGListView::Orientation orientation) \c section.delegate holds the delegate component for each section. + \c section.labelPositioning determines whether the current and/or + next section labels stick to the start/end of the view, and whether + the labels are shown inline. This value can be a combination of: + + \list + \o ViewSection.InlineLabels - section labels are shown inline between + the item delegates separating sections (default). + \o ViewSection.CurrentLabelAtStart - the current section label sticks to the + start of the view as it is moved. + \o ViewSection.NextLabelAtEnd - the next section label (beyond all visible + sections) sticks to the end of the view as it is moved. \note Enabling + \c ViewSection.NextLabelAtEnd requires the view to scan ahead for the next + section, which has performance implications, especially for slower models. + \endlist + Each item in the list has attached properties named \c ListView.section, - \c ListView.previousSection and \c ListView.nextSection. These may be - used to place a section header for related items. + \c ListView.previousSection and \c ListView.nextSection. For example, here is a ListView that displays a list of animals, separated into sections. Each item in the ListView is placed in a different section @@ -2000,6 +2212,10 @@ void QSGListView::viewportMoved() } d->inFlickCorrection = false; } + if (d->sectionCriteria) { + d->updateCurrentSection(); + d->updateStickySections(); + } d->inViewportMoved = false; } diff --git a/src/declarative/items/qsglistview_p.h b/src/declarative/items/qsglistview_p.h index 8ff4b05..a3c31df 100644 --- a/src/declarative/items/qsglistview_p.h +++ b/src/declarative/items/qsglistview_p.h @@ -53,15 +53,19 @@ QT_BEGIN_NAMESPACE QT_MODULE(Declarative) +class QSGListView; +class QSGListViewPrivate; class Q_AUTOTEST_EXPORT QSGViewSection : public QObject { Q_OBJECT Q_PROPERTY(QString property READ property WRITE setProperty NOTIFY propertyChanged) Q_PROPERTY(SectionCriteria criteria READ criteria WRITE setCriteria NOTIFY criteriaChanged) Q_PROPERTY(QDeclarativeComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) + Q_PROPERTY(int labelPositioning READ labelPositioning WRITE setLabelPositioning NOTIFY labelPositioningChanged) Q_ENUMS(SectionCriteria) + Q_ENUMS(LabelPositioning) public: - QSGViewSection(QObject *parent=0) : QObject(parent), m_criteria(FullString), m_delegate(0) {} + QSGViewSection(QSGListView *parent=0); QString property() const { return m_property; } void setProperty(const QString &); @@ -75,21 +79,27 @@ public: QString sectionString(const QString &value); + enum LabelPositioning { InlineLabels = 0x01, CurrentLabelAtStart = 0x02, NextLabelAtEnd = 0x04 }; + int labelPositioning() { return m_labelPositioning; } + void setLabelPositioning(int pos); + Q_SIGNALS: void propertyChanged(); void criteriaChanged(); void delegateChanged(); + void labelPositioningChanged(); private: QString m_property; SectionCriteria m_criteria; QDeclarativeComponent *m_delegate; + int m_labelPositioning; + QSGListViewPrivate *m_view; }; class QSGVisualModel; class QSGListViewAttached; -class QSGListViewPrivate; class Q_AUTOTEST_EXPORT QSGListView : public QSGItemView { Q_OBJECT diff --git a/tests/auto/declarative/qsglistview/data/listview-sections_delegate.qml b/tests/auto/declarative/qsglistview/data/listview-sections_delegate.qml index 82f332c..496d8d7 100644 --- a/tests/auto/declarative/qsglistview/data/listview-sections_delegate.qml +++ b/tests/auto/declarative/qsglistview/data/listview-sections_delegate.qml @@ -2,6 +2,7 @@ import QtQuick 2.0 Rectangle { property string sectionProperty: "number" + property int sectionPositioning: ViewSection.InlineLabels width: 240 height: 320 color: "#ffffff" @@ -63,7 +64,8 @@ Rectangle { color: "#99bb99" height: 20 width: list.width - Text { text: section } + Text { text: section + ", " + parent.y + ", " + parent.objectName } } + section.labelPositioning: sectionPositioning } } diff --git a/tests/auto/declarative/qsglistview/tst_qsglistview.cpp b/tests/auto/declarative/qsglistview/tst_qsglistview.cpp index cbd1b01..ce1587d 100644 --- a/tests/auto/declarative/qsglistview/tst_qsglistview.cpp +++ b/tests/auto/declarative/qsglistview/tst_qsglistview.cpp @@ -105,6 +105,7 @@ private slots: void enforceRange_withoutHighlight(); void spacing(); void sections(); + void sectionsPositioning(); void sectionsDelegate(); void cacheBuffer(); void positionViewAtIndex(); @@ -144,6 +145,7 @@ private: template void moved(); template void clear(); QSGView *createView(); + QSGItem *findVisibleChild(QSGItem *parent, const QString &objectName); template T *findItem(QSGItem *parent, const QString &id, int index=-1); template @@ -1701,6 +1703,135 @@ void tst_QSGListView::sectionsDelegate() delete canvas; } +void tst_QSGListView::sectionsPositioning() +{ + QSGView *canvas = createView(); + canvas->show(); + + TestModel model; + for (int i = 0; i < 30; i++) + model.addItem("Item" + QString::number(i), QString::number(i/5)); + + QDeclarativeContext *ctxt = canvas->rootContext(); + ctxt->setContextProperty("testModel", &model); + + canvas->setSource(QUrl::fromLocalFile(SRCDIR "/data/listview-sections_delegate.qml")); + qApp->processEvents(); + canvas->rootObject()->setProperty("sectionPositioning", QVariant(int(QSGViewSection::InlineLabels | QSGViewSection::CurrentLabelAtStart | QSGViewSection::NextLabelAtEnd))); + + QSGListView *listview = findItem(canvas->rootObject(), "list"); + QTRY_VERIFY(listview != 0); + + QSGItem *contentItem = listview->contentItem(); + QTRY_VERIFY(contentItem != 0); + + for (int i = 0; i < 3; ++i) { + QSGItem *item = findItem(contentItem, "sect_" + QString::number(i)); + QVERIFY(item); + QTRY_COMPARE(item->y(), qreal(i*20*6)); + } + + QSGItem *topItem = findVisibleChild(contentItem, "sect_0"); // section header + QVERIFY(topItem); + QCOMPARE(topItem->y(), 0.); + + QSGItem *bottomItem = findVisibleChild(contentItem, "sect_3"); // section footer + QVERIFY(bottomItem); + QCOMPARE(bottomItem->y(), 300.); + + // move down a little and check that section header is at top + listview->setContentY(10); + QCOMPARE(topItem->y(), 0.); + + // push the top header up + listview->setContentY(110); + topItem = findVisibleChild(contentItem, "sect_0"); // section header + QVERIFY(topItem); + QCOMPARE(topItem->y(), 100.); + + QSGItem *item = findVisibleChild(contentItem, "sect_1"); + QVERIFY(item); + QCOMPARE(item->y(), 120.); + + bottomItem = findVisibleChild(contentItem, "sect_4"); // section footer + QVERIFY(bottomItem); + QCOMPARE(bottomItem->y(), 410.); + + // Move past section 0 + listview->setContentY(120); + topItem = findVisibleChild(contentItem, "sect_0"); // section header + QVERIFY(!topItem); + + // Push section footer down + listview->setContentY(70); + bottomItem = findVisibleChild(contentItem, "sect_4"); // section footer + QVERIFY(bottomItem); + QCOMPARE(bottomItem->y(), 380.); + + // Change current section + listview->setContentY(10); + model.modifyItem(0, "One", "aaa"); + model.modifyItem(1, "Two", "aaa"); + model.modifyItem(2, "Three", "aaa"); + model.modifyItem(3, "Four", "aaa"); + model.modifyItem(4, "Five", "aaa"); + QTest::qWait(300); + + QTRY_COMPARE(listview->currentSection(), QString("aaa")); + + for (int i = 0; i < 3; ++i) { + QSGItem *item = findItem(contentItem, + "sect_" + (i == 0 ? QString("aaa") : QString::number(i))); + QVERIFY(item); + QTRY_COMPARE(item->y(), qreal(i*20*6)); + } + + topItem = findVisibleChild(contentItem, "sect_aaa"); // section header + QVERIFY(topItem); + QCOMPARE(topItem->y(), 10.); + + // remove section boundary + listview->setContentY(120); + model.removeItem(5); + QTRY_COMPARE(listview->count(), model.count()); + for (int i = 0; i < 3; ++i) { + QSGItem *item = findItem(contentItem, + "sect_" + (i == 0 ? QString("aaa") : QString::number(i))); + QVERIFY(item); + QTRY_COMPARE(item->y(), qreal(i*20*6)); + } + + QTRY_VERIFY(topItem = findVisibleChild(contentItem, "sect_aaa")); // section header + QCOMPARE(topItem->y(), 120.); + QVERIFY(topItem = findVisibleChild(contentItem, "sect_1")); + QCOMPARE(topItem->y(), 140.); + + // Change the next section + listview->setContentY(0); + bottomItem = findVisibleChild(contentItem, "sect_3"); // section footer + QVERIFY(bottomItem); + QCOMPARE(bottomItem->y(), 320.); + + model.modifyItem(14, "New", "new"); + + QTRY_VERIFY(bottomItem = findVisibleChild(contentItem, "sect_new")); // section footer + QCOMPARE(bottomItem->y(), 320.); + + // Turn sticky footer off + listview->setContentY(50); + canvas->rootObject()->setProperty("sectionPositioning", QVariant(int(QSGViewSection::InlineLabels | QSGViewSection::CurrentLabelAtStart))); + item = findVisibleChild(contentItem, "sect_new"); // inline label restored + QCOMPARE(item->y(), 360.); + + // Turn sticky header off + listview->setContentY(50); + canvas->rootObject()->setProperty("sectionPositioning", QVariant(int(QSGViewSection::InlineLabels))); + item = findVisibleChild(contentItem, "sect_aaa"); // inline label restored + QCOMPARE(item->y(), 20.); + + delete canvas; +} + void tst_QSGListView::currentIndex() { TestModel model; @@ -3491,6 +3622,18 @@ QSGView *tst_QSGListView::createView() return canvas; } +QSGItem *tst_QSGListView::findVisibleChild(QSGItem *parent, const QString &objectName) +{ + QSGItem *item = 0; + QList items = parent->findChildren(objectName); + for (int i = 0; i < items.count(); ++i) { + if (items.at(i)->isVisible()) { + item = items.at(i); + break; + } + } + return item; +} /* Find an item with the specified objectName. If index is supplied then the item must also evaluate the {index} expression equal to index -- 1.7.2.5