Unity 8
listviewwithpageheader.cpp
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 #pragma GCC diagnostic push
99 #pragma GCC diagnostic ignored "-pedantic"
100 #include <private/qqmlcontext_p.h>
101 #include <private/qqmldelegatemodel_p.h>
102 #include <private/qqmlglobal_p.h>
103 #include <private/qquickitem_p.h>
104 #include <private/qquickanimation_p.h>
105 #pragma GCC diagnostic pop
106 // #include <private/qquickrectangle_p.h>
107 
108 qreal ListViewWithPageHeader::ListItem::height() const
109 {
110  return m_item->height() + (m_sectionItem ? m_sectionItem->height() : 0);
111 }
112 
113 qreal ListViewWithPageHeader::ListItem::y() const
114 {
115  return m_item->y() - (m_sectionItem ? m_sectionItem->height() : 0);
116 }
117 
118 void ListViewWithPageHeader::ListItem::setY(qreal newY)
119 {
120  if (m_sectionItem) {
121  m_sectionItem->setY(newY);
122  m_item->setY(newY + m_sectionItem->height());
123  } else {
124  m_item->setY(newY);
125  }
126 }
127 
128 bool ListViewWithPageHeader::ListItem::culled() const
129 {
130  return QQuickItemPrivate::get(m_item)->culled;
131 }
132 
133 void ListViewWithPageHeader::ListItem::setCulled(bool culled)
134 {
135  QQuickItemPrivate::get(m_item)->setCulled(culled);
136  if (m_sectionItem)
137  QQuickItemPrivate::get(m_sectionItem)->setCulled(culled);
138 }
139 
140 void ListViewWithPageHeader::ListItem::setSectionItem(QQuickItem *sectionItem)
141 {
142  m_sectionItem = sectionItem;
143 }
144 
145 ListViewWithPageHeader::ListViewWithPageHeader()
146  : m_delegateModel(nullptr)
147  , m_asyncRequestedIndex(-1)
148  , m_delegateValidated(false)
149  , m_firstVisibleIndex(-1)
150  , m_minYExtent(0)
151  , m_contentHeightDirty(false)
152  , m_headerItem(nullptr)
153  , m_previousContentY(0)
154  , m_headerItemShownHeight(0)
155  , m_sectionDelegate(nullptr)
156  , m_topSectionItem(nullptr)
157  , m_forceNoClip(false)
158  , m_inLayout(false)
159  , m_inContentHeightKeepHeaderShown(false)
160  , m_cacheBuffer(0)
161 {
162  m_clipItem = new QQuickItem(contentItem());
163 // m_clipItem = new QQuickRectangle(contentItem());
164 // ((QQuickRectangle*)m_clipItem)->setColor(Qt::gray);
165 
166  m_contentYAnimation = new QQuickNumberAnimation(this);
167  m_contentYAnimation->setEasing(QEasingCurve::OutQuad);
168  m_contentYAnimation->setProperty(QStringLiteral("contentY"));
169  m_contentYAnimation->setDuration(200);
170  m_contentYAnimation->setTargetObject(this);
171 
172  connect(contentItem(), &QQuickItem::widthChanged, this, &ListViewWithPageHeader::onContentWidthChanged);
173  connect(this, &ListViewWithPageHeader::contentHeightChanged, this, &ListViewWithPageHeader::onContentHeightChanged);
174  connect(this, &ListViewWithPageHeader::heightChanged, this, &ListViewWithPageHeader::onHeightChanged);
175  connect(m_contentYAnimation, &QQuickNumberAnimation::runningChanged, this, &ListViewWithPageHeader::contentYAnimationRunningChanged);
176 
177  setFlickableDirection(VerticalFlick);
178 }
179 
180 ListViewWithPageHeader::~ListViewWithPageHeader()
181 {
182 }
183 
184 QAbstractItemModel *ListViewWithPageHeader::model() const
185 {
186  return m_delegateModel ? m_delegateModel->model().value<QAbstractItemModel *>() : nullptr;
187 }
188 
189 void ListViewWithPageHeader::setModel(QAbstractItemModel *model)
190 {
191  if (model != this->model()) {
192  if (!m_delegateModel) {
193  createDelegateModel();
194  } else {
195  disconnect(m_delegateModel, &QQmlDelegateModel::modelUpdated, this, &ListViewWithPageHeader::onModelUpdated);
196  }
197  m_delegateModel->setModel(QVariant::fromValue<QAbstractItemModel *>(model));
198  connect(m_delegateModel, &QQmlDelegateModel::modelUpdated, this, &ListViewWithPageHeader::onModelUpdated);
199  Q_EMIT modelChanged();
200  polish();
201  // TODO?
202 // Q_EMIT contentHeightChanged();
203 // Q_EMIT contentYChanged();
204  }
205 }
206 
207 QQmlComponent *ListViewWithPageHeader::delegate() const
208 {
209  return m_delegateModel ? m_delegateModel->delegate() : nullptr;
210 }
211 
212 void ListViewWithPageHeader::setDelegate(QQmlComponent *delegate)
213 {
214  if (delegate != this->delegate()) {
215  if (!m_delegateModel) {
216  createDelegateModel();
217  }
218 
219  // Cleanup the existing items
220  Q_FOREACH(ListItem *item, m_visibleItems)
221  releaseItem(item);
222  m_visibleItems.clear();
223  initializeValuesForEmptyList();
224 
225  m_delegateModel->setDelegate(delegate);
226 
227  Q_EMIT delegateChanged();
228  m_delegateValidated = false;
229  m_contentHeightDirty = true;
230  polish();
231  }
232 }
233 
234 void ListViewWithPageHeader::initializeValuesForEmptyList()
235 {
236  m_firstVisibleIndex = -1;
237  adjustMinYExtent();
238  setContentY(0);
239  m_clipItem->setY(0);
240  if (m_topSectionItem) {
241  QQuickItemPrivate::get(m_topSectionItem)->setCulled(true);
242  }
243 }
244 
245 QQuickItem *ListViewWithPageHeader::header() const
246 {
247  return m_headerItem;
248 }
249 
250 void ListViewWithPageHeader::setHeader(QQuickItem *headerItem)
251 {
252  if (m_headerItem != headerItem) {
253  qreal oldHeaderHeight = 0;
254  qreal oldHeaderY = 0;
255  if (m_headerItem) {
256  oldHeaderHeight = m_headerItem->height();
257  oldHeaderY = m_headerItem->y();
258  m_headerItem->setParentItem(nullptr);
259  }
260  m_headerItem = headerItem;
261  if (m_headerItem) {
262  m_headerItem->setParentItem(contentItem());
263  m_headerItem->setZ(1);
264  m_previousHeaderImplicitHeight = m_headerItem->implicitHeight();
265  QQuickItemPrivate::get(m_headerItem)->addItemChangeListener(this, QQuickItemPrivate::ImplicitHeight);
266  }
267  qreal newHeaderHeight = m_headerItem ? m_headerItem->height() : 0;
268  if (!m_visibleItems.isEmpty() && newHeaderHeight != oldHeaderHeight) {
269  headerHeightChanged(newHeaderHeight, oldHeaderHeight, oldHeaderY);
270  polish();
271  m_contentHeightDirty = true;
272  }
273  Q_EMIT headerChanged();
274  }
275 }
276 
277 QQmlComponent *ListViewWithPageHeader::sectionDelegate() const
278 {
279  return m_sectionDelegate;
280 }
281 
282 void ListViewWithPageHeader::setSectionDelegate(QQmlComponent *delegate)
283 {
284  if (delegate != m_sectionDelegate) {
285  // TODO clean existing sections
286 
287  m_sectionDelegate = delegate;
288 
289  m_topSectionItem = getSectionItem(QString(), false /*watchGeometry*/);
290  m_topSectionItem->setZ(3);
291  QQuickItemPrivate::get(m_topSectionItem)->setCulled(true);
292  connect(m_topSectionItem, &QQuickItem::heightChanged, this, &ListViewWithPageHeader::stickyHeaderHeightChanged);
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  }
591  Q_EMIT headerItemShownHeightChanged();
592  } else {
593  // Stick the header item to the top when dragging down
594  m_headerItem->setY(contentY());
595  m_headerItem->setHeight(m_headerItem->implicitHeight() + (-m_minYExtent - contentY()));
596  }
597  // We will be changing the clip item, need to accomadate for it
598  // otherwise we move the firstItem down/up twice (unless the
599  // show header animation is running, where we want to keep the viewport stable)
600  if (!showHeaderAnimationRunning) {
601  diff += oldHeaderItemShownHeight - m_headerItemShownHeight;
602  } else {
603  diff = -diff;
604  }
605  }
606  if (!m_visibleItems.isEmpty()) {
607  updateClipItem();
608  ListItem *firstItem = m_visibleItems.first();
609  firstItem->setY(firstItem->y() + diff);
610  if (showHeaderAnimationRunning) {
611  adjustMinYExtent();
612  }
613  }
614 }
615 
616 void ListViewWithPageHeader::createDelegateModel()
617 {
618  m_delegateModel = new QQmlDelegateModel(qmlContext(this), this);
619  connect(m_delegateModel, &QQmlDelegateModel::createdItem, this, &ListViewWithPageHeader::itemCreated);
620  if (isComponentComplete())
621  m_delegateModel->componentComplete();
622  updateWatchedRoles();
623 }
624 
625 void ListViewWithPageHeader::refill()
626 {
627  if (m_inLayout) {
628  return;
629  }
630  if (!isComponentComplete()) {
631  return;
632  }
633 
634  const qreal from = contentY();
635  const qreal to = from + height();
636  const qreal bufferFrom = from - m_cacheBuffer;
637  const qreal bufferTo = to + m_cacheBuffer;
638 
639  bool added = addVisibleItems(from, to, false);
640  bool removed = removeNonVisibleItems(bufferFrom, bufferTo);
641  added |= addVisibleItems(bufferFrom, bufferTo, true);
642 
643  if (added || removed) {
644  m_contentHeightDirty = true;
645  }
646 }
647 
648 bool ListViewWithPageHeader::addVisibleItems(qreal fillFrom, qreal fillTo, bool asynchronous)
649 {
650  if (!delegate())
651  return false;
652 
653  if (m_delegateModel->count() == 0)
654  return false;
655 
656  ListItem *item;
657 // qDebug() << "ListViewWithPageHeader::addVisibleItems" << fillFrom << fillTo << asynchronous;
658 
659  int modelIndex = 0;
660  qreal pos = 0;
661  if (!m_visibleItems.isEmpty()) {
662  modelIndex = m_firstVisibleIndex + m_visibleItems.count();
663  item = m_visibleItems.last();
664  pos = item->y() + item->height() + m_clipItem->y();
665  }
666  bool changed = false;
667 // qDebug() << (modelIndex < m_delegateModel->count()) << pos << fillTo;
668  while (modelIndex < m_delegateModel->count() && pos <= fillTo) {
669 // qDebug() << "refill: append item" << modelIndex << "pos" << pos << "asynchronous" << asynchronous;
670  if (!(item = createItem(modelIndex, asynchronous)))
671  break;
672  pos += item->height();
673  ++modelIndex;
674  changed = true;
675  }
676 
677  modelIndex = 0;
678  pos = 0;
679  if (!m_visibleItems.isEmpty()) {
680  modelIndex = m_firstVisibleIndex - 1;
681  item = m_visibleItems.first();
682  pos = item->y() + m_clipItem->y();
683  }
684  while (modelIndex >= 0 && pos > fillFrom) {
685 // qDebug() << "refill: prepend item" << modelIndex << "pos" << pos << "fillFrom" << fillFrom << "asynchronous" << asynchronous;
686  if (!(item = createItem(modelIndex, asynchronous)))
687  break;
688  pos -= item->height();
689  --modelIndex;
690  changed = true;
691  }
692 
693  return changed;
694 }
695 
696 void ListViewWithPageHeader::reallyReleaseItem(ListItem *listItem)
697 {
698  QQuickItem *item = listItem->m_item;
699  QQmlDelegateModel::ReleaseFlags flags = m_delegateModel->release(item);
700  if (flags & QQmlDelegateModel::Destroyed) {
701  item->setParentItem(nullptr);
702  }
703  listItem->sectionItem()->deleteLater();
704  delete listItem;
705 }
706 
707 void ListViewWithPageHeader::releaseItem(ListItem *listItem)
708 {
709  QQuickItemPrivate::get(listItem->m_item)->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
710  if (listItem->sectionItem()) {
711  QQuickItemPrivate::get(listItem->sectionItem())->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
712  }
713  m_itemsToRelease << listItem;
714 }
715 
716 void ListViewWithPageHeader::updateWatchedRoles()
717 {
718  if (m_delegateModel) {
719  QList<QByteArray> roles;
720  if (!m_sectionProperty.isEmpty())
721  roles << m_sectionProperty.toUtf8();
722  m_delegateModel->setWatchedRoles(roles);
723  }
724 }
725 
726 QQuickItem *ListViewWithPageHeader::getSectionItem(int modelIndex, bool alreadyInserted)
727 {
728  if (!m_sectionDelegate)
729  return nullptr;
730 
731  const QString section = m_delegateModel->stringValue(modelIndex, m_sectionProperty);
732 
733  if (modelIndex > 0) {
734  const QString prevSection = m_delegateModel->stringValue(modelIndex - 1, m_sectionProperty);
735  if (section == prevSection)
736  return nullptr;
737  }
738  if (modelIndex + 1 < model()->rowCount() && !alreadyInserted) {
739  // Already inserted items can't steal next section header
740  const QString nextSection = m_delegateModel->stringValue(modelIndex + 1, m_sectionProperty);
741  if (section == nextSection) {
742  // Steal the section header
743  ListItem *nextItem = itemAtIndex(modelIndex); // Not +1 since not yet inserted into m_visibleItems
744  if (nextItem) {
745  QQuickItem *sectionItem = nextItem->sectionItem();
746  nextItem->setSectionItem(nullptr);
747  return sectionItem;
748  }
749  }
750  }
751 
752  return getSectionItem(section);
753 }
754 
755 QQuickItem *ListViewWithPageHeader::getSectionItem(const QString &sectionText, bool watchGeometry)
756 {
757  QQuickItem *sectionItem = nullptr;
758 
759  QQmlContext *creationContext = m_sectionDelegate->creationContext();
760  QQmlContext *context = new QQmlContext(creationContext ? creationContext : qmlContext(this));
761  QObject *nobj = m_sectionDelegate->beginCreate(context);
762  if (nobj) {
763  QQml_setParent_noEvent(context, nobj);
764  sectionItem = qobject_cast<QQuickItem *>(nobj);
765  if (!sectionItem) {
766  delete nobj;
767  } else {
768  sectionItem->setProperty("text", sectionText);
769  sectionItem->setProperty("delegateIndex", -1);
770  sectionItem->setZ(2);
771  QQml_setParent_noEvent(sectionItem, m_clipItem);
772  sectionItem->setParentItem(m_clipItem);
773  }
774  } else {
775  delete context;
776  }
777  m_sectionDelegate->completeCreate();
778 
779  if (watchGeometry) {
780  QQuickItemPrivate::get(sectionItem)->addItemChangeListener(this, QQuickItemPrivate::Geometry);
781  }
782 
783  return sectionItem;
784 }
785 
786 void ListViewWithPageHeader::updateSectionItem(int modelIndex)
787 {
788  ListItem *item = itemAtIndex(modelIndex);
789  if (item) {
790  const QString sectionText = m_delegateModel->stringValue(modelIndex, m_sectionProperty);
791 
792  bool needSectionHeader = true;
793  // if it is the same section as the previous item need to drop the section
794  if (modelIndex > 0) {
795  const QString prevSection = m_delegateModel->stringValue(modelIndex - 1, m_sectionProperty);
796  if (sectionText == prevSection) {
797  needSectionHeader = false;
798  }
799  }
800 
801  if (needSectionHeader) {
802  if (!item->sectionItem()) {
803  item->setSectionItem(getSectionItem(sectionText));
804  } else {
805  item->sectionItem()->setProperty("text", sectionText);
806  }
807  } else {
808  if (item->sectionItem()) {
809  item->sectionItem()->deleteLater();
810  item->setSectionItem(nullptr);
811  }
812  }
813  }
814 }
815 
816 bool ListViewWithPageHeader::removeNonVisibleItems(qreal bufferFrom, qreal bufferTo)
817 {
818 // qDebug() << "ListViewWithPageHeader::removeNonVisibleItems" << bufferFrom << bufferTo;
819  // Do not remove items if we are overshooting up or down, since we'll come back
820  // to the "stable" position and delete/create items without any reason
821  if (contentY() < -m_minYExtent) {
822  return false;
823  } else if (contentY() + height() > contentHeight()) {
824  return false;
825  }
826  bool changed = false;
827 
828  bool foundVisible = false;
829  int i = 0;
830  int removedItems = 0;
831  const auto oldFirstVisibleIndex = m_firstVisibleIndex;
832  while (i < m_visibleItems.count()) {
833  ListItem *item = m_visibleItems[i];
834  const qreal pos = item->y() + m_clipItem->y();
835 // qDebug() << i << pos << (pos + item->height()) << bufferFrom << bufferTo;
836  if (pos + item->height() < bufferFrom || pos > bufferTo) {
837 // qDebug() << "Releasing" << i << (pos + item->height() < bufferFrom) << pos + item->height() << bufferFrom << (pos > bufferTo) << pos << bufferTo;
838  releaseItem(item);
839  m_visibleItems.removeAt(i);
840  changed = true;
841  ++removedItems;
842  } else {
843  if (!foundVisible) {
844  foundVisible = true;
845  const int itemIndex = m_firstVisibleIndex + removedItems + i;
846  m_firstVisibleIndex = itemIndex;
847  }
848  ++i;
849  }
850  }
851  if (!foundVisible) {
852  initializeValuesForEmptyList();
853  }
854  if (m_firstVisibleIndex != oldFirstVisibleIndex) {
855  adjustMinYExtent();
856  }
857 
858  return changed;
859 }
860 
861 ListViewWithPageHeader::ListItem *ListViewWithPageHeader::createItem(int modelIndex, bool asynchronous)
862 {
863 // qDebug() << "CREATE ITEM" << modelIndex;
864  if (asynchronous && m_asyncRequestedIndex != -1)
865  return nullptr;
866 
867  m_asyncRequestedIndex = -1;
868  QObject* object = m_delegateModel->object(modelIndex, asynchronous);
869  QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
870  if (!item) {
871  if (object) {
872  m_delegateModel->release(object);
873  if (!m_delegateValidated) {
874  m_delegateValidated = true;
875  QObject* delegateObj = delegate();
876  qmlInfo(delegateObj ? delegateObj : this) << "Delegate must be of Item type";
877  }
878  } else {
879  m_asyncRequestedIndex = modelIndex;
880  }
881  return 0;
882  } else {
883 // qDebug() << "ListViewWithPageHeader::createItem::We have the item" << modelIndex << item;
884  ListItem *listItem = new ListItem;
885  listItem->m_item = item;
886  listItem->setSectionItem(getSectionItem(modelIndex, false /*Not yet inserted into m_visibleItems*/));
887  QQuickItemPrivate::get(item)->addItemChangeListener(this, QQuickItemPrivate::Geometry);
888  ListItem *prevItem = itemAtIndex(modelIndex - 1);
889  bool lostItem = false; // Is an item that we requested async but because of model changes
890  // it is no longer attached to any of the existing items (has no prev nor next item)
891  // nor is the first item
892  if (prevItem) {
893  listItem->setY(prevItem->y() + prevItem->height());
894  } else {
895  ListItem *currItem = itemAtIndex(modelIndex);
896  if (currItem) {
897  // There's something already in m_visibleItems at out index, meaning this is an insert, so attach to its top
898  listItem->setY(currItem->y() - listItem->height());
899  } else {
900  ListItem *nextItem = itemAtIndex(modelIndex + 1);
901  if (nextItem) {
902  listItem->setY(nextItem->y() - listItem->height());
903  } else if (modelIndex == 0) {
904  listItem->setY(-m_clipItem->y() + (m_headerItem ? m_headerItem->height() : 0));
905  } else if (!m_visibleItems.isEmpty()) {
906  lostItem = true;
907  }
908  }
909  }
910  if (lostItem) {
911  listItem->setCulled(true);
912  releaseItem(listItem);
913  listItem = nullptr;
914  } else {
915  listItem->setCulled(listItem->y() + listItem->height() + m_clipItem->y() <= contentY() || listItem->y() + m_clipItem->y() >= contentY() + height());
916  if (m_visibleItems.isEmpty()) {
917  m_visibleItems << listItem;
918  } else {
919  m_visibleItems.insert(modelIndex - m_firstVisibleIndex, listItem);
920  }
921  if (m_firstVisibleIndex < 0 || modelIndex < m_firstVisibleIndex) {
922  m_firstVisibleIndex = modelIndex;
923  polish();
924  }
925  if (listItem->sectionItem()) {
926  listItem->sectionItem()->setProperty("delegateIndex", modelIndex);
927  }
928  adjustMinYExtent();
929  m_contentHeightDirty = true;
930  }
931  return listItem;
932  }
933 }
934 
935 void ListViewWithPageHeader::itemCreated(int modelIndex, QObject *object)
936 {
937  QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
938  if (!item) {
939  qWarning() << "ListViewWithPageHeader::itemCreated got a non item for index" << modelIndex;
940  return;
941  }
942 // qDebug() << "ListViewWithPageHeader::itemCreated" << modelIndex << item;
943  // Check we are not being taken down and don't paint anything
944  // TODO Check if we still need this in 5.2
945  // For reproduction just inifnite loop testDash or testDashContent
946  if (!QQmlEngine::contextForObject(this)->parentContext())
947  return;
948 
949  item->setParentItem(m_clipItem);
950  // FIXME Why do we need the refreshExpressions call?
951  QQmlContext *context = QQmlEngine::contextForObject(item)->parentContext();
952  QQmlContextPrivate::get(context)->data->refreshExpressions();
953  item->setProperty("heightToClip", QVariant::fromValue<int>(0));
954  if (modelIndex == m_asyncRequestedIndex) {
955  createItem(modelIndex, false);
956  refill();
957  }
958 }
959 
960 void ListViewWithPageHeader::updateClipItem()
961 {
962  m_clipItem->setHeight(height() - m_headerItemShownHeight);
963  m_clipItem->setY(contentY() + m_headerItemShownHeight);
964  m_clipItem->setClip(!m_forceNoClip && m_headerItemShownHeight > 0);
965 }
966 
967 void ListViewWithPageHeader::onContentHeightChanged()
968 {
969  updateClipItem();
970 }
971 
972 void ListViewWithPageHeader::onContentWidthChanged()
973 {
974  m_clipItem->setWidth(contentItem()->width());
975 }
976 
977 void ListViewWithPageHeader::onHeightChanged()
978 {
979  polish();
980 }
981 
982 
983 void ListViewWithPageHeader::onModelUpdated(const QQmlChangeSet &changeSet, bool /*reset*/)
984 {
985  // TODO Do something with reset
986 // qDebug() << "ListViewWithPageHeader::onModelUpdated" << changeSet << reset;
987  const auto oldFirstVisibleIndex = m_firstVisibleIndex;
988 
989  Q_FOREACH(const QQmlChangeSet::Change remove, changeSet.removes()) {
990 // qDebug() << "ListViewWithPageHeader::onModelUpdated Remove" << remove.index << remove.count;
991  if (remove.index + remove.count > m_firstVisibleIndex && remove.index < m_firstVisibleIndex + m_visibleItems.count()) {
992  const qreal oldFirstValidIndexPos = m_visibleItems.first()->y();
993  // If all the items we are removing are either not created or culled
994  // we have to grow down to avoid viewport changing
995  bool growDown = true;
996  for (int i = 0; growDown && i < remove.count; ++i) {
997  const int modelIndex = remove.index + i;
998  ListItem *item = itemAtIndex(modelIndex);
999  if (item && !item->culled()) {
1000  growDown = false;
1001  }
1002  }
1003  for (int i = remove.count - 1; i >= 0; --i) {
1004  const int visibleIndex = remove.index + i - m_firstVisibleIndex;
1005  if (visibleIndex >= 0 && visibleIndex < m_visibleItems.count()) {
1006  ListItem *item = m_visibleItems[visibleIndex];
1007  // Pass the section item down if needed
1008  if (item->sectionItem() && visibleIndex + 1 < m_visibleItems.count()) {
1009  ListItem *nextItem = m_visibleItems[visibleIndex + 1];
1010  if (!nextItem->sectionItem()) {
1011  nextItem->setSectionItem(item->sectionItem());
1012  item->setSectionItem(nullptr);
1013  }
1014  }
1015  releaseItem(item);
1016  m_visibleItems.removeAt(visibleIndex);
1017  }
1018  }
1019  if (growDown) {
1020  adjustMinYExtent();
1021  } else if (remove.index <= m_firstVisibleIndex && !m_visibleItems.isEmpty()) {
1022  m_visibleItems.first()->setY(oldFirstValidIndexPos);
1023  }
1024  if (m_visibleItems.isEmpty()) {
1025  m_firstVisibleIndex = -1;
1026  } else {
1027  m_firstVisibleIndex -= qMax(0, m_firstVisibleIndex - remove.index);
1028  }
1029  } else if (remove.index + remove.count <= m_firstVisibleIndex) {
1030  m_firstVisibleIndex -= remove.count;
1031  }
1032  for (int i = remove.count - 1; i >= 0; --i) {
1033  const int modelIndex = remove.index + i;
1034  if (modelIndex == m_asyncRequestedIndex) {
1035  m_asyncRequestedIndex = -1;
1036  } else if (modelIndex < m_asyncRequestedIndex) {
1037  m_asyncRequestedIndex--;
1038  }
1039  }
1040  }
1041 
1042  Q_FOREACH(const QQmlChangeSet::Change insert, changeSet.inserts()) {
1043 // qDebug() << "ListViewWithPageHeader::onModelUpdated Insert" << insert.index << insert.count;
1044  const bool insertingInValidIndexes = insert.index > m_firstVisibleIndex && insert.index < m_firstVisibleIndex + m_visibleItems.count();
1045  const bool firstItemWithViewOnTop = insert.index == 0 && m_firstVisibleIndex == 0 && m_visibleItems.first()->y() + m_clipItem->y() > contentY();
1046  if (insertingInValidIndexes || firstItemWithViewOnTop)
1047  {
1048  // If the items we are adding won't be really visible
1049  // we grow up instead of down to not change the viewport
1050  bool growUp = false;
1051  if (!firstItemWithViewOnTop) {
1052  for (int i = 0; i < m_visibleItems.count(); ++i) {
1053  if (!m_visibleItems[i]->culled()) {
1054  if (insert.index <= m_firstVisibleIndex + i) {
1055  growUp = true;
1056  }
1057  break;
1058  }
1059  }
1060  }
1061 
1062  const qreal oldFirstValidIndexPos = m_visibleItems.first()->y();
1063  for (int i = insert.count - 1; i >= 0; --i) {
1064  const int modelIndex = insert.index + i;
1065  ListItem *item = createItem(modelIndex, false);
1066  if (growUp) {
1067  ListItem *firstItem = m_visibleItems.first();
1068  firstItem->setY(firstItem->y() - item->height());
1069  }
1070  // Adding an item may break a "same section" chain, so check
1071  // if we need adding a new section item
1072  if (m_sectionDelegate) {
1073  ListItem *nextItem = itemAtIndex(modelIndex + 1);
1074  if (nextItem && !nextItem->sectionItem()) {
1075  nextItem->setSectionItem(getSectionItem(modelIndex + 1, true /* alredy inserted into m_visibleItems*/));
1076  if (growUp && nextItem->sectionItem()) {
1077  ListItem *firstItem = m_visibleItems.first();
1078  firstItem->setY(firstItem->y() - nextItem->sectionItem()->height());
1079  }
1080  }
1081  }
1082  }
1083  if (firstItemWithViewOnTop) {
1084  ListItem *firstItem = m_visibleItems.first();
1085  firstItem->setY(oldFirstValidIndexPos);
1086  }
1087  adjustMinYExtent();
1088  } else if (insert.index <= m_firstVisibleIndex) {
1089  m_firstVisibleIndex += insert.count;
1090  }
1091 
1092  for (int i = insert.count - 1; i >= 0; --i) {
1093  const int modelIndex = insert.index + i;
1094  if (modelIndex <= m_asyncRequestedIndex) {
1095  m_asyncRequestedIndex++;
1096  }
1097  }
1098  }
1099 
1100  Q_FOREACH(const QQmlChangeSet::Change change, changeSet.changes()) {
1101  for (int i = change.start(); i < change.end(); ++i) {
1102  updateSectionItem(i);
1103  }
1104  // Also update the section header for the next item after the change since it may be influenced
1105  updateSectionItem(change.end());
1106  }
1107 
1108  if (m_firstVisibleIndex != oldFirstVisibleIndex) {
1109  if (m_visibleItems.isEmpty()) {
1110  initializeValuesForEmptyList();
1111  } else {
1112  adjustMinYExtent();
1113  }
1114  }
1115 
1116  for (int i = 0; i < m_visibleItems.count(); ++i) {
1117  ListItem *item = m_visibleItems[i];
1118  if (item->sectionItem()) {
1119  item->sectionItem()->setProperty("delegateIndex", m_firstVisibleIndex + i);
1120  }
1121  }
1122 
1123  layout();
1124  polish();
1125  m_contentHeightDirty = true;
1126 }
1127 
1128 void ListViewWithPageHeader::contentYAnimationRunningChanged(bool running)
1129 {
1130  setInteractive(!running);
1131  if (!running) {
1132  m_contentHeightDirty = true;
1133  polish();
1134  }
1135 }
1136 
1137 void ListViewWithPageHeader::itemGeometryChanged(QQuickItem *item, const QRectF &newGeometry, const QRectF &oldGeometry)
1138 {
1139  const qreal heightDiff = newGeometry.height() - oldGeometry.height();
1140  if (heightDiff != 0) {
1141  if (!m_visibleItems.isEmpty()) {
1142  ListItem *firstItem = m_visibleItems.first();
1143  const auto prevFirstItemY = firstItem->y();
1144  if (!m_inContentHeightKeepHeaderShown && oldGeometry.y() + oldGeometry.height() + m_clipItem->y() <= contentY()) {
1145  firstItem->setY(firstItem->y() - heightDiff);
1146  } else if (item == firstItem->sectionItem()) {
1147  firstItem->setY(firstItem->y() + heightDiff);
1148  }
1149 
1150  if (firstItem->y() != prevFirstItemY) {
1151  adjustMinYExtent();
1152  layout();
1153  }
1154  }
1155  refill();
1156  adjustMinYExtent();
1157  polish();
1158  m_contentHeightDirty = true;
1159  }
1160 }
1161 
1162 void ListViewWithPageHeader::itemImplicitHeightChanged(QQuickItem *item)
1163 {
1164  if (item == m_headerItem) {
1165  const qreal diff = m_headerItem->implicitHeight() - m_previousHeaderImplicitHeight;
1166  if (diff != 0) {
1167  adjustHeader(diff);
1168  m_previousHeaderImplicitHeight = m_headerItem->implicitHeight();
1169  layout();
1170  polish();
1171  m_contentHeightDirty = true;
1172  }
1173  }
1174 }
1175 
1176 void ListViewWithPageHeader::headerHeightChanged(qreal newHeaderHeight, qreal oldHeaderHeight, qreal oldHeaderY)
1177 {
1178  const qreal heightDiff = newHeaderHeight - oldHeaderHeight;
1179  if (m_headerItemShownHeight > 0) {
1180  // If the header is shown because of the clip
1181  // Change its size
1182  m_headerItemShownHeight += heightDiff;
1183  m_headerItemShownHeight = qBound(static_cast<qreal>(0.), m_headerItemShownHeight, newHeaderHeight);
1184  updateClipItem();
1185  adjustMinYExtent();
1186  Q_EMIT headerItemShownHeightChanged();
1187  } else {
1188  if (oldHeaderY + oldHeaderHeight > contentY()) {
1189  // If the header is shown because its position
1190  // Change its size
1191  ListItem *firstItem = m_visibleItems.first();
1192  firstItem->setY(firstItem->y() + heightDiff);
1193  layout();
1194  } else {
1195  // If the header is not on screen, just change the start of the list
1196  // so the viewport is not changed
1197  adjustMinYExtent();
1198  }
1199  }
1200 }
1201 
1202 
1203 void ListViewWithPageHeader::adjustMinYExtent()
1204 {
1205  if (m_visibleItems.isEmpty() || (contentHeight() + m_minYExtent < height())) {
1206  m_minYExtent = 0;
1207  } else {
1208  qreal nonCreatedHeight = 0;
1209  if (m_firstVisibleIndex != 0) {
1210  // Calculate the average height of items to estimate the position of the list start
1211  const int visibleItems = m_visibleItems.count();
1212  qreal visibleItemsHeight = 0;
1213  Q_FOREACH(ListItem *item, m_visibleItems) {
1214  visibleItemsHeight += item->height();
1215  }
1216  nonCreatedHeight = m_firstVisibleIndex * visibleItemsHeight / visibleItems;
1217 // qDebug() << m_firstVisibleIndex << visibleItemsHeight << visibleItems << nonCreatedHeight;
1218  }
1219  const qreal headerHeight = (m_headerItem ? m_headerItem->implicitHeight() : 0);
1220  m_minYExtent = nonCreatedHeight - m_visibleItems.first()->y() - m_clipItem->y() + headerHeight;
1221  if (m_minYExtent != 0 && qFuzzyIsNull(m_minYExtent)) {
1222  m_minYExtent = 0;
1223  m_visibleItems.first()->setY(nonCreatedHeight - m_clipItem->y() + headerHeight);
1224  }
1225  }
1226 }
1227 
1228 ListViewWithPageHeader::ListItem *ListViewWithPageHeader::itemAtIndex(int modelIndex) const
1229 {
1230  const int visibleIndexedModelIndex = modelIndex - m_firstVisibleIndex;
1231  if (visibleIndexedModelIndex >= 0 && visibleIndexedModelIndex < m_visibleItems.count())
1232  return m_visibleItems[visibleIndexedModelIndex];
1233 
1234  return nullptr;
1235 }
1236 
1237 void ListViewWithPageHeader::layout()
1238 {
1239  if (m_inLayout)
1240  return;
1241 
1242  m_inLayout = true;
1243  if (!m_visibleItems.isEmpty()) {
1244  const qreal visibleFrom = contentY() - m_clipItem->y() + m_headerItemShownHeight;
1245  const qreal visibleTo = contentY() + height() - m_clipItem->y();
1246 
1247  qreal pos = m_visibleItems.first()->y();
1248 
1249 // qDebug() << "ListViewWithPageHeader::layout Updating positions and heights. contentY" << contentY() << "minYExtent" << minYExtent();
1250  int firstReallyVisibleItem = -1;
1251  int modelIndex = m_firstVisibleIndex;
1252  Q_FOREACH(ListItem *item, m_visibleItems) {
1253  const bool cull = pos + item->height() <= visibleFrom || pos >= visibleTo;
1254  item->setCulled(cull);
1255  item->setY(pos);
1256  if (!cull && firstReallyVisibleItem == -1) {
1257  firstReallyVisibleItem = modelIndex;
1258  if (m_topSectionItem) {
1259  // Positing the top section sticky item is a two step process
1260  // First we set it either we cull it (because it doesn't need to be sticked to the top)
1261  // or stick it to the top
1262  // Then after the loop we'll make sure that if there's another section just below it
1263  // pushed the sticky section up to make it disappear
1264  const qreal topSectionStickPos = m_headerItemShownHeight + contentY() - m_clipItem->y();
1265  bool showStickySectionItem;
1266  // We need to show the "top section sticky item" when the position at the "top" of the
1267  // viewport is bigger than the start of the position of the first visible item
1268  // i.e. the first visible item starts before the viewport, or when the first
1269  // visible item starts just at the viewport start and it does not have its own section item
1270  if (topSectionStickPos > pos) {
1271  showStickySectionItem = true;
1272  } else if (topSectionStickPos == pos) {
1273  showStickySectionItem = !item->sectionItem();
1274  } else {
1275  showStickySectionItem = false;
1276  }
1277  if (!showStickySectionItem) {
1278  QQuickItemPrivate::get(m_topSectionItem)->setCulled(true);
1279  if (item->sectionItem()) {
1280  // This seems it should happen since why would we cull the top section
1281  // if the first visible item has no section header? This only happens briefly
1282  // when increasing the height of a list that is at the bottom, the m_topSectionItem
1283  // gets shown shortly in the next polish call
1284  QQuickItemPrivate::get(item->sectionItem())->setCulled(false);
1285  }
1286  } else {
1287  // Update the top sticky section header
1288  const QString section = m_delegateModel->stringValue(modelIndex, m_sectionProperty);
1289  m_topSectionItem->setProperty("text", section);
1290 
1291  QQuickItemPrivate::get(m_topSectionItem)->setCulled(false);
1292  m_topSectionItem->setY(topSectionStickPos);
1293  int delegateIndex = modelIndex;
1294  // Look for the first index with this section text
1295  while (delegateIndex > 0) {
1296  const QString prevSection = m_delegateModel->stringValue(delegateIndex - 1, m_sectionProperty);
1297  if (prevSection != section)
1298  break;
1299  delegateIndex--;
1300  }
1301  m_topSectionItem->setProperty("delegateIndex", delegateIndex);
1302  if (item->sectionItem()) {
1303  QQuickItemPrivate::get(item->sectionItem())->setCulled(true);
1304  }
1305  }
1306  }
1307  }
1308  const qreal clipFrom = visibleFrom + (!item->sectionItem() && m_topSectionItem && !QQuickItemPrivate::get(m_topSectionItem)->culled ? m_topSectionItem->height() : 0);
1309  if (!cull && pos < clipFrom) {
1310  item->m_item->setProperty("heightToClip", clipFrom - pos);
1311  } else {
1312  item->m_item->setProperty("heightToClip", QVariant::fromValue<int>(0));
1313  }
1314 // qDebug() << "ListViewWithPageHeader::layout" << item->m_item;
1315  pos += item->height();
1316  ++modelIndex;
1317  }
1318 
1319  // Second step of section sticky item positioning
1320  // Look at the next section header, check if it's pushing up the sticky one
1321  if (m_topSectionItem) {
1322  if (firstReallyVisibleItem >= 0) {
1323  for (int i = firstReallyVisibleItem - m_firstVisibleIndex + 1; i < m_visibleItems.count(); ++i) {
1324  ListItem *item = m_visibleItems[i];
1325  if (item->sectionItem()) {
1326  if (m_topSectionItem->y() + m_topSectionItem->height() > item->y()) {
1327  m_topSectionItem->setY(item->y() - m_topSectionItem->height());
1328  }
1329  break;
1330  }
1331  }
1332  }
1333  }
1334  }
1335  m_inLayout = false;
1336 }
1337 
1338 void ListViewWithPageHeader::updatePolish()
1339 {
1340  // Check we are not being taken down and don't paint anything
1341  // TODO Check if we still need this in 5.2
1342  // For reproduction just inifnite loop testDash or testDashContent
1343  if (!QQmlEngine::contextForObject(this)->parentContext())
1344  return;
1345 
1346  Q_FOREACH(ListItem *item, m_itemsToRelease)
1347  reallyReleaseItem(item);
1348  m_itemsToRelease.clear();
1349 
1350  if (!model())
1351  return;
1352 
1353  layout();
1354 
1355  refill();
1356 
1357  if (m_contentHeightDirty) {
1358  qreal contentHeight;
1359  if (m_visibleItems.isEmpty()) {
1360  contentHeight = m_headerItem ? m_headerItem->height() : 0;
1361  } else {
1362  const int modelCount = model()->rowCount();
1363  const int visibleItems = m_visibleItems.count();
1364  const int lastValidIndex = m_firstVisibleIndex + visibleItems - 1;
1365  qreal nonCreatedHeight = 0;
1366  if (lastValidIndex != modelCount - 1) {
1367  const int visibleItems = m_visibleItems.count();
1368  qreal visibleItemsHeight = 0;
1369  Q_FOREACH(ListItem *item, m_visibleItems) {
1370  visibleItemsHeight += item->height();
1371  }
1372  const int unknownSizes = modelCount - (m_firstVisibleIndex + visibleItems);
1373  nonCreatedHeight = unknownSizes * visibleItemsHeight / visibleItems;
1374  }
1375  ListItem *item = m_visibleItems.last();
1376  contentHeight = nonCreatedHeight + item->y() + item->height() + m_clipItem->y();
1377  if (m_firstVisibleIndex != 0) {
1378  // Make sure that if we are shrinking we tell the view we still fit
1379  m_minYExtent = qMax(m_minYExtent, -(contentHeight - height()));
1380  }
1381  }
1382 
1383  m_contentHeightDirty = false;
1384  adjustMinYExtent();
1385  if (contentHeight + m_minYExtent < height()) {
1386  // need this since in the previous call to adjustMinYExtent contentHeight is not set yet
1387  m_minYExtent = 0;
1388  }
1389  m_inContentHeightKeepHeaderShown = m_headerItem && m_headerItem->y() == contentY();
1390  setContentHeight(contentHeight);
1391  m_inContentHeightKeepHeaderShown = false;
1392  }
1393 }
1394 
1395 #include "moc_listviewwithpageheader.cpp"