@@ -0,0 +1,613 @@
2+ * Copyright (C) 2013 Canonical, Ltd.
4+ * This program is free software; you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation; version 3.
8+ * This program is distributed in the hope that it will be useful,
9+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
10+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11+ * GNU General Public License for more details.
13+ * You should have received a copy of the GNU General Public License
14+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
17+#include "listviewwithpageheader.h"
21+#include <private/qquickvisualdatamodel_p.h>
22+#include <private/qqmlglobal_p.h>
23+#include <private/qquickitem_p.h>
24+// #include <private/qquickrectangle_p.h>
26+ListViewWithPageHeader::ListViewWithPageHeader()
27+ : m_delegateModel(nullptr)
28+ , m_delegateValidated(false)
29+ , m_firstVisibleIndex(-1)
31+ , m_contentHeightDirty(false)
32+ , m_headerItem(nullptr)
33+ , m_previousContentY(0)
34+ , m_headerItemShownHeight(0)
36+ m_clipItem = new QQuickItem(contentItem());
37+// m_clipItem = new QQuickRectangle(contentItem());
38+// ((QQuickRectangle*)m_clipItem)->setColor(Qt::gray);
40+ connect(this, SIGNAL(contentWidthChanged()), this, SLOT(onContentWidthChanged()));
41+ connect(this, SIGNAL(contentHeightChanged()), this, SLOT(onContentHeightChanged()));
44+ListViewWithPageHeader::~ListViewWithPageHeader()
48+QAbstractItemModel *ListViewWithPageHeader::model() const
50+ return m_delegateModel ? m_delegateModel->model().value<QAbstractItemModel *>() : nullptr;
53+void ListViewWithPageHeader::setModel(QAbstractItemModel *model)
55+ if (model != this->model()) {
56+ if (!m_delegateModel) {
57+ createDelegateModel();
59+ disconnect(m_delegateModel, SIGNAL(modelUpdated(QQuickChangeSet,bool)), this, SLOT(onModelUpdated(QQuickChangeSet,bool)));
61+ m_delegateModel->setModel(QVariant::fromValue<QAbstractItemModel *>(model));
62+ connect(m_delegateModel, SIGNAL(modelUpdated(QQuickChangeSet,bool)), this, SLOT(onModelUpdated(QQuickChangeSet,bool)));
63+ Q_EMIT modelChanged();
65+// Q_EMIT contentHeightChanged();
66+// Q_EMIT contentYChanged();
70+QQmlComponent *ListViewWithPageHeader::delegate() const
72+ return m_delegateModel ? m_delegateModel->delegate() : nullptr;
75+void ListViewWithPageHeader::setDelegate(QQmlComponent *delegate)
77+ if (delegate != this->delegate()) {
78+ if (!m_delegateModel) {
79+ createDelegateModel();
82+ // Cleanup the existing items
83+ foreach(QQuickItem *item, m_visibleItems)
85+ m_visibleItems.clear();
86+ m_firstVisibleIndex = -1;
87+ QQuickFlickable::setContentY(0);
90+ m_delegateModel->setDelegate(delegate);
92+ Q_EMIT delegateChanged();
93+ m_delegateValidated = false;
94+ m_contentHeightDirty = true;
99+QQuickItem *ListViewWithPageHeader::header() const
101+ return m_headerItem;
104+void ListViewWithPageHeader::setHeader(QQuickItem *headerItem)
106+ if (m_headerItem != headerItem) {
107+ qreal oldHeaderHeight = 0;
108+ qreal oldHeaderY = 0;
110+ oldHeaderHeight = m_headerItem->height();
111+ oldHeaderY = m_headerItem->y();
112+ QQuickItemPrivate::get(m_headerItem)->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
114+ m_headerItem = headerItem;
116+ m_headerItem->setParentItem(contentItem());
117+ m_headerItem->setZ(1);
118+ QQuickItemPrivate::get(m_headerItem)->addItemChangeListener(this, QQuickItemPrivate::Geometry);
120+ qreal newHeaderHeight = m_headerItem ? m_headerItem->height() : 0;
121+ if (!m_visibleItems.isEmpty() && newHeaderHeight != oldHeaderHeight) {
122+ headerHeightChanged(newHeaderHeight, oldHeaderHeight, oldHeaderY);
124+ m_contentHeightDirty = true;
126+ Q_EMIT headerChanged();
130+void ListViewWithPageHeader::positionAtBeginning()
132+ qreal headerHeight = (m_headerItem ? m_headerItem->height() : 0);
133+ if (m_firstVisibleIndex != 0) {
134+ // This could be optimized by trying to reuse the interesection
135+ // of items that may end up intersecting between the existing
136+ // m_visibleItems and the items we are creating in the next loop
137+ foreach(QQuickItem *item, m_visibleItems)
139+ m_visibleItems.clear();
140+ m_firstVisibleIndex = -1;
142+ // Create the item 0, it will be already correctly positioned at createItem()
143+ m_clipItem->setY(0);
144+ QQuickItem *item = createItem(0, false);
145+ // Create the subsequent items
147+ qreal pos = item->y() + item->height();
148+ const qreal buffer = height() / 2;
149+ const qreal bufferTo = height() + buffer;
150+ while (modelIndex < m_delegateModel->count() && pos <= bufferTo) {
151+ if (!(item = createItem(modelIndex, false)))
153+ pos += item->height();
157+ m_previousContentY = m_visibleItems.first()->y() - headerHeight;
159+ QQuickFlickable::setContentY(m_visibleItems.first()->y() + m_clipItem->y() - headerHeight);
162+void ListViewWithPageHeader::showHeader()
167+qreal ListViewWithPageHeader::minYExtent() const
169+// qDebug() << "ListViewWithPageHeader::minYExtent" << m_minYExtent;
170+ return m_minYExtent;
173+void ListViewWithPageHeader::componentComplete()
175+ if (m_delegateModel)
176+ m_delegateModel->componentComplete();
178+ QQuickFlickable::componentComplete();
183+void ListViewWithPageHeader::viewportMoved(Qt::Orientations orient)
185+ QQuickFlickable::viewportMoved(orient);
186+// qDebug() << "ListViewWithPageHeader::viewportMoved" << contentY();
187+ qreal diff = m_previousContentY - contentY();
189+ auto oldHeaderItemShownHeight = m_headerItemShownHeight;
190+ if (contentY() < -m_minYExtent) {
191+ // Stick the header item to the top when dragging down
192+ m_headerItem->setY(contentY());
194+ // We are going down (but it's not because of the rebound at the end)
195+ // (but the header was not shown by it's own position)
196+ // or the header is partially shown
197+ const bool scrolledUp = m_previousContentY > contentY();
198+ const bool notRebounding = contentY() + height() < contentHeight();
199+ const bool notShownByItsOwn = contentY() + diff > m_headerItem->y() + m_headerItem->height();
201+ if (!scrolledUp && contentY() == -m_minYExtent) {
202+ m_headerItemShownHeight = 0;
203+ m_headerItem->setY(contentY());
204+ } else if ((scrolledUp && notRebounding && notShownByItsOwn) || (m_headerItemShownHeight > 0)) {
205+ m_headerItemShownHeight += diff;
206+ if (contentY() == -m_minYExtent) {
207+ m_headerItemShownHeight = 0;
209+ m_headerItemShownHeight = qBound(0., m_headerItemShownHeight, m_headerItem->height());
211+ if (m_headerItemShownHeight > 0) {
212+ m_headerItem->setY(contentY() - m_headerItem->height() + m_headerItemShownHeight);
214+ m_headerItem->setY(-m_minYExtent);
218+ // We will be changing the clip item, need to accomadate for it
219+ // otherwise we move the firstItem down/up twice
220+ diff += oldHeaderItemShownHeight - m_headerItemShownHeight;
222+ if (!m_visibleItems.isEmpty()) {
224+ QQuickItem *firstItem = m_visibleItems.first();
225+ firstItem->setY(firstItem->y() + diff);
227+ m_previousContentY = contentY();
231+void ListViewWithPageHeader::setContentY(qreal /*pos*/)
233+ qWarning() << "ListViewWithPageHeader::setContentY unsupported feature";
236+void ListViewWithPageHeader::createDelegateModel()
238+ m_delegateModel = new QQuickVisualDataModel(qmlContext(this), this);
239+ connect(m_delegateModel, SIGNAL(createdItem(int,QQuickItem*)), this, SLOT(itemCreated(int,QQuickItem*)));
240+ if (isComponentComplete())
241+ m_delegateModel->componentComplete();
244+void ListViewWithPageHeader::refill()
246+ if (!isComponentComplete()) {
247+ qWarning() << "Incomplete ListViewWithPageHeader::refill";
251+ const qreal buffer = height() / 2;
252+ const qreal from = contentY();
253+ const qreal to = from + height();
254+ const qreal bufferFrom = from - buffer;
255+ const qreal bufferTo = to + buffer;
257+ bool added = addVisibleItems(from, to, false);
258+ bool removed = removeNonVisibleItems(bufferFrom, bufferTo);
259+ added |= addVisibleItems(bufferFrom, bufferTo, true);
261+ if (added || removed) {
262+ m_contentHeightDirty = true;
266+bool ListViewWithPageHeader::addVisibleItems(qreal fillFrom, qreal fillTo, bool asynchronous)
271+ if (m_delegateModel->count() == 0)
275+// qDebug() << "ListViewWithPageHeader::addVisibleItems" << fillFrom << fillTo << asynchronous;
279+ if (!m_visibleItems.isEmpty()) {
280+ modelIndex = m_firstVisibleIndex + m_visibleItems.count();
281+ item = m_visibleItems.last();
282+ pos = item->y() + item->height() + m_clipItem->y();
284+ bool changed = false;
285+// qDebug() << (modelIndex < m_delegateModel->count()) << pos << fillTo;
286+ while (modelIndex < m_delegateModel->count() && pos <= fillTo) {
287+// qDebug() << "refill: append item" << modelIndex << "pos" << pos << "asynchronous" << asynchronous;
288+ if (!(item = createItem(modelIndex, asynchronous)))
290+ pos += item->height();
297+ if (!m_visibleItems.isEmpty()) {
298+ modelIndex = m_firstVisibleIndex - 1;
299+ item = m_visibleItems.first();
300+ pos = item->y() + m_clipItem->y();
302+ while (modelIndex >= 0 && pos > fillFrom) {
303+// qDebug() << "refill: prepend item" << modelIndex << "pos" << pos << "fillFrom" << fillFrom << "asynchronous" << asynchronous;
304+ if (!(item = createItem(modelIndex, asynchronous)))
306+ pos -= item->height();
314+void ListViewWithPageHeader::releaseItem(QQuickItem *item)
316+ QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(item);
317+ itemPrivate->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
318+ QQuickVisualModel::ReleaseFlags flags = m_delegateModel->release(item);
319+ if (flags & QQuickVisualModel::Destroyed) {
320+ item->setParentItem(nullptr);
324+bool ListViewWithPageHeader::removeNonVisibleItems(qreal bufferFrom, qreal bufferTo)
326+// qDebug() << "ListViewWithPageHeader::removeNonVisibleItems" << bufferFrom << bufferTo;
327+ bool changed = false;
329+ bool foundVisible = false;
331+ int removedItems = 0;
332+ while (i < m_visibleItems.count()) {
333+ QQuickItem *item = m_visibleItems[i];
334+ const qreal pos = item->y() + m_clipItem->y();
335+// qDebug() << i << pos << (pos + item->height()) << bufferFrom << bufferTo;
336+ if (pos + item->height() < bufferFrom || pos > bufferTo) {
337+// qDebug() << "Releasing" << i << (pos + item->height() < bufferFrom) << pos + item->height() << bufferFrom << (pos > bufferTo) << pos << bufferTo;
339+ m_visibleItems.removeAt(i);
343+ if (!foundVisible) {
344+ foundVisible = true;
345+ const int itemIndex = m_firstVisibleIndex + removedItems + i;
346+ m_firstVisibleIndex = itemIndex;
355+QQuickItem *ListViewWithPageHeader::createItem(int modelIndex, bool asynchronous)
357+// qDebug() << "CREATE ITEM" << modelIndex;
358+ if (asynchronous && m_asyncRequestedIndexes.contains(modelIndex))
361+ m_asyncRequestedIndexes.remove(modelIndex);
362+ QQuickItem *item = m_delegateModel->item(modelIndex, asynchronous);
364+ m_asyncRequestedIndexes << modelIndex;
367+// qDebug() << "ListViewWithPageHeader::createItem::We have the item" << modelIndex << item;
368+ QQuickItemPrivate::get(item)->addItemChangeListener(this, QQuickItemPrivate::Geometry);
369+ QQuickItem *prevItem = itemAtIndex(modelIndex - 1);
371+ item->setY(prevItem->y() + prevItem->height());
373+ QQuickItem *nextItem = itemAtIndex(modelIndex + 1);
375+ item->setY(nextItem->y() - item->height());
376+ } else if (modelIndex == 0 && m_headerItem) {
377+ item->setY(m_headerItem->height());
380+ QQuickItemPrivate::get(item)->setCulled(item->y() + item->height() + m_clipItem->y() < contentY() || item->y() + m_clipItem->y() >= contentY() + height());
381+ if (m_visibleItems.isEmpty()) {
382+ m_visibleItems << item;
384+ m_visibleItems.insert(modelIndex - m_firstVisibleIndex, item);
386+ if (m_firstVisibleIndex < 0 || modelIndex < m_firstVisibleIndex) {
387+ m_firstVisibleIndex = modelIndex;
394+void ListViewWithPageHeader::itemCreated(int modelIndex, QQuickItem *item)
396+// qDebug() << "ListViewWithPageHeader::itemCreated" << modelIndex << item;
398+ item->setParentItem(m_clipItem);
399+ if (m_asyncRequestedIndexes.remove(modelIndex)) {
400+ createItem(modelIndex, false);
404+void ListViewWithPageHeader::updateClipItem()
406+ m_clipItem->setHeight(height() - m_headerItemShownHeight);
407+ m_clipItem->setY(contentY() + m_headerItemShownHeight);
408+ m_clipItem->setClip(m_headerItemShownHeight > 0);
411+void ListViewWithPageHeader::onContentHeightChanged()
416+void ListViewWithPageHeader::onContentWidthChanged()
418+ m_clipItem->setWidth(contentItem()->width());
421+void ListViewWithPageHeader::onModelUpdated(const QQuickChangeSet &changeSet, bool /*reset*/)
423+ // TODO Do something with reset
424+// qDebug() << "ListViewWithPageHeader::onModelUpdated" << changeSet << reset;
425+ foreach(const QQuickChangeSet::Remove &remove, changeSet.removes()) {
426+// qDebug() << "ListViewWithPageHeader::onModelUpdated Remove" << remove;
427+ if (remove.index + remove.count > m_firstVisibleIndex && remove.index < m_firstVisibleIndex + m_visibleItems.count()) {
428+ const qreal oldFirstValidIndexPos = m_visibleItems.first()->y();
429+ // If all the items we are removing are either not created or culled
430+ // we have to grow down to avoid viewport changing
431+ bool growDown = true;
432+ for (int i = 0; growDown && i < remove.count; ++i) {
433+ const int modelIndex = remove.index + i;
434+ QQuickItem *item = itemAtIndex(modelIndex);
435+ if (item && !QQuickItemPrivate::get(item)->culled) {
439+ for (int i = remove.count - 1; i >= 0; --i) {
440+ const int visibleIndex = remove.index + i - m_firstVisibleIndex;
441+ if (visibleIndex >= 0 && visibleIndex < m_visibleItems.count()) {
442+ QQuickItem *item = m_visibleItems[visibleIndex];
444+ m_visibleItems.removeAt(visibleIndex);
449+ } else if (remove.index <= m_firstVisibleIndex && !m_visibleItems.isEmpty()) {
450+ // We removed the first item that is the one that positions the rest
451+ // position the new first item correctly
452+ m_visibleItems.first()->setY(oldFirstValidIndexPos);
454+ } else if (remove.index + remove.count <= m_firstVisibleIndex) {
455+ m_firstVisibleIndex -= remove.count;
459+ foreach(const QQuickChangeSet::Insert &insert, changeSet.inserts()) {
460+// qDebug() << "ListViewWithPageHeader::onModelUpdated Insert" << insert;
461+ const bool insertingInValidIndexes = insert.index > m_firstVisibleIndex && insert.index < m_firstVisibleIndex + m_visibleItems.count();
462+ const bool firstItemWithViewOnTop = insert.index == 0 && m_firstVisibleIndex == 0 && m_visibleItems.first()->y() + m_clipItem->y() > contentY();
463+ if (insertingInValidIndexes || firstItemWithViewOnTop)
465+ // If the items we are adding won't be really visible
466+ // we grow up instead of down to not change the viewport
467+ bool growUp = false;
468+ if (!firstItemWithViewOnTop) {
469+ for (int i = 0; i < m_visibleItems.count(); ++i) {
470+ if (!QQuickItemPrivate::get(m_visibleItems[i])->culled) {
471+ if (insert.index <= m_firstVisibleIndex + i) {
479+ const qreal oldFirstValidIndexPos = m_visibleItems.first()->y();
480+ for (int i = insert.count - 1; i >= 0; --i) {
481+ const int modelIndex = insert.index + i;
482+ QQuickItem *item = createItem(modelIndex, false);
484+ QQuickItem *firstItem = m_visibleItems.first();
485+ firstItem->setY(firstItem->y() - item->height());
489+ if (firstItemWithViewOnTop) {
490+ QQuickItem *firstItem = m_visibleItems.first();
491+ firstItem->setY(oldFirstValidIndexPos);
493+ } else if (insert.index <= m_firstVisibleIndex) {
494+ m_firstVisibleIndex += insert.count;
499+ m_contentHeightDirty = true;
502+void ListViewWithPageHeader::itemGeometryChanged(QQuickItem *item, const QRectF &newGeometry, const QRectF &oldGeometry)
504+ const qreal heightDiff = newGeometry.height() - oldGeometry.height();
505+ if (heightDiff != 0) {
506+ if (item == m_headerItem) {
507+ headerHeightChanged(newGeometry.height(), oldGeometry.height(), oldGeometry.y());
509+ if (oldGeometry.y() + oldGeometry.height() + m_clipItem->y() < contentY() && !m_visibleItems.isEmpty()) {
510+ item->setY(item->y() - heightDiff);
511+ if (m_visibleItems.first() == item) {
517+ m_contentHeightDirty = true;
521+void ListViewWithPageHeader::headerHeightChanged(qreal newHeaderHeight, qreal oldHeaderHeight, qreal oldHeaderY)
523+ const qreal heightDiff = newHeaderHeight - oldHeaderHeight;
524+ if (m_headerItemShownHeight > 0) {
525+ // If the header is shown because of the clip
527+ m_headerItemShownHeight += heightDiff;
528+ m_headerItemShownHeight = qBound(0., m_headerItemShownHeight, newHeaderHeight);
532+ if (oldHeaderY + oldHeaderHeight > contentY()) {
533+ // If the header is shown because its position
535+ QQuickItem *firstItem = m_visibleItems.first();
536+ firstItem->setY(firstItem->y() + heightDiff);
538+ // If the header is not on screen, just change the start of the list
539+ // so the viewport is not changed
546+void ListViewWithPageHeader::adjustMinYExtent()
548+ if (m_visibleItems.isEmpty()) {
551+ m_minYExtent = -m_visibleItems.first()->y() - m_clipItem->y() + (m_headerItem ? m_headerItem->height() : 0);
555+QQuickItem *ListViewWithPageHeader::itemAtIndex(int modelIndex) const
557+ const int visibleIndexedModelIndex = modelIndex - m_firstVisibleIndex;
558+ if (visibleIndexedModelIndex >= 0 && visibleIndexedModelIndex < m_visibleItems.count())
559+ return m_visibleItems[visibleIndexedModelIndex];
564+void ListViewWithPageHeader::updatePolish()
569+ if (!m_visibleItems.isEmpty()) {
570+ const qreal visibleFrom = contentY() - m_clipItem->y();
571+ const qreal visibleTo = contentY() + height() - m_clipItem->y();
573+ qreal pos = m_visibleItems.first()->y();
575+// qDebug() << "ListViewWithPageHeader::updatePolish Updating positions and heights. contentY" << contentY() << "minYExtent" << minYExtent();
576+ foreach(QQuickItem *item, m_visibleItems) {
577+ QQuickItemPrivate::get(item)->setCulled(pos + item->height() < visibleFrom || pos >= visibleTo);
579+// qDebug() << "ListViewWithPageHeader::updatePolish" << /*index << */pos + m_clipItem->y() << /*QQuickItemPrivate::get(item)->culled <<*/ (pos + item->height() < visibleFrom) << (pos > visibleTo);
580+ pos += item->height();
586+ if (m_contentHeightDirty) {
587+ qreal contentHeight;
588+ const int modelCount = model()->rowCount();
589+ const int visibleItems = m_visibleItems.count();
590+ const int lastValidIndex = m_firstVisibleIndex + visibleItems - 1;
591+ if (lastValidIndex == modelCount - 1) {
592+ QQuickItem *item = m_visibleItems.last();
593+ contentHeight = item->y() + item->height() + m_clipItem->y();
594+ if (m_firstVisibleIndex != 0) {
595+ // Make sure that if we are shrinking we tell the view we still fit
596+ m_minYExtent = qMax(m_minYExtent, -(contentHeight - height()));
599+ contentHeight = m_headerItem ? m_headerItem->height() : 0;
600+ if (!m_visibleItems.isEmpty()) {
601+ foreach(QQuickItem *item, m_visibleItems) {
602+ contentHeight += item->height();
604+ const int unknownSizes = modelCount - visibleItems;
605+ contentHeight += unknownSizes * contentHeight / visibleItems;
609+ m_contentHeightDirty = false;
610+ setContentHeight(contentHeight);