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