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