@@ -0,0 +1,1394 @@
1+/*
2+ * Copyright (C) 2013 Canonical, Ltd.
3+ *
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.
7+ *
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.
12+ *
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/>.
15+ */
16+
17+/*
18+ * Some documentation on how this thing works:
19+ *
20+ * A flickable has two very important concepts that define the top and
21+ * height of the flickable area.
22+ * The top is returned in minYExtent()
23+ * The height is set using setContentHeight()
24+ * By changing those two values we can make the list grow up or down
25+ * as needed. e.g. if we are in the middle of the list
26+ * and something that is above the viewport grows, since we do not
27+ * want to change the viewport because of that we just adjust the
28+ * minYExtent so that the list grows up.
29+ *
30+ * The implementation on the list relies on the delegateModel doing
31+ * most of the instantiation work. You call createItem() when you
32+ * need to create an item asking for it async or not. If returns null
33+ * it means the item will be created async and the model will call the
34+ * itemCreated slot with the item.
35+ *
36+ * updatePolish is the central point of dispatch for the work of the
37+ * class. It is called by the scene graph just before drawing the class.
38+ * In it we:
39+ * * Make sure all items are positioned correctly
40+ * * Add/Remove items if needed
41+ * * Update the content height if it was dirty
42+ *
43+ * m_visibleItems contains all the items we have created at the moment.
44+ * Actually not all of them are visible since it includes the ones
45+ * in the cache area we create asynchronously to help performance.
46+ * The first item in m_visibleItems has the m_firstVisibleIndex in
47+ * the model. If you actually want to know what is the first
48+ * item in the viewport you have to find the first non culled element
49+ * in m_visibleItems
50+ *
51+ * All the items (except the header) are childs of m_clipItem which
52+ * is a child of the contentItem() of the flickable (The contentItem()
53+ * is what actually 'moves' in a a flickable). This way
54+ * we can implement the clipping needed so we can have the header
55+ * shown in the middle of the list over the items without the items
56+ * leaking under the header in case it is transparent.
57+ *
58+ * The first item of m_visibleItems is the one that defines the
59+ * positions of all the rest of items (see updatePolish()) and
60+ * this is why sometimes we move it even if it's not the item
61+ * that has triggered the function (i.e. in itemGeometryChanged())
62+ *
63+ * m_visibleItems is a list of ListItem. Each ListItem
64+ * will contain a item and potentially a sectionItem. The sectionItem
65+ * is only there when the list is using sectionDelegate+sectionProperty
66+ * and this is the first item of the section. Each ListItem is vertically
67+ * layouted with the sectionItem first and then the item.
68+ *
69+ * For sectioning we also have a section item alone (m_topSectionItem)
70+ * that is used for the cases we need to show the sticky section item at
71+ * the top of the view.
72+ *
73+ * Each delegate item has a context property called heightToClip that is
74+ * used to communicate to the delegate implementation in case it has to
75+ * clip itself because of overlapping with the top sticky section item.
76+ * This is an implementation decision since it has been agreed it
77+ * is easier to implement the clipping in QML with this info than to
78+ * do it at the C++ level.
79+ *
80+ * Note that minYExtent and height are not always totally accurate, since
81+ * we don't have the items created we can't guess their heights
82+ * so we can only guarantee the values are correct when the first/last
83+ * items of the list are visible, otherwise we just live with good enough
84+ * values that make the list scrollable
85+ *
86+ * There are a few things that are not really implemented or tested properly
87+ * which we don't use at the moment like changing the model, changing
88+ * the section delegate, having a section delegate that changes its size, etc.
89+ * The known missing features are marked with TODOs along the code.
90+ */
91+
92+#include "listviewwithpageheader.h"
93+
94+#include <QCoreApplication>
95+#include <QDebug>
96+#include <qqmlinfo.h>
97+#include <qqmlengine.h>
98+#include <private/qqmlcontext_p.h>
99+#include <private/qqmldelegatemodel_p.h>
100+#include <private/qqmlglobal_p.h>
101+#include <private/qquickitem_p.h>
102+#include <private/qquickanimation_p.h>
103+// #include <private/qquickrectangle_p.h>
104+
105+qreal ListViewWithPageHeader::ListItem::height() const
106+{
107+ return m_item->height() + (m_sectionItem ? m_sectionItem->height() : 0);
108+}
109+
110+qreal ListViewWithPageHeader::ListItem::y() const
111+{
112+ return m_item->y() - (m_sectionItem ? m_sectionItem->height() : 0);
113+}
114+
115+void ListViewWithPageHeader::ListItem::setY(qreal newY)
116+{
117+ if (m_sectionItem) {
118+ m_sectionItem->setY(newY);
119+ m_item->setY(newY + m_sectionItem->height());
120+ } else {
121+ m_item->setY(newY);
122+ }
123+}
124+
125+bool ListViewWithPageHeader::ListItem::culled() const
126+{
127+ return QQuickItemPrivate::get(m_item)->culled;
128+}
129+
130+void ListViewWithPageHeader::ListItem::setCulled(bool culled)
131+{
132+ QQuickItemPrivate::get(m_item)->setCulled(culled);
133+ if (m_sectionItem)
134+ QQuickItemPrivate::get(m_sectionItem)->setCulled(culled);
135+}
136+
137+void ListViewWithPageHeader::ListItem::setSectionItem(QQuickItem *sectionItem)
138+{
139+ m_sectionItem = sectionItem;
140+}
141+
142+ListViewWithPageHeader::ListViewWithPageHeader()
143+ : m_delegateModel(nullptr)
144+ , m_asyncRequestedIndex(-1)
145+ , m_delegateValidated(false)
146+ , m_firstVisibleIndex(-1)
147+ , m_minYExtent(0)
148+ , m_contentHeightDirty(false)
149+ , m_headerItem(nullptr)
150+ , m_previousContentY(0)
151+ , m_headerItemShownHeight(0)
152+ , m_sectionDelegate(nullptr)
153+ , m_topSectionItem(nullptr)
154+ , m_forceNoClip(false)
155+ , m_inLayout(false)
156+ , m_inContentHeightKeepHeaderShown(false)
157+ , m_cacheBuffer(0)
158+{
159+ m_clipItem = new QQuickItem(contentItem());
160+// m_clipItem = new QQuickRectangle(contentItem());
161+// ((QQuickRectangle*)m_clipItem)->setColor(Qt::gray);
162+
163+ m_contentYAnimation = new QQuickNumberAnimation(this);
164+ m_contentYAnimation->setEasing(QEasingCurve::OutQuad);
165+ m_contentYAnimation->setProperty(QStringLiteral("contentY"));
166+ m_contentYAnimation->setDuration(200);
167+ m_contentYAnimation->setTargetObject(this);
168+
169+ connect(contentItem(), &QQuickItem::widthChanged, this, &ListViewWithPageHeader::onContentWidthChanged);
170+ connect(this, &ListViewWithPageHeader::contentHeightChanged, this, &ListViewWithPageHeader::onContentHeightChanged);
171+ connect(this, &ListViewWithPageHeader::heightChanged, this, &ListViewWithPageHeader::onHeightChanged);
172+ connect(m_contentYAnimation, &QQuickNumberAnimation::runningChanged, this, &ListViewWithPageHeader::contentYAnimationRunningChanged);
173+
174+ setFlickableDirection(VerticalFlick);
175+}
176+
177+ListViewWithPageHeader::~ListViewWithPageHeader()
178+{
179+}
180+
181+QAbstractItemModel *ListViewWithPageHeader::model() const
182+{
183+ return m_delegateModel ? m_delegateModel->model().value<QAbstractItemModel *>() : nullptr;
184+}
185+
186+void ListViewWithPageHeader::setModel(QAbstractItemModel *model)
187+{
188+ if (model != this->model()) {
189+ if (!m_delegateModel) {
190+ createDelegateModel();
191+ } else {
192+ disconnect(m_delegateModel, &QQmlDelegateModel::modelUpdated, this, &ListViewWithPageHeader::onModelUpdated);
193+ }
194+ m_delegateModel->setModel(QVariant::fromValue<QAbstractItemModel *>(model));
195+ connect(m_delegateModel, &QQmlDelegateModel::modelUpdated, this, &ListViewWithPageHeader::onModelUpdated);
196+ Q_EMIT modelChanged();
197+ polish();
198+ // TODO?
199+// Q_EMIT contentHeightChanged();
200+// Q_EMIT contentYChanged();
201+ }
202+}
203+
204+QQmlComponent *ListViewWithPageHeader::delegate() const
205+{
206+ return m_delegateModel ? m_delegateModel->delegate() : nullptr;
207+}
208+
209+void ListViewWithPageHeader::setDelegate(QQmlComponent *delegate)
210+{
211+ if (delegate != this->delegate()) {
212+ if (!m_delegateModel) {
213+ createDelegateModel();
214+ }
215+
216+ // Cleanup the existing items
217+ Q_FOREACH(ListItem *item, m_visibleItems)
218+ releaseItem(item);
219+ m_visibleItems.clear();
220+ initializeValuesForEmptyList();
221+
222+ m_delegateModel->setDelegate(delegate);
223+
224+ Q_EMIT delegateChanged();
225+ m_delegateValidated = false;
226+ m_contentHeightDirty = true;
227+ polish();
228+ }
229+}
230+
231+void ListViewWithPageHeader::initializeValuesForEmptyList()
232+{
233+ m_firstVisibleIndex = -1;
234+ adjustMinYExtent();
235+ setContentY(0);
236+ m_clipItem->setY(0);
237+ if (m_topSectionItem) {
238+ QQuickItemPrivate::get(m_topSectionItem)->setCulled(true);
239+ }
240+}
241+
242+QQuickItem *ListViewWithPageHeader::header() const
243+{
244+ return m_headerItem;
245+}
246+
247+void ListViewWithPageHeader::setHeader(QQuickItem *headerItem)
248+{
249+ if (m_headerItem != headerItem) {
250+ qreal oldHeaderHeight = 0;
251+ qreal oldHeaderY = 0;
252+ if (m_headerItem) {
253+ oldHeaderHeight = m_headerItem->height();
254+ oldHeaderY = m_headerItem->y();
255+ m_headerItem->setParentItem(nullptr);
256+ QQuickItemPrivate::get(m_headerItem)->removeItemChangeListener(this, QQuickItemPrivate::ImplicitHeight);
257+ }
258+ m_headerItem = headerItem;
259+ if (m_headerItem) {
260+ m_headerItem->setParentItem(contentItem());
261+ m_headerItem->setZ(1);
262+ m_previousHeaderImplicitHeight = m_headerItem->implicitHeight();
263+ QQuickItemPrivate::get(m_headerItem)->addItemChangeListener(this, QQuickItemPrivate::ImplicitHeight);
264+ }
265+ qreal newHeaderHeight = m_headerItem ? m_headerItem->height() : 0;
266+ if (!m_visibleItems.isEmpty() && newHeaderHeight != oldHeaderHeight) {
267+ headerHeightChanged(newHeaderHeight, oldHeaderHeight, oldHeaderY);
268+ polish();
269+ m_contentHeightDirty = true;
270+ }
271+ Q_EMIT headerChanged();
272+ }
273+}
274+
275+QQmlComponent *ListViewWithPageHeader::sectionDelegate() const
276+{
277+ return m_sectionDelegate;
278+}
279+
280+void ListViewWithPageHeader::setSectionDelegate(QQmlComponent *delegate)
281+{
282+ if (delegate != m_sectionDelegate) {
283+ // TODO clean existing sections
284+
285+ m_sectionDelegate = delegate;
286+
287+ m_topSectionItem = getSectionItem(QString(), false /*watchGeometry*/);
288+ if (m_topSectionItem) {
289+ m_topSectionItem->setZ(3);
290+ QQuickItemPrivate::get(m_topSectionItem)->setCulled(true);
291+ connect(m_topSectionItem, &QQuickItem::heightChanged, this, &ListViewWithPageHeader::stickyHeaderHeightChanged);
292+ }
293+
294+ // TODO create sections for existing items
295+
296+ Q_EMIT sectionDelegateChanged();
297+ Q_EMIT stickyHeaderHeightChanged();
298+ }
299+}
300+
301+QString ListViewWithPageHeader::sectionProperty() const
302+{
303+ return m_sectionProperty;
304+}
305+
306+void ListViewWithPageHeader::setSectionProperty(const QString &property)
307+{
308+ if (property != m_sectionProperty) {
309+ m_sectionProperty = property;
310+
311+ updateWatchedRoles();
312+
313+ // TODO recreate sections
314+
315+ Q_EMIT sectionPropertyChanged();
316+ }
317+}
318+
319+bool ListViewWithPageHeader::forceNoClip() const
320+{
321+ return m_forceNoClip;
322+}
323+
324+void ListViewWithPageHeader::setForceNoClip(bool noClip)
325+{
326+ if (noClip != m_forceNoClip) {
327+ m_forceNoClip = noClip;
328+ updateClipItem();
329+ Q_EMIT forceNoClipChanged();
330+ }
331+}
332+
333+int ListViewWithPageHeader::stickyHeaderHeight() const
334+{
335+ return m_topSectionItem ? m_topSectionItem->height() : 0;
336+}
337+
338+qreal ListViewWithPageHeader::headerItemShownHeight() const
339+{
340+ return m_headerItemShownHeight;
341+}
342+
343+int ListViewWithPageHeader::cacheBuffer() const
344+{
345+ return m_cacheBuffer;
346+}
347+
348+void ListViewWithPageHeader::setCacheBuffer(int cacheBuffer)
349+{
350+ if (cacheBuffer < 0) {
351+ qmlInfo(this) << "Cannot set a negative cache buffer";
352+ return;
353+ }
354+
355+ if (cacheBuffer != m_cacheBuffer) {
356+ m_cacheBuffer = cacheBuffer;
357+ Q_EMIT cacheBufferChanged();
358+ polish();
359+ }
360+}
361+
362+void ListViewWithPageHeader::positionAtBeginning()
363+{
364+ if (m_delegateModel->count() <= 0)
365+ return;
366+
367+ qreal headerHeight = (m_headerItem ? m_headerItem->height() : 0);
368+ if (m_firstVisibleIndex != 0) {
369+ // TODO This could be optimized by trying to reuse the interesection
370+ // of items that may end up intersecting between the existing
371+ // m_visibleItems and the items we are creating in the next loop
372+ Q_FOREACH(ListItem *item, m_visibleItems)
373+ releaseItem(item);
374+ m_visibleItems.clear();
375+ m_firstVisibleIndex = -1;
376+
377+ // Create the item 0, it will be already correctly positioned at createItem()
378+ m_clipItem->setY(0);
379+ ListItem *item = createItem(0, false);
380+ // Create the subsequent items
381+ int modelIndex = 1;
382+ qreal pos = item->y() + item->height();
383+ const qreal bufferTo = height() + m_cacheBuffer;
384+ while (modelIndex < m_delegateModel->count() && pos <= bufferTo) {
385+ if (!(item = createItem(modelIndex, false)))
386+ break;
387+ pos += item->height();
388+ ++modelIndex;
389+ }
390+
391+ m_previousContentY = m_visibleItems.first()->y() - headerHeight;
392+ }
393+ setContentY(m_visibleItems.first()->y() + m_clipItem->y() - headerHeight);
394+ if (m_headerItem) {
395+ // TODO This should not be needed and the code that adjust the m_headerItem position
396+ // in viewportMoved() should be enough but in some cases we have not found a way to reproduce
397+ // yet the code of viewportMoved() fails so here we make sure that at least if we are calling
398+ // positionAtBeginning the header item will be correctly positioned
399+ m_headerItem->setY(-m_minYExtent);
400+ }
401+}
402+
403+static inline bool uFuzzyCompare(qreal r1, qreal r2)
404+{
405+ return qFuzzyCompare(r1, r2) || (qFuzzyIsNull(r1) && qFuzzyIsNull(r2));
406+}
407+
408+void ListViewWithPageHeader::showHeader()
409+{
410+ if (!m_headerItem)
411+ return;
412+
413+ const auto to = qMax(-minYExtent(), contentY() - m_headerItem->height() + m_headerItemShownHeight);
414+ if (!uFuzzyCompare(to, contentY())) {
415+ const bool headerShownByItsOwn = contentY() < m_headerItem->y() + m_headerItem->height();
416+ if (headerShownByItsOwn && m_headerItemShownHeight == 0) {
417+ // We are not clipping since we are just at the top of the viewport
418+ // but because of the showHeader animation we will need to, so
419+ // enable the clipping without logically moving the items
420+ m_headerItemShownHeight = m_headerItem->y() + m_headerItem->height() - contentY();
421+ if (!m_visibleItems.isEmpty()) {
422+ updateClipItem();
423+ ListItem *firstItem = m_visibleItems.first();
424+ firstItem->setY(firstItem->y() - m_headerItemShownHeight);
425+ layout();
426+ }
427+ Q_EMIT headerItemShownHeightChanged();
428+ }
429+ m_contentYAnimation->setTo(to);
430+ contentYAnimationType = ContentYAnimationShowHeader;
431+ m_contentYAnimation->start();
432+ }
433+}
434+
435+int ListViewWithPageHeader::firstCreatedIndex() const
436+{
437+ return m_firstVisibleIndex;
438+}
439+
440+int ListViewWithPageHeader::createdItemCount() const
441+{
442+ return m_visibleItems.count();
443+}
444+
445+QQuickItem *ListViewWithPageHeader::item(int modelIndex) const
446+{
447+ ListItem *item = itemAtIndex(modelIndex);
448+ if (item)
449+ return item->m_item;
450+ else
451+ return nullptr;
452+}
453+
454+bool ListViewWithPageHeader::maximizeVisibleArea(int modelIndex)
455+{
456+ ListItem *listItem = itemAtIndex(modelIndex);
457+ if (listItem) {
458+ return maximizeVisibleArea(listItem, listItem->height());
459+ }
460+
461+ return false;
462+}
463+
464+bool ListViewWithPageHeader::maximizeVisibleArea(int modelIndex, int itemHeight)
465+{
466+ if (itemHeight < 0)
467+ return false;
468+
469+ ListItem *listItem = itemAtIndex(modelIndex);
470+ if (listItem) {
471+ return maximizeVisibleArea(listItem, itemHeight + (listItem->sectionItem() ? listItem->sectionItem()->height() : 0));
472+ }
473+
474+ return false;
475+}
476+
477+bool ListViewWithPageHeader::maximizeVisibleArea(ListItem *listItem, int listItemHeight)
478+{
479+ if (listItem) {
480+ layout();
481+ const auto listItemY = m_clipItem->y() + listItem->y();
482+ if (listItemY > contentY() && listItemY + listItemHeight > contentY() + height()) {
483+ // we can scroll the list up to show more stuff
484+ const auto to = qMin(listItemY, listItemY + listItemHeight - height());
485+ m_contentYAnimation->setTo(to);
486+ contentYAnimationType = ContentYAnimationMaximizeVisibleArea;
487+ m_contentYAnimation->start();
488+ } else if ((listItemY < contentY() && listItemY + listItemHeight < contentY() + height()) ||
489+ (m_topSectionItem && !listItem->sectionItem() && listItemY - m_topSectionItem->height() < contentY() && listItemY + listItemHeight < contentY() + height()))
490+ {
491+ // we can scroll the list down to show more stuff
492+ auto realVisibleListItemY = listItemY;
493+ if (m_topSectionItem) {
494+ // If we are showing the top section sticky item and this item doesn't have a section
495+ // item we have to make sure to scroll it a bit more so that it is not underlapping
496+ // the top section sticky item
497+ bool topSectionShown = !QQuickItemPrivate::get(m_topSectionItem)->culled;
498+ if (topSectionShown && !listItem->sectionItem()) {
499+ realVisibleListItemY -= m_topSectionItem->height();
500+ }
501+ }
502+ const auto to = qMax(realVisibleListItemY, listItemY + listItemHeight - height());
503+ m_contentYAnimation->setTo(to);
504+ contentYAnimationType = ContentYAnimationMaximizeVisibleArea;
505+ m_contentYAnimation->start();
506+ }
507+ return true;
508+ }
509+ return false;
510+}
511+
512+qreal ListViewWithPageHeader::minYExtent() const
513+{
514+// qDebug() << "ListViewWithPageHeader::minYExtent" << m_minYExtent;
515+ return m_minYExtent;
516+}
517+
518+qreal ListViewWithPageHeader::maxYExtent() const
519+{
520+ return height() - contentHeight();
521+}
522+
523+void ListViewWithPageHeader::componentComplete()
524+{
525+ if (m_delegateModel)
526+ m_delegateModel->componentComplete();
527+
528+ QQuickFlickable::componentComplete();
529+
530+ polish();
531+}
532+
533+void ListViewWithPageHeader::viewportMoved(Qt::Orientations orient)
534+{
535+ // Check we are not being taken down and don't paint anything
536+ // TODO Check if we still need this in 5.2
537+ // For reproduction just inifnite loop testDash or testDashContent
538+ if (!QQmlEngine::contextForObject(this)->parentContext())
539+ return;
540+
541+ QQuickFlickable::viewportMoved(orient);
542+// qDebug() << "ListViewWithPageHeader::viewportMoved" << contentY();
543+ const qreal diff = m_previousContentY - contentY();
544+ adjustHeader(diff);
545+ m_previousContentY = contentY();
546+ layout();
547+ polish();
548+}
549+
550+void ListViewWithPageHeader::adjustHeader(qreal diff)
551+{
552+ const bool showHeaderAnimationRunning = m_contentYAnimation->isRunning() && contentYAnimationType == ContentYAnimationShowHeader;
553+ if (m_headerItem) {
554+ const auto oldHeaderItemShownHeight = m_headerItemShownHeight;
555+ if (uFuzzyCompare(contentY(), -m_minYExtent) || contentY() > -m_minYExtent) {
556+ m_headerItem->setHeight(m_headerItem->implicitHeight());
557+ // We are going down (but it's not because of the rebound at the end)
558+ // (but the header was not shown by it's own position)
559+ // or the header is partially shown and we are not doing a maximizeVisibleArea either
560+ const bool scrolledUp = m_previousContentY > contentY();
561+ const bool notRebounding = qRound(contentY() + height()) < qRound(contentHeight());
562+ const bool notShownByItsOwn = contentY() + diff >= m_headerItem->y() + m_headerItem->height();
563+ const bool maximizeVisibleAreaRunning = m_contentYAnimation->isRunning() && contentYAnimationType == ContentYAnimationMaximizeVisibleArea;
564+
565+ if (!scrolledUp && (contentY() == -m_minYExtent || (m_headerItemShownHeight == 0 && m_previousContentY == m_headerItem->y()))) {
566+ m_headerItemShownHeight = 0;
567+ m_headerItem->setY(-m_minYExtent);
568+ } else if ((scrolledUp && notRebounding && notShownByItsOwn && !maximizeVisibleAreaRunning) || (m_headerItemShownHeight > 0) || m_inContentHeightKeepHeaderShown) {
569+ if (maximizeVisibleAreaRunning && diff > 0) {
570+ // If we are maximizing and the header was shown, make sure we hide it
571+ m_headerItemShownHeight -= diff;
572+ } else {
573+ m_headerItemShownHeight += diff;
574+ }
575+ if (uFuzzyCompare(contentY(), -m_minYExtent)) {
576+ m_headerItemShownHeight = 0;
577+ } else {
578+ m_headerItemShownHeight = qBound(static_cast<qreal>(0.), m_headerItemShownHeight, m_headerItem->height());
579+ }
580+ if (m_headerItemShownHeight > 0) {
581+ if (uFuzzyCompare(m_headerItem->height(), m_headerItemShownHeight)) {
582+ m_headerItem->setY(contentY());
583+ m_headerItemShownHeight = m_headerItem->height();
584+ } else {
585+ m_headerItem->setY(contentY() - m_headerItem->height() + m_headerItemShownHeight);
586+ }
587+ } else {
588+ m_headerItem->setY(-m_minYExtent);
589+ }
590+ } else if (m_headerItemShownHeight == 0 && m_previousContentY > m_headerItem->y() && contentY() < m_headerItem->y()) {
591+ // The header was hidden but now that we've moved up (e.g. because of item removed) it's visible
592+ // make sure it isn't
593+ m_headerItem->setY(-m_minYExtent);
594+ }
595+ Q_EMIT headerItemShownHeightChanged();
596+ } else {
597+ // Stick the header item to the top when dragging down
598+ m_headerItem->setY(contentY());
599+ m_headerItem->setHeight(m_headerItem->implicitHeight() + (-m_minYExtent - contentY()));
600+ }
601+ // We will be changing the clip item, need to accomadate for it
602+ // otherwise we move the firstItem down/up twice (unless the
603+ // show header animation is running, where we want to keep the viewport stable)
604+ if (!showHeaderAnimationRunning) {
605+ diff += oldHeaderItemShownHeight - m_headerItemShownHeight;
606+ } else {
607+ diff = -diff;
608+ }
609+ }
610+ if (!m_visibleItems.isEmpty()) {
611+ updateClipItem();
612+ ListItem *firstItem = m_visibleItems.first();
613+ firstItem->setY(firstItem->y() + diff);
614+ if (showHeaderAnimationRunning) {
615+ adjustMinYExtent();
616+ }
617+ }
618+}
619+
620+void ListViewWithPageHeader::createDelegateModel()
621+{
622+ m_delegateModel = new QQmlDelegateModel(qmlContext(this), this);
623+ connect(m_delegateModel, &QQmlDelegateModel::createdItem, this, &ListViewWithPageHeader::itemCreated);
624+ if (isComponentComplete())
625+ m_delegateModel->componentComplete();
626+ updateWatchedRoles();
627+}
628+
629+void ListViewWithPageHeader::refill()
630+{
631+ if (m_inLayout) {
632+ return;
633+ }
634+ if (!isComponentComplete()) {
635+ return;
636+ }
637+
638+ const qreal from = contentY();
639+ const qreal to = from + height();
640+ const qreal bufferFrom = from - m_cacheBuffer;
641+ const qreal bufferTo = to + m_cacheBuffer;
642+
643+ bool added = addVisibleItems(from, to, false);
644+ bool removed = removeNonVisibleItems(bufferFrom, bufferTo);
645+ added |= addVisibleItems(bufferFrom, bufferTo, true);
646+
647+ if (added || removed) {
648+ m_contentHeightDirty = true;
649+ }
650+}
651+
652+bool ListViewWithPageHeader::addVisibleItems(qreal fillFrom, qreal fillTo, bool asynchronous)
653+{
654+ if (!delegate())
655+ return false;
656+
657+ if (m_delegateModel->count() == 0)
658+ return false;
659+
660+ ListItem *item;
661+// qDebug() << "ListViewWithPageHeader::addVisibleItems" << fillFrom << fillTo << asynchronous;
662+
663+ int modelIndex = 0;
664+ qreal pos = 0;
665+ if (!m_visibleItems.isEmpty()) {
666+ modelIndex = m_firstVisibleIndex + m_visibleItems.count();
667+ item = m_visibleItems.last();
668+ pos = item->y() + item->height() + m_clipItem->y();
669+ }
670+ bool changed = false;
671+// qDebug() << (modelIndex < m_delegateModel->count()) << pos << fillTo;
672+ while (modelIndex < m_delegateModel->count() && pos <= fillTo) {
673+// qDebug() << "refill: append item" << modelIndex << "pos" << pos << "asynchronous" << asynchronous;
674+ if (!(item = createItem(modelIndex, asynchronous)))
675+ break;
676+ pos += item->height();
677+ ++modelIndex;
678+ changed = true;
679+ }
680+
681+ modelIndex = 0;
682+ pos = 0;
683+ if (!m_visibleItems.isEmpty()) {
684+ modelIndex = m_firstVisibleIndex - 1;
685+ item = m_visibleItems.first();
686+ pos = item->y() + m_clipItem->y();
687+ }
688+ while (modelIndex >= 0 && pos > fillFrom) {
689+// qDebug() << "refill: prepend item" << modelIndex << "pos" << pos << "fillFrom" << fillFrom << "asynchronous" << asynchronous;
690+ if (!(item = createItem(modelIndex, asynchronous)))
691+ break;
692+ pos -= item->height();
693+ --modelIndex;
694+ changed = true;
695+ }
696+
697+ return changed;
698+}
699+
700+void ListViewWithPageHeader::reallyReleaseItem(ListItem *listItem)
701+{
702+ QQuickItem *item = listItem->m_item;
703+ QQmlDelegateModel::ReleaseFlags flags = m_delegateModel->release(item);
704+ if (flags & QQmlDelegateModel::Destroyed) {
705+ item->setParentItem(nullptr);
706+ }
707+ if (listItem->sectionItem()) {
708+ listItem->sectionItem()->deleteLater();
709+ }
710+ delete listItem;
711+}
712+
713+void ListViewWithPageHeader::releaseItem(ListItem *listItem)
714+{
715+ QQuickItemPrivate::get(listItem->m_item)->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
716+ if (listItem->sectionItem()) {
717+ QQuickItemPrivate::get(listItem->sectionItem())->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
718+ }
719+ m_itemsToRelease << listItem;
720+}
721+
722+void ListViewWithPageHeader::updateWatchedRoles()
723+{
724+ if (m_delegateModel) {
725+ QList<QByteArray> roles;
726+ if (!m_sectionProperty.isEmpty())
727+ roles << m_sectionProperty.toUtf8();
728+ m_delegateModel->setWatchedRoles(roles);
729+ }
730+}
731+
732+QQuickItem *ListViewWithPageHeader::getSectionItem(int modelIndex, bool alreadyInserted)
733+{
734+ if (!m_sectionDelegate)
735+ return nullptr;
736+
737+ const QString section = m_delegateModel->stringValue(modelIndex, m_sectionProperty);
738+
739+ if (modelIndex > 0) {
740+ const QString prevSection = m_delegateModel->stringValue(modelIndex - 1, m_sectionProperty);
741+ if (section == prevSection)
742+ return nullptr;
743+ }
744+ if (modelIndex + 1 < model()->rowCount() && !alreadyInserted) {
745+ // Already inserted items can't steal next section header
746+ const QString nextSection = m_delegateModel->stringValue(modelIndex + 1, m_sectionProperty);
747+ if (section == nextSection) {
748+ // Steal the section header
749+ ListItem *nextItem = itemAtIndex(modelIndex); // Not +1 since not yet inserted into m_visibleItems
750+ if (nextItem) {
751+ QQuickItem *sectionItem = nextItem->sectionItem();
752+ nextItem->setSectionItem(nullptr);
753+ return sectionItem;
754+ }
755+ }
756+ }
757+
758+ return getSectionItem(section);
759+}
760+
761+QQuickItem *ListViewWithPageHeader::getSectionItem(const QString §ionText, bool watchGeometry)
762+{
763+ QQuickItem *sectionItem = nullptr;
764+
765+ QQmlContext *creationContext = m_sectionDelegate->creationContext();
766+ QQmlContext *context = new QQmlContext(creationContext ? creationContext : qmlContext(this));
767+ QObject *nobj = m_sectionDelegate->beginCreate(context);
768+ if (nobj) {
769+ QQml_setParent_noEvent(context, nobj);
770+ sectionItem = qobject_cast<QQuickItem *>(nobj);
771+ if (!sectionItem) {
772+ delete nobj;
773+ } else {
774+ sectionItem->setProperty("text", sectionText);
775+ sectionItem->setProperty("delegate", QVariant());
776+ sectionItem->setZ(2);
777+ QQml_setParent_noEvent(sectionItem, m_clipItem);
778+ sectionItem->setParentItem(m_clipItem);
779+ }
780+ } else {
781+ delete context;
782+ }
783+ m_sectionDelegate->completeCreate();
784+
785+ if (watchGeometry && sectionItem) {
786+ QQuickItemPrivate::get(sectionItem)->addItemChangeListener(this, QQuickItemPrivate::Geometry);
787+ }
788+
789+ return sectionItem;
790+}
791+
792+void ListViewWithPageHeader::updateSectionItem(int modelIndex)
793+{
794+ ListItem *item = itemAtIndex(modelIndex);
795+ if (item) {
796+ const QString sectionText = m_delegateModel->stringValue(modelIndex, m_sectionProperty);
797+
798+ bool needSectionHeader = true;
799+ // if it is the same section as the previous item need to drop the section
800+ if (modelIndex > 0) {
801+ const QString prevSection = m_delegateModel->stringValue(modelIndex - 1, m_sectionProperty);
802+ if (sectionText == prevSection) {
803+ needSectionHeader = false;
804+ }
805+ }
806+
807+ if (needSectionHeader) {
808+ if (!item->sectionItem()) {
809+ item->setSectionItem(getSectionItem(sectionText));
810+ } else {
811+ item->sectionItem()->setProperty("text", sectionText);
812+ }
813+ } else {
814+ if (item->sectionItem()) {
815+ item->sectionItem()->deleteLater();
816+ item->setSectionItem(nullptr);
817+ }
818+ }
819+ }
820+}
821+
822+bool ListViewWithPageHeader::removeNonVisibleItems(qreal bufferFrom, qreal bufferTo)
823+{
824+// qDebug() << "ListViewWithPageHeader::removeNonVisibleItems" << bufferFrom << bufferTo;
825+ // Do not remove items if we are overshooting up or down, since we'll come back
826+ // to the "stable" position and delete/create items without any reason
827+ if (contentY() < -m_minYExtent) {
828+ return false;
829+ } else if (contentY() + height() > contentHeight()) {
830+ return false;
831+ }
832+ bool changed = false;
833+
834+ bool foundVisible = false;
835+ int i = 0;
836+ int removedItems = 0;
837+ const auto oldFirstVisibleIndex = m_firstVisibleIndex;
838+ while (i < m_visibleItems.count()) {
839+ ListItem *item = m_visibleItems[i];
840+ const qreal pos = item->y() + m_clipItem->y();
841+// qDebug() << i << pos << (pos + item->height()) << bufferFrom << bufferTo;
842+ if (pos + item->height() < bufferFrom || pos > bufferTo) {
843+// qDebug() << "Releasing" << i << (pos + item->height() < bufferFrom) << pos + item->height() << bufferFrom << (pos > bufferTo) << pos << bufferTo;
844+ releaseItem(item);
845+ m_visibleItems.removeAt(i);
846+ changed = true;
847+ ++removedItems;
848+ } else {
849+ if (!foundVisible) {
850+ foundVisible = true;
851+ const int itemIndex = m_firstVisibleIndex + removedItems + i;
852+ m_firstVisibleIndex = itemIndex;
853+ }
854+ ++i;
855+ }
856+ }
857+ if (!foundVisible) {
858+ initializeValuesForEmptyList();
859+ }
860+ if (m_firstVisibleIndex != oldFirstVisibleIndex) {
861+ adjustMinYExtent();
862+ }
863+
864+ return changed;
865+}
866+
867+ListViewWithPageHeader::ListItem *ListViewWithPageHeader::createItem(int modelIndex, bool asynchronous)
868+{
869+// qDebug() << "CREATE ITEM" << modelIndex;
870+ if (asynchronous && m_asyncRequestedIndex != -1)
871+ return nullptr;
872+
873+ m_asyncRequestedIndex = -1;
874+ QObject* object = m_delegateModel->object(modelIndex, asynchronous);
875+ QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
876+ if (!item) {
877+ if (object) {
878+ m_delegateModel->release(object);
879+ if (!m_delegateValidated) {
880+ m_delegateValidated = true;
881+ QObject* delegateObj = delegate();
882+ qmlInfo(delegateObj ? delegateObj : this) << "Delegate must be of Item type";
883+ }
884+ } else {
885+ m_asyncRequestedIndex = modelIndex;
886+ }
887+ return 0;
888+ } else {
889+// qDebug() << "ListViewWithPageHeader::createItem::We have the item" << modelIndex << item;
890+ ListItem *listItem = new ListItem;
891+ listItem->m_item = item;
892+ listItem->setSectionItem(getSectionItem(modelIndex, false /*Not yet inserted into m_visibleItems*/));
893+ QQuickItemPrivate::get(item)->addItemChangeListener(this, QQuickItemPrivate::Geometry);
894+ ListItem *prevItem = itemAtIndex(modelIndex - 1);
895+ bool lostItem = false; // Is an item that we requested async but because of model changes
896+ // it is no longer attached to any of the existing items (has no prev nor next item)
897+ // nor is the first item
898+ if (prevItem) {
899+ listItem->setY(prevItem->y() + prevItem->height());
900+ } else {
901+ ListItem *currItem = itemAtIndex(modelIndex);
902+ if (currItem) {
903+ // There's something already in m_visibleItems at out index, meaning this is an insert, so attach to its top
904+ listItem->setY(currItem->y() - listItem->height());
905+ } else {
906+ ListItem *nextItem = itemAtIndex(modelIndex + 1);
907+ if (nextItem) {
908+ listItem->setY(nextItem->y() - listItem->height());
909+ } else if (modelIndex == 0) {
910+ listItem->setY(-m_clipItem->y() + (m_headerItem ? m_headerItem->height() : 0));
911+ } else if (!m_visibleItems.isEmpty()) {
912+ lostItem = true;
913+ }
914+ }
915+ }
916+ if (lostItem) {
917+ listItem->setCulled(true);
918+ releaseItem(listItem);
919+ listItem = nullptr;
920+ } else {
921+ listItem->setCulled(listItem->y() + listItem->height() + m_clipItem->y() <= contentY() || listItem->y() + m_clipItem->y() >= contentY() + height());
922+ if (m_visibleItems.isEmpty()) {
923+ m_visibleItems << listItem;
924+ } else {
925+ m_visibleItems.insert(modelIndex - m_firstVisibleIndex, listItem);
926+ }
927+ if (m_firstVisibleIndex < 0 || modelIndex < m_firstVisibleIndex) {
928+ m_firstVisibleIndex = modelIndex;
929+ polish();
930+ }
931+ if (listItem->sectionItem()) {
932+ listItem->sectionItem()->setProperty("delegate", QVariant::fromValue(listItem->m_item));
933+ }
934+ adjustMinYExtent();
935+ m_contentHeightDirty = true;
936+ }
937+ return listItem;
938+ }
939+}
940+
941+void ListViewWithPageHeader::itemCreated(int modelIndex, QObject *object)
942+{
943+ QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
944+ if (!item) {
945+ qWarning() << "ListViewWithPageHeader::itemCreated got a non item for index" << modelIndex;
946+ return;
947+ }
948+// qDebug() << "ListViewWithPageHeader::itemCreated" << modelIndex << item;
949+ // Check we are not being taken down and don't paint anything
950+ // TODO Check if we still need this in 5.2
951+ // For reproduction just inifnite loop testDash or testDashContent
952+ if (!QQmlEngine::contextForObject(this)->parentContext())
953+ return;
954+
955+ item->setParentItem(m_clipItem);
956+ // FIXME Why do we need the refreshExpressions call?
957+ QQmlContext *context = QQmlEngine::contextForObject(item)->parentContext();
958+ QQmlContextPrivate::get(context)->data->refreshExpressions();
959+ item->setProperty("heightToClip", QVariant::fromValue<int>(0));
960+ if (modelIndex == m_asyncRequestedIndex) {
961+ createItem(modelIndex, false);
962+ refill();
963+ }
964+}
965+
966+void ListViewWithPageHeader::updateClipItem()
967+{
968+ m_clipItem->setHeight(height() - m_headerItemShownHeight);
969+ m_clipItem->setY(contentY() + m_headerItemShownHeight);
970+ m_clipItem->setClip(!m_forceNoClip && m_headerItemShownHeight > 0);
971+}
972+
973+void ListViewWithPageHeader::onContentHeightChanged()
974+{
975+ updateClipItem();
976+}
977+
978+void ListViewWithPageHeader::onContentWidthChanged()
979+{
980+ m_clipItem->setWidth(contentItem()->width());
981+}
982+
983+void ListViewWithPageHeader::onHeightChanged()
984+{
985+ m_clipItem->setHeight(height() - m_headerItemShownHeight);
986+ polish();
987+}
988+
989+
990+void ListViewWithPageHeader::onModelUpdated(const QQmlChangeSet &changeSet, bool /*reset*/)
991+{
992+ // TODO Do something with reset
993+// qDebug() << "ListViewWithPageHeader::onModelUpdated" << changeSet << reset;
994+ const auto oldFirstVisibleIndex = m_firstVisibleIndex;
995+
996+ Q_FOREACH(const QQmlChangeSet::Change remove, changeSet.removes()) {
997+// qDebug() << "ListViewWithPageHeader::onModelUpdated Remove" << remove.index << remove.count;
998+ if (remove.index + remove.count > m_firstVisibleIndex && remove.index < m_firstVisibleIndex + m_visibleItems.count()) {
999+ const qreal oldFirstValidIndexPos = m_visibleItems.first()->y();
1000+ // If all the items we are removing are either not created or culled
1001+ // we have to grow down to avoid viewport changing
1002+ bool growDown = true;
1003+ for (int i = 0; growDown && i < remove.count; ++i) {
1004+ const int modelIndex = remove.index + i;
1005+ ListItem *item = itemAtIndex(modelIndex);
1006+ if (item && !item->culled()) {
1007+ growDown = false;
1008+ }
1009+ }
1010+ for (int i = remove.count - 1; i >= 0; --i) {
1011+ const int visibleIndex = remove.index + i - m_firstVisibleIndex;
1012+ if (visibleIndex >= 0 && visibleIndex < m_visibleItems.count()) {
1013+ ListItem *item = m_visibleItems[visibleIndex];
1014+ // Pass the section item down if needed
1015+ if (item->sectionItem() && visibleIndex + 1 < m_visibleItems.count()) {
1016+ ListItem *nextItem = m_visibleItems[visibleIndex + 1];
1017+ if (!nextItem->sectionItem()) {
1018+ nextItem->setSectionItem(item->sectionItem());
1019+ item->setSectionItem(nullptr);
1020+ }
1021+ }
1022+ releaseItem(item);
1023+ m_visibleItems.removeAt(visibleIndex);
1024+ }
1025+ }
1026+ if (growDown) {
1027+ adjustMinYExtent();
1028+ } else if (remove.index <= m_firstVisibleIndex && !m_visibleItems.isEmpty()) {
1029+ m_visibleItems.first()->setY(oldFirstValidIndexPos);
1030+ }
1031+ if (m_visibleItems.isEmpty()) {
1032+ m_firstVisibleIndex = -1;
1033+ } else {
1034+ m_firstVisibleIndex -= qMax(0, m_firstVisibleIndex - remove.index);
1035+ }
1036+ } else if (remove.index + remove.count <= m_firstVisibleIndex) {
1037+ m_firstVisibleIndex -= remove.count;
1038+ }
1039+ for (int i = remove.count - 1; i >= 0; --i) {
1040+ const int modelIndex = remove.index + i;
1041+ if (modelIndex == m_asyncRequestedIndex) {
1042+ m_asyncRequestedIndex = -1;
1043+ } else if (modelIndex < m_asyncRequestedIndex) {
1044+ m_asyncRequestedIndex--;
1045+ }
1046+ }
1047+ }
1048+
1049+ Q_FOREACH(const QQmlChangeSet::Change insert, changeSet.inserts()) {
1050+// qDebug() << "ListViewWithPageHeader::onModelUpdated Insert" << insert.index << insert.count;
1051+ const bool insertingInValidIndexes = insert.index > m_firstVisibleIndex && insert.index < m_firstVisibleIndex + m_visibleItems.count();
1052+ const bool firstItemWithViewOnTop = insert.index == 0 && m_firstVisibleIndex == 0 && m_visibleItems.first()->y() + m_clipItem->y() > contentY();
1053+ if (insertingInValidIndexes || firstItemWithViewOnTop)
1054+ {
1055+ // If the items we are adding won't be really visible
1056+ // we grow up instead of down to not change the viewport
1057+ bool growUp = false;
1058+ if (!firstItemWithViewOnTop) {
1059+ for (int i = 0; i < m_visibleItems.count(); ++i) {
1060+ if (!m_visibleItems[i]->culled()) {
1061+ if (insert.index <= m_firstVisibleIndex + i) {
1062+ growUp = true;
1063+ }
1064+ break;
1065+ }
1066+ }
1067+ }
1068+
1069+ const qreal oldFirstValidIndexPos = m_visibleItems.first()->y();
1070+ for (int i = insert.count - 1; i >= 0; --i) {
1071+ const int modelIndex = insert.index + i;
1072+ ListItem *item = createItem(modelIndex, false);
1073+ if (growUp) {
1074+ ListItem *firstItem = m_visibleItems.first();
1075+ firstItem->setY(firstItem->y() - item->height());
1076+ }
1077+ // Adding an item may break a "same section" chain, so check
1078+ // if we need adding a new section item
1079+ if (m_sectionDelegate) {
1080+ ListItem *nextItem = itemAtIndex(modelIndex + 1);
1081+ if (nextItem && !nextItem->sectionItem()) {
1082+ nextItem->setSectionItem(getSectionItem(modelIndex + 1, true /* alredy inserted into m_visibleItems*/));
1083+ if (growUp && nextItem->sectionItem()) {
1084+ ListItem *firstItem = m_visibleItems.first();
1085+ firstItem->setY(firstItem->y() - nextItem->sectionItem()->height());
1086+ }
1087+ }
1088+ }
1089+ }
1090+ if (firstItemWithViewOnTop) {
1091+ ListItem *firstItem = m_visibleItems.first();
1092+ firstItem->setY(oldFirstValidIndexPos);
1093+ }
1094+ adjustMinYExtent();
1095+ } else if (insert.index <= m_firstVisibleIndex) {
1096+ m_firstVisibleIndex += insert.count;
1097+ }
1098+
1099+ for (int i = insert.count - 1; i >= 0; --i) {
1100+ const int modelIndex = insert.index + i;
1101+ if (modelIndex <= m_asyncRequestedIndex) {
1102+ m_asyncRequestedIndex++;
1103+ }
1104+ }
1105+ }
1106+
1107+ Q_FOREACH(const QQmlChangeSet::Change change, changeSet.changes()) {
1108+ for (int i = change.start(); i < change.end(); ++i) {
1109+ updateSectionItem(i);
1110+ }
1111+ // Also update the section header for the next item after the change since it may be influenced
1112+ updateSectionItem(change.end());
1113+ }
1114+
1115+ if (m_firstVisibleIndex != oldFirstVisibleIndex) {
1116+ if (m_visibleItems.isEmpty()) {
1117+ initializeValuesForEmptyList();
1118+ } else {
1119+ adjustMinYExtent();
1120+ }
1121+ }
1122+
1123+ for (int i = 0; i < m_visibleItems.count(); ++i) {
1124+ ListItem *item = m_visibleItems[i];
1125+ if (item->sectionItem()) {
1126+ item->sectionItem()->setProperty("delegate", QVariant::fromValue(item->m_item));
1127+ }
1128+ }
1129+
1130+ layout();
1131+ polish();
1132+ m_contentHeightDirty = true;
1133+}
1134+
1135+void ListViewWithPageHeader::contentYAnimationRunningChanged(bool running)
1136+{
1137+ setInteractive(!running);
1138+ if (!running) {
1139+ m_contentHeightDirty = true;
1140+ polish();
1141+ }
1142+}
1143+
1144+void ListViewWithPageHeader::itemGeometryChanged(QQuickItem *item, const QRectF &newGeometry, const QRectF &oldGeometry)
1145+{
1146+ const qreal heightDiff = newGeometry.height() - oldGeometry.height();
1147+ if (heightDiff != 0) {
1148+ if (!m_visibleItems.isEmpty()) {
1149+ ListItem *firstItem = m_visibleItems.first();
1150+ const auto prevFirstItemY = firstItem->y();
1151+ if (!m_inContentHeightKeepHeaderShown && oldGeometry.y() + oldGeometry.height() + m_clipItem->y() <= contentY()) {
1152+ firstItem->setY(firstItem->y() - heightDiff);
1153+ } else if (item == firstItem->sectionItem()) {
1154+ firstItem->setY(firstItem->y() + heightDiff);
1155+ }
1156+
1157+ if (firstItem->y() != prevFirstItemY) {
1158+ adjustMinYExtent();
1159+ layout();
1160+ }
1161+ }
1162+ refill();
1163+ adjustMinYExtent();
1164+ polish();
1165+ m_contentHeightDirty = true;
1166+ }
1167+}
1168+
1169+void ListViewWithPageHeader::itemImplicitHeightChanged(QQuickItem *item)
1170+{
1171+ if (item == m_headerItem) {
1172+ const qreal diff = m_headerItem->implicitHeight() - m_previousHeaderImplicitHeight;
1173+ if (diff != 0) {
1174+ adjustHeader(diff);
1175+ m_previousHeaderImplicitHeight = m_headerItem->implicitHeight();
1176+ layout();
1177+ polish();
1178+ m_contentHeightDirty = true;
1179+ }
1180+ }
1181+}
1182+
1183+void ListViewWithPageHeader::headerHeightChanged(qreal newHeaderHeight, qreal oldHeaderHeight, qreal oldHeaderY)
1184+{
1185+ const qreal heightDiff = newHeaderHeight - oldHeaderHeight;
1186+ if (m_headerItemShownHeight > 0) {
1187+ // If the header is shown because of the clip
1188+ // Change its size
1189+ m_headerItemShownHeight += heightDiff;
1190+ m_headerItemShownHeight = qBound(static_cast<qreal>(0.), m_headerItemShownHeight, newHeaderHeight);
1191+ updateClipItem();
1192+ adjustMinYExtent();
1193+ Q_EMIT headerItemShownHeightChanged();
1194+ } else {
1195+ if (oldHeaderY + oldHeaderHeight > contentY()) {
1196+ // If the header is shown because its position
1197+ // Change its size
1198+ ListItem *firstItem = m_visibleItems.first();
1199+ firstItem->setY(firstItem->y() + heightDiff);
1200+ layout();
1201+ } else {
1202+ // If the header is not on screen, just change the start of the list
1203+ // so the viewport is not changed
1204+ adjustMinYExtent();
1205+ }
1206+ }
1207+}
1208+
1209+
1210+void ListViewWithPageHeader::adjustMinYExtent()
1211+{
1212+ if (m_visibleItems.isEmpty() || (contentHeight() + m_minYExtent < height())) {
1213+ m_minYExtent = 0;
1214+ } else {
1215+ qreal nonCreatedHeight = 0;
1216+ if (m_firstVisibleIndex != 0) {
1217+ // Calculate the average height of items to estimate the position of the list start
1218+ const int visibleItems = m_visibleItems.count();
1219+ qreal visibleItemsHeight = 0;
1220+ Q_FOREACH(ListItem *item, m_visibleItems) {
1221+ visibleItemsHeight += item->height();
1222+ }
1223+ nonCreatedHeight = m_firstVisibleIndex * visibleItemsHeight / visibleItems;
1224+// qDebug() << m_firstVisibleIndex << visibleItemsHeight << visibleItems << nonCreatedHeight;
1225+ }
1226+ const qreal headerHeight = (m_headerItem ? m_headerItem->implicitHeight() : 0);
1227+ m_minYExtent = nonCreatedHeight - m_visibleItems.first()->y() - m_clipItem->y() + headerHeight;
1228+ if (m_minYExtent != 0 && qFuzzyIsNull(m_minYExtent)) {
1229+ m_minYExtent = 0;
1230+ m_visibleItems.first()->setY(nonCreatedHeight - m_clipItem->y() + headerHeight);
1231+ }
1232+ }
1233+}
1234+
1235+ListViewWithPageHeader::ListItem *ListViewWithPageHeader::itemAtIndex(int modelIndex) const
1236+{
1237+ const int visibleIndexedModelIndex = modelIndex - m_firstVisibleIndex;
1238+ if (visibleIndexedModelIndex >= 0 && visibleIndexedModelIndex < m_visibleItems.count())
1239+ return m_visibleItems[visibleIndexedModelIndex];
1240+
1241+ return nullptr;
1242+}
1243+
1244+void ListViewWithPageHeader::layout()
1245+{
1246+ if (m_inLayout)
1247+ return;
1248+
1249+ m_inLayout = true;
1250+ if (!m_visibleItems.isEmpty()) {
1251+ const qreal visibleFrom = contentY() - m_clipItem->y() + m_headerItemShownHeight;
1252+ const qreal visibleTo = contentY() + height() - m_clipItem->y();
1253+
1254+ qreal pos = m_visibleItems.first()->y();
1255+
1256+// qDebug() << "ListViewWithPageHeader::layout Updating positions and heights. contentY" << contentY() << "minYExtent" << minYExtent();
1257+ int firstReallyVisibleItem = -1;
1258+ int modelIndex = m_firstVisibleIndex;
1259+ Q_FOREACH(ListItem *item, m_visibleItems) {
1260+ const bool cull = pos + item->height() <= visibleFrom || pos >= visibleTo;
1261+ item->setCulled(cull);
1262+ item->setY(pos);
1263+ if (!cull && firstReallyVisibleItem == -1) {
1264+ firstReallyVisibleItem = modelIndex;
1265+ if (m_topSectionItem) {
1266+ // Positing the top section sticky item is a two step process
1267+ // First we set it either we cull it (because it doesn't need to be sticked to the top)
1268+ // or stick it to the top
1269+ // Then after the loop we'll make sure that if there's another section just below it
1270+ // pushed the sticky section up to make it disappear
1271+ const qreal topSectionStickPos = m_headerItemShownHeight + contentY() - m_clipItem->y();
1272+ bool showStickySectionItem;
1273+ // We need to show the "top section sticky item" when the position at the "top" of the
1274+ // viewport is bigger than the start of the position of the first visible item
1275+ // i.e. the first visible item starts before the viewport, or when the first
1276+ // visible item starts just at the viewport start and it does not have its own section item
1277+ if (topSectionStickPos > pos) {
1278+ showStickySectionItem = true;
1279+ } else if (topSectionStickPos == pos) {
1280+ showStickySectionItem = !item->sectionItem();
1281+ } else {
1282+ showStickySectionItem = false;
1283+ }
1284+ if (!showStickySectionItem) {
1285+ QQuickItemPrivate::get(m_topSectionItem)->setCulled(true);
1286+ if (item->sectionItem()) {
1287+ // This seems it should happen since why would we cull the top section
1288+ // if the first visible item has no section header? This only happens briefly
1289+ // when increasing the height of a list that is at the bottom, the m_topSectionItem
1290+ // gets shown shortly in the next polish call
1291+ QQuickItemPrivate::get(item->sectionItem())->setCulled(false);
1292+ }
1293+ } else {
1294+ // Update the top sticky section header
1295+ const QString section = m_delegateModel->stringValue(modelIndex, m_sectionProperty);
1296+ m_topSectionItem->setProperty("text", section);
1297+
1298+ QQuickItemPrivate::get(m_topSectionItem)->setCulled(false);
1299+ m_topSectionItem->setY(topSectionStickPos);
1300+ m_topSectionItem->setProperty("delegate", QVariant::fromValue(item->m_item));
1301+ if (item->sectionItem()) {
1302+ QQuickItemPrivate::get(item->sectionItem())->setCulled(true);
1303+ }
1304+ }
1305+ }
1306+ }
1307+ const qreal clipFrom = visibleFrom + (!item->sectionItem() && m_topSectionItem && !QQuickItemPrivate::get(m_topSectionItem)->culled ? m_topSectionItem->height() : 0);
1308+ if (!cull && pos < clipFrom) {
1309+ item->m_item->setProperty("heightToClip", clipFrom - pos);
1310+ } else {
1311+ item->m_item->setProperty("heightToClip", QVariant::fromValue<int>(0));
1312+ }
1313+// qDebug() << "ListViewWithPageHeader::layout" << item->m_item;
1314+ pos += item->height();
1315+ ++modelIndex;
1316+ }
1317+
1318+ // Second step of section sticky item positioning
1319+ // Look at the next section header, check if it's pushing up the sticky one
1320+ if (m_topSectionItem) {
1321+ if (firstReallyVisibleItem >= 0) {
1322+ for (int i = firstReallyVisibleItem - m_firstVisibleIndex + 1; i < m_visibleItems.count(); ++i) {
1323+ ListItem *item = m_visibleItems[i];
1324+ if (item->sectionItem()) {
1325+ if (m_topSectionItem->y() + m_topSectionItem->height() > item->y()) {
1326+ m_topSectionItem->setY(item->y() - m_topSectionItem->height());
1327+ }
1328+ break;
1329+ }
1330+ }
1331+ }
1332+ }
1333+ }
1334+ if (m_headerItem) {
1335+ const bool cullHeader = m_headerItem->y() + m_headerItem->height() < contentY();
1336+ QQuickItemPrivate::get(m_headerItem)->setCulled(cullHeader);
1337+ }
1338+ m_inLayout = false;
1339+}
1340+
1341+void ListViewWithPageHeader::updatePolish()
1342+{
1343+ // Check we are not being taken down and don't paint anything
1344+ // TODO Check if we still need this in 5.2
1345+ // For reproduction just inifnite loop testDash or testDashContent
1346+ if (!QQmlEngine::contextForObject(this)->parentContext())
1347+ return;
1348+
1349+ Q_FOREACH(ListItem *item, m_itemsToRelease)
1350+ reallyReleaseItem(item);
1351+ m_itemsToRelease.clear();
1352+
1353+ if (!model())
1354+ return;
1355+
1356+ layout();
1357+
1358+ refill();
1359+
1360+ if (m_contentHeightDirty) {
1361+ qreal contentHeight;
1362+ if (m_visibleItems.isEmpty()) {
1363+ contentHeight = m_headerItem ? m_headerItem->height() : 0;
1364+ } else {
1365+ const int modelCount = model()->rowCount();
1366+ const int visibleItems = m_visibleItems.count();
1367+ const int lastValidIndex = m_firstVisibleIndex + visibleItems - 1;
1368+ qreal nonCreatedHeight = 0;
1369+ if (lastValidIndex != modelCount - 1) {
1370+ const int visibleItems = m_visibleItems.count();
1371+ qreal visibleItemsHeight = 0;
1372+ Q_FOREACH(ListItem *item, m_visibleItems) {
1373+ visibleItemsHeight += item->height();
1374+ }
1375+ const int unknownSizes = modelCount - (m_firstVisibleIndex + visibleItems);
1376+ nonCreatedHeight = unknownSizes * visibleItemsHeight / visibleItems;
1377+ }
1378+ ListItem *item = m_visibleItems.last();
1379+ contentHeight = nonCreatedHeight + item->y() + item->height() + m_clipItem->y();
1380+ if (m_firstVisibleIndex != 0) {
1381+ // Make sure that if we are shrinking we tell the view we still fit
1382+ m_minYExtent = qMax(m_minYExtent, -(contentHeight - height()));
1383+ }
1384+ }
1385+
1386+ m_contentHeightDirty = false;
1387+ adjustMinYExtent();
1388+ m_inContentHeightKeepHeaderShown = m_headerItem && m_headerItem->y() == contentY();
1389+ setContentHeight(contentHeight);
1390+ m_inContentHeightKeepHeaderShown = false;
1391+ }
1392+}
1393+
1394+#include "moc_listviewwithpageheader.cpp"