on 0c91e5b help sign in
unity8/plugins/ListViewWithPageHeader/listviewwithpageheader.cpp
997 lines 38 kB history raw
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 */
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 * Note that minYExtent and height are not always totally accurate, since
74 * we don't have the items created we can't guess their heights
75 * so we can only guarantee the values are correct when the first/last
76 * items of the list are visible, otherwise we just live with good enough
77 * values that make the list scrollable
78 *
79 * There are a few things that are not really implemented or tested properly
80 * which we don't use at the moment like changing the model, changing
81 * the section delegate, having a section delegate that changes its size, etc.
82 * The known missing features are marked with TODOs along the code.
83 */
85#include "listviewwithpageheader.h"
87#include <QDebug>
88#include <qqmlinfo.h>
89#include <qqmlengine.h>
90#include <private/qquickvisualdatamodel_p.h>
91#include <private/qqmlglobal_p.h>
92#include <private/qquickitem_p.h>
93#include <private/qquickanimation_p.h>
94// #include <private/qquickrectangle_p.h>
96qreal ListViewWithPageHeader::ListItem::height() const
98 return m_item->height() + (m_sectionItem ? m_sectionItem->height() : 0);
101qreal ListViewWithPageHeader::ListItem::y() const
103 return m_item->y() - (m_sectionItem ? m_sectionItem->height() : 0);
106void ListViewWithPageHeader::ListItem::setY(qreal newY)
108 if (m_sectionItem) {
109 m_sectionItem->setY(newY);
110 m_item->setY(newY + m_sectionItem->height());
111 } else {
112 m_item->setY(newY);
113 }
116bool ListViewWithPageHeader::ListItem::culled() const
118 return QQuickItemPrivate::get(m_item)->culled;
121void ListViewWithPageHeader::ListItem::setCulled(bool culled)
123 QQuickItemPrivate::get(m_item)->setCulled(culled);
124 if (m_sectionItem)
125 QQuickItemPrivate::get(m_sectionItem)->setCulled(culled);
128ListViewWithPageHeader::ListViewWithPageHeader()
129 : m_delegateModel(nullptr)
130 , m_asyncRequestedIndex(-1)
131 , m_delegateValidated(false)
132 , m_firstVisibleIndex(-1)
133 , m_minYExtent(0)
134 , m_contentHeightDirty(false)
135 , m_headerItem(nullptr)
136 , m_previousContentY(0)
137 , m_headerItemShownHeight(0)
138 , m_sectionDelegate(nullptr)
139 , m_topSectionItem(nullptr)
140 , m_forceNoClip(false)
142 m_clipItem = new QQuickItem(contentItem());
143// m_clipItem = new QQuickRectangle(contentItem());
144// ((QQuickRectangle*)m_clipItem)->setColor(Qt::gray);
146 m_headerShowAnimation = new QQuickNumberAnimation(this);
147 m_headerShowAnimation->setEasing(QEasingCurve::OutQuad);
148 m_headerShowAnimation->setProperty("contentY");
149 m_headerShowAnimation->setDuration(200);
150 m_headerShowAnimation->setTargetObject(this);
152 connect(this, SIGNAL(contentWidthChanged()), this, SLOT(onContentWidthChanged()));
153 connect(this, SIGNAL(contentHeightChanged()), this, SLOT(onContentHeightChanged()));
154 connect(this, SIGNAL(heightChanged()), this, SLOT(onHeightChanged()));
157ListViewWithPageHeader::~ListViewWithPageHeader()
161QAbstractItemModel *ListViewWithPageHeader::model() const
163 return m_delegateModel ? m_delegateModel->model().value<QAbstractItemModel *>() : nullptr;
166void ListViewWithPageHeader::setModel(QAbstractItemModel *model)
168 if (model != this->model()) {
169 if (!m_delegateModel) {
170 createDelegateModel();
171 } else {
172 disconnect(m_delegateModel, SIGNAL(modelUpdated(QQuickChangeSet,bool)), this, SLOT(onModelUpdated(QQuickChangeSet,bool)));
173 }
174 m_delegateModel->setModel(QVariant::fromValue<QAbstractItemModel *>(model));
175 connect(m_delegateModel, SIGNAL(modelUpdated(QQuickChangeSet,bool)), this, SLOT(onModelUpdated(QQuickChangeSet,bool)));
176 Q_EMIT modelChanged();
177 // TODO?
178// Q_EMIT contentHeightChanged();
179// Q_EMIT contentYChanged();
180 }
183QQmlComponent *ListViewWithPageHeader::delegate() const
185 return m_delegateModel ? m_delegateModel->delegate() : nullptr;
188void ListViewWithPageHeader::setDelegate(QQmlComponent *delegate)
190 if (delegate != this->delegate()) {
191 if (!m_delegateModel) {
192 createDelegateModel();
193 }
195 // Cleanup the existing items
196 foreach(ListItem *item, m_visibleItems)
197 releaseItem(item);
198 m_visibleItems.clear();
199 m_firstVisibleIndex = -1;
200 adjustMinYExtent();
201 setContentY(0);
202 m_clipItem->setY(0);
204 m_delegateModel->setDelegate(delegate);
206 Q_EMIT delegateChanged();
207 m_delegateValidated = false;
208 m_contentHeightDirty = true;
209 polish();
210 }
213QQuickItem *ListViewWithPageHeader::header() const
215 return m_headerItem;
218void ListViewWithPageHeader::setHeader(QQuickItem *headerItem)
220 if (m_headerItem != headerItem) {
221 qreal oldHeaderHeight = 0;
222 qreal oldHeaderY = 0;
223 if (m_headerItem) {
224 oldHeaderHeight = m_headerItem->height();
225 oldHeaderY = m_headerItem->y();
226 QQuickItemPrivate::get(m_headerItem)->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
227 m_headerItem->setParentItem(nullptr);
228 }
229 m_headerItem = headerItem;
230 if (m_headerItem) {
231 m_headerItem->setParentItem(contentItem());
232 m_headerItem->setZ(1);
233 QQuickItemPrivate::get(m_headerItem)->addItemChangeListener(this, QQuickItemPrivate::Geometry);
234 }
235 qreal newHeaderHeight = m_headerItem ? m_headerItem->height() : 0;
236 if (!m_visibleItems.isEmpty() && newHeaderHeight != oldHeaderHeight) {
237 headerHeightChanged(newHeaderHeight, oldHeaderHeight, oldHeaderY);
238 polish();
239 m_contentHeightDirty = true;
240 }
241 Q_EMIT headerChanged();
242 }
245QQmlComponent *ListViewWithPageHeader::sectionDelegate() const
247 return m_sectionDelegate;
250void ListViewWithPageHeader::setSectionDelegate(QQmlComponent *delegate)
252 if (delegate != m_sectionDelegate) {
253 // TODO clean existing sections
255 m_sectionDelegate = delegate;
257 m_topSectionItem = getSectionItem(QString());
258 m_topSectionItem->setZ(3);
259 QQuickItemPrivate::get(m_topSectionItem)->setCulled(true);
261 // TODO create sections for existing items
263 Q_EMIT sectionDelegateChanged();
264 }
267QString ListViewWithPageHeader::sectionProperty() const
269 return m_sectionProperty;
272void ListViewWithPageHeader::setSectionProperty(const QString &property)
274 if (property != m_sectionProperty) {
275 m_sectionProperty = property;
277 updateWatchedRoles();
279 // TODO recreate sections
281 Q_EMIT sectionPropertyChanged();
282 }
285bool ListViewWithPageHeader::forceNoClip() const
287 return m_forceNoClip;
290void ListViewWithPageHeader::setForceNoClip(bool noClip)
292 if (noClip != m_forceNoClip) {
293 m_forceNoClip = noClip;
294 updateClipItem();
295 Q_EMIT forceNoClipChanged();
296 }
299void ListViewWithPageHeader::positionAtBeginning()
301 qreal headerHeight = (m_headerItem ? m_headerItem->height() : 0);
302 if (m_firstVisibleIndex != 0) {
303 // This could be optimized by trying to reuse the interesection
304 // of items that may end up intersecting between the existing
305 // m_visibleItems and the items we are creating in the next loop
306 foreach(ListItem *item, m_visibleItems)
307 releaseItem(item);
308 m_visibleItems.clear();
309 m_firstVisibleIndex = -1;
311 // Create the item 0, it will be already correctly positioned at createItem()
312 m_clipItem->setY(0);
313 ListItem *item = createItem(0, false);
314 // Create the subsequent items
315 int modelIndex = 1;
316 qreal pos = item->y() + item->height();
317 const qreal buffer = height() / 2;
318 const qreal bufferTo = height() + buffer;
319 while (modelIndex < m_delegateModel->count() && pos <= bufferTo) {
320 if (!(item = createItem(modelIndex, false)))
321 break;
322 pos += item->height();
323 ++modelIndex;
324 }
326 m_previousContentY = m_visibleItems.first()->y() - headerHeight;
327 }
328 setContentY(m_visibleItems.first()->y() + m_clipItem->y() - headerHeight);
331void ListViewWithPageHeader::showHeader()
333 auto to = qMax(-minYExtent(), contentY() - m_headerItem->height() + m_headerItemShownHeight);
334 if (to != contentY()) {
335 m_headerShowAnimation->setTo(to);
336 m_headerShowAnimation->start();
337 }
340qreal ListViewWithPageHeader::minYExtent() const
342// qDebug() << "ListViewWithPageHeader::minYExtent" << m_minYExtent;
343 return m_minYExtent;
346void ListViewWithPageHeader::componentComplete()
348 if (m_delegateModel)
349 m_delegateModel->componentComplete();
351 QQuickFlickable::componentComplete();
353 polish();
356void ListViewWithPageHeader::viewportMoved(Qt::Orientations orient)
358 QQuickFlickable::viewportMoved(orient);
359// qDebug() << "ListViewWithPageHeader::viewportMoved" << contentY();
360 qreal diff = m_previousContentY - contentY();
361 if (m_headerItem) {
362 auto oldHeaderItemShownHeight = m_headerItemShownHeight;
363 if (contentY() < -m_minYExtent) {
364 // Stick the header item to the top when dragging down
365 m_headerItem->setY(contentY());
366 } else {
367 // We are going down (but it's not because of the rebound at the end)
368 // (but the header was not shown by it's own position)
369 // or the header is partially shown
370 const bool scrolledUp = m_previousContentY > contentY();
371 const bool notRebounding = contentY() + height() < contentHeight();
372 const bool notShownByItsOwn = contentY() + diff > m_headerItem->y() + m_headerItem->height();
374 if (!scrolledUp && contentY() == -m_minYExtent) {
375 m_headerItemShownHeight = 0;
376 m_headerItem->setY(contentY());
377 } else if ((scrolledUp && notRebounding && notShownByItsOwn) || (m_headerItemShownHeight > 0)) {
378 m_headerItemShownHeight += diff;
379 if (contentY() == -m_minYExtent) {
380 m_headerItemShownHeight = 0;
381 } else {
382 m_headerItemShownHeight = qBound(static_cast<qreal>(0.), m_headerItemShownHeight, m_headerItem->height());
383 }
384 if (m_headerItemShownHeight > 0) {
385 m_headerItem->setY(contentY() - m_headerItem->height() + m_headerItemShownHeight);
386 } else {
387 m_headerItem->setY(-m_minYExtent);
388 }
389 }
390 }
391 // We will be changing the clip item, need to accomadate for it
392 // otherwise we move the firstItem down/up twice (unless the
393 // show header animation is running, where we want to keep the viewport stable)
394 if (!m_headerShowAnimation->isRunning())
395 diff += oldHeaderItemShownHeight - m_headerItemShownHeight;
396 else
397 diff = -diff;
398 }
399 if (!m_visibleItems.isEmpty()) {
400 updateClipItem();
401 ListItem *firstItem = m_visibleItems.first();
402 firstItem->setY(firstItem->y() + diff);
403 if (m_headerShowAnimation->isRunning()) {
404 adjustMinYExtent();
405 }
406 }
408 m_previousContentY = contentY();
409 layout();
410 polish();
413void ListViewWithPageHeader::createDelegateModel()
415 m_delegateModel = new QQuickVisualDataModel(qmlContext(this), this);
416 connect(m_delegateModel, SIGNAL(createdItem(int,QQuickItem*)), this, SLOT(itemCreated(int,QQuickItem*)));
417 if (isComponentComplete())
418 m_delegateModel->componentComplete();
419 updateWatchedRoles();
422void ListViewWithPageHeader::refill()
424 if (!isComponentComplete()) {
425 return;
426 }
428 const qreal buffer = height() / 2;
429 const qreal from = contentY();
430 const qreal to = from + height();
431 const qreal bufferFrom = from - buffer;
432 const qreal bufferTo = to + buffer;
434 bool added = addVisibleItems(from, to, false);
435 bool removed = removeNonVisibleItems(bufferFrom, bufferTo);
436 added |= addVisibleItems(bufferFrom, bufferTo, true);
438 if (added || removed) {
439 m_contentHeightDirty = true;
440 }
443bool ListViewWithPageHeader::addVisibleItems(qreal fillFrom, qreal fillTo, bool asynchronous)
445 if (!delegate())
446 return false;
448 if (m_delegateModel->count() == 0)
449 return false;
451 ListItem *item;
452// qDebug() << "ListViewWithPageHeader::addVisibleItems" << fillFrom << fillTo << asynchronous;
454 int modelIndex = 0;
455 qreal pos = 0;
456 if (!m_visibleItems.isEmpty()) {
457 modelIndex = m_firstVisibleIndex + m_visibleItems.count();
458 item = m_visibleItems.last();
459 pos = item->y() + item->height() + m_clipItem->y();
460 }
461 bool changed = false;
462// qDebug() << (modelIndex < m_delegateModel->count()) << pos << fillTo;
463 while (modelIndex < m_delegateModel->count() && pos <= fillTo) {
464// qDebug() << "refill: append item" << modelIndex << "pos" << pos << "asynchronous" << asynchronous;
465 if (!(item = createItem(modelIndex, asynchronous)))
466 break;
467 pos += item->height();
468 ++modelIndex;
469 changed = true;
470 }
472 modelIndex = 0;
473 pos = 0;
474 if (!m_visibleItems.isEmpty()) {
475 modelIndex = m_firstVisibleIndex - 1;
476 item = m_visibleItems.first();
477 pos = item->y() + m_clipItem->y();
478 }
479 while (modelIndex >= 0 && pos > fillFrom) {
480// qDebug() << "refill: prepend item" << modelIndex << "pos" << pos << "fillFrom" << fillFrom << "asynchronous" << asynchronous;
481 if (!(item = createItem(modelIndex, asynchronous)))
482 break;
483 pos -= item->height();
484 --modelIndex;
485 changed = true;
486 }
488 return changed;
491void ListViewWithPageHeader::releaseItem(ListItem *listItem)
493 QQuickItem *item = listItem->m_item;
494 QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(item);
495 itemPrivate->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
496 QQuickVisualModel::ReleaseFlags flags = m_delegateModel->release(item);
497 if (flags & QQuickVisualModel::Destroyed) {
498 item->setParentItem(nullptr);
499 }
500 delete listItem->m_sectionItem;
501 delete listItem;
504void ListViewWithPageHeader::updateWatchedRoles()
506 if (m_delegateModel) {
507 QList<QByteArray> roles;
508 if (!m_sectionProperty.isEmpty())
509 roles << m_sectionProperty.toUtf8();
510 m_delegateModel->setWatchedRoles(roles);
511 }
514QQuickItem *ListViewWithPageHeader::getSectionItem(int modelIndex, bool alreadyInserted)
516 if (!m_sectionDelegate)
517 return nullptr;
519 const QString section = m_delegateModel->stringValue(modelIndex, m_sectionProperty);
520 if (modelIndex > 0) {
521 const QString prevSection = m_delegateModel->stringValue(modelIndex - 1, m_sectionProperty);
522 if (section == prevSection)
523 return nullptr;
524 }
525 if (modelIndex + 1 < model()->rowCount() && !alreadyInserted) {
526 // Already inserted items can't steal next section header
527 const QString nextSection = m_delegateModel->stringValue(modelIndex + 1, m_sectionProperty);
528 if (section == nextSection) {
529 // Steal the section header
530 ListItem *nextItem = itemAtIndex(modelIndex); // Not +1 since not yet inserted into m_visibleItems
531 if (nextItem) {
532 QQuickItem *sectionItem = nextItem->m_sectionItem;
533 nextItem->m_sectionItem = nullptr;
534 return sectionItem;
535 }
536 }
537 }
539 return getSectionItem(section);
542QQuickItem *ListViewWithPageHeader::getSectionItem(const QString &sectionText)
544 QQuickItem *sectionItem = nullptr;
546 QQmlContext *creationContext = m_sectionDelegate->creationContext();
547 QQmlContext *context = new QQmlContext(creationContext ? creationContext : qmlContext(this));
548 context->setContextProperty(QLatin1String("section"), sectionText);
549 QObject *nobj = m_sectionDelegate->beginCreate(context);
550 if (nobj) {
551 QQml_setParent_noEvent(context, nobj);
552 sectionItem = qobject_cast<QQuickItem *>(nobj);
553 if (!sectionItem) {
554 delete nobj;
555 } else {
556 sectionItem->setZ(2);
557 QQml_setParent_noEvent(sectionItem, m_clipItem);
558 sectionItem->setParentItem(m_clipItem);
559 }
560 } else {
561 delete context;
562 }
563 m_sectionDelegate->completeCreate();
565 // TODO attach to sectionItem so we can accomodate to it changing its height
567 return sectionItem;
570bool ListViewWithPageHeader::removeNonVisibleItems(qreal bufferFrom, qreal bufferTo)
572// qDebug() << "ListViewWithPageHeader::removeNonVisibleItems" << bufferFrom << bufferTo;
573 bool changed = false;
575 bool foundVisible = false;
576 int i = 0;
577 int removedItems = 0;
578 const auto oldFirstVisibleIndex = m_firstVisibleIndex;
579 while (i < m_visibleItems.count()) {
580 ListItem *item = m_visibleItems[i];
581 const qreal pos = item->y() + m_clipItem->y();
582// qDebug() << i << pos << (pos + item->height()) << bufferFrom << bufferTo;
583 if (pos + item->height() < bufferFrom || pos > bufferTo) {
584// qDebug() << "Releasing" << i << (pos + item->height() < bufferFrom) << pos + item->height() << bufferFrom << (pos > bufferTo) << pos << bufferTo;
585 releaseItem(item);
586 m_visibleItems.removeAt(i);
587 changed = true;
588 ++removedItems;
589 } else {
590 if (!foundVisible) {
591 foundVisible = true;
592 const int itemIndex = m_firstVisibleIndex + removedItems + i;
593 m_firstVisibleIndex = itemIndex;
594 }
595 ++i;
596 }
597 }
598 if (m_firstVisibleIndex != oldFirstVisibleIndex) {
599 adjustMinYExtent();
600 }
602 return changed;
605ListViewWithPageHeader::ListItem *ListViewWithPageHeader::createItem(int modelIndex, bool asynchronous)
607// qDebug() << "CREATE ITEM" << modelIndex;
608 if (asynchronous && m_asyncRequestedIndex != -1)
609 return nullptr;
611 m_asyncRequestedIndex = -1;
612 QQuickItem *item = m_delegateModel->item(modelIndex, asynchronous);
613 if (!item) {
614 m_asyncRequestedIndex = modelIndex;
615 return 0;
616 } else {
617// qDebug() << "ListViewWithPageHeader::createItem::We have the item" << modelIndex << item;
618 ListItem *listItem = new ListItem;
619 listItem->m_item = item;
620 listItem->m_sectionItem = getSectionItem(modelIndex, false /*Not yet inserted into m_visibleItems*/);
621 QQuickItemPrivate::get(item)->addItemChangeListener(this, QQuickItemPrivate::Geometry);
622 ListItem *prevItem = itemAtIndex(modelIndex - 1);
623 bool lostItem = false; // Is an item that we requested async but because of model changes
624 // it is no longer attached to any of the existing items (has no prev nor next item)
625 // nor is the first item
626 if (prevItem) {
627 listItem->setY(prevItem->y() + prevItem->height());
628 } else {
629 ListItem *nextItem = itemAtIndex(modelIndex + 1);
630 if (nextItem) {
631 listItem->setY(nextItem->y() - listItem ->height());
632 } else if (modelIndex == 0 && m_headerItem) {
633 listItem->setY(m_headerItem->height());
634 } else if (!m_visibleItems.isEmpty()) {
635 lostItem = true;
636 }
637 }
638 if (lostItem) {
639 releaseItem(listItem);
640 listItem = nullptr;
641 } else {
642 listItem->setCulled(listItem->y() + listItem->height() + m_clipItem->y() < contentY() || listItem->y() + m_clipItem->y() >= contentY() + height());
643 if (m_visibleItems.isEmpty()) {
644 m_visibleItems << listItem;
645 } else {
646 m_visibleItems.insert(modelIndex - m_firstVisibleIndex, listItem);
647 }
648 if (m_firstVisibleIndex < 0 || modelIndex < m_firstVisibleIndex) {
649 m_firstVisibleIndex = modelIndex;
650 adjustMinYExtent();
651 }
652 }
653 return listItem;
654 }
657void ListViewWithPageHeader::itemCreated(int modelIndex, QQuickItem *item)
659// qDebug() << "ListViewWithPageHeader::itemCreated" << modelIndex << item;
661 item->setParentItem(m_clipItem);
662 QQmlContext *context = QQmlEngine::contextForObject(item)->parentContext();
663 context->setContextProperty(QLatin1String("ListViewWithPageHeader"), this);
664 if (modelIndex == m_asyncRequestedIndex) {
665 createItem(modelIndex, false);
666 }
669void ListViewWithPageHeader::updateClipItem()
671 m_clipItem->setHeight(height() - m_headerItemShownHeight);
672 m_clipItem->setY(contentY() + m_headerItemShownHeight);
673 m_clipItem->setClip(!m_forceNoClip && m_headerItemShownHeight > 0);
676void ListViewWithPageHeader::onContentHeightChanged()
678 updateClipItem();
681void ListViewWithPageHeader::onContentWidthChanged()
683 m_clipItem->setWidth(contentItem()->width());
686void ListViewWithPageHeader::onHeightChanged()
688 setMaximumFlickVelocity(height() * 10);
689 setFlickDeceleration(height() * 2);
692void ListViewWithPageHeader::onModelUpdated(const QQuickChangeSet &changeSet, bool /*reset*/)
694 // TODO Do something with reset
695// qDebug() << "ListViewWithPageHeader::onModelUpdated" << changeSet << reset;
696 const auto oldFirstVisibleIndex = m_firstVisibleIndex;
698 foreach(const QQuickChangeSet::Remove &remove, changeSet.removes()) {
699// qDebug() << "ListViewWithPageHeader::onModelUpdated Remove" << remove;
700 if (remove.index + remove.count > m_firstVisibleIndex && remove.index < m_firstVisibleIndex + m_visibleItems.count()) {
701 const qreal oldFirstValidIndexPos = m_visibleItems.first()->y();
702 // If all the items we are removing are either not created or culled
703 // we have to grow down to avoid viewport changing
704 bool growDown = true;
705 for (int i = 0; growDown && i < remove.count; ++i) {
706 const int modelIndex = remove.index + i;
707 ListItem *item = itemAtIndex(modelIndex);
708 if (item && !item->culled()) {
709 growDown = false;
710 }
711 }
712 for (int i = remove.count - 1; i >= 0; --i) {
713 const int visibleIndex = remove.index + i - m_firstVisibleIndex;
714 if (visibleIndex >= 0 && visibleIndex < m_visibleItems.count()) {
715 ListItem *item = m_visibleItems[visibleIndex];
716 // Pass the section item down if needed
717 if (item->m_sectionItem && visibleIndex + 1 < m_visibleItems.count()) {
718 ListItem *nextItem = m_visibleItems[visibleIndex + 1];
719 if (!nextItem->m_sectionItem) {
720 nextItem->m_sectionItem = item->m_sectionItem;
721 item->m_sectionItem = nullptr;
722 }
723 }
724 releaseItem(item);
725 m_visibleItems.removeAt(visibleIndex);
726 }
727 }
728 if (growDown) {
729 adjustMinYExtent();
730 } else if (remove.index <= m_firstVisibleIndex && !m_visibleItems.isEmpty()) {
731 // We removed the first item that is the one that positions the rest
732 // position the new first item correctly
733 m_visibleItems.first()->setY(oldFirstValidIndexPos);
734 }
735 } else if (remove.index + remove.count <= m_firstVisibleIndex) {
736 m_firstVisibleIndex -= remove.count;
737 }
738 for (int i = remove.count - 1; i >= 0; --i) {
739 const int modelIndex = remove.index + i;
740 if (modelIndex == m_asyncRequestedIndex) {
741 m_asyncRequestedIndex = -1;
742 } else if (modelIndex < m_asyncRequestedIndex) {
743 m_asyncRequestedIndex--;
744 }
745 }
746 }
748 foreach(const QQuickChangeSet::Insert &insert, changeSet.inserts()) {
749// qDebug() << "ListViewWithPageHeader::onModelUpdated Insert" << insert.index << insert.count;
750 const bool insertingInValidIndexes = insert.index > m_firstVisibleIndex && insert.index < m_firstVisibleIndex + m_visibleItems.count();
751 const bool firstItemWithViewOnTop = insert.index == 0 && m_firstVisibleIndex == 0 && m_visibleItems.first()->y() + m_clipItem->y() > contentY();
752 if (insertingInValidIndexes || firstItemWithViewOnTop)
753 {
754 // If the items we are adding won't be really visible
755 // we grow up instead of down to not change the viewport
756 bool growUp = false;
757 if (!firstItemWithViewOnTop) {
758 for (int i = 0; i < m_visibleItems.count(); ++i) {
759 if (!m_visibleItems[i]->culled()) {
760 if (insert.index <= m_firstVisibleIndex + i) {
761 growUp = true;
762 }
763 break;
764 }
765 }
766 }
768 const qreal oldFirstValidIndexPos = m_visibleItems.first()->y();
769 for (int i = insert.count - 1; i >= 0; --i) {
770 const int modelIndex = insert.index + i;
771 ListItem *item = createItem(modelIndex, false);
772 if (growUp) {
773 ListItem *firstItem = m_visibleItems.first();
774 firstItem->setY(firstItem->y() - item->height());
775 adjustMinYExtent();
776 }
777 // Adding an item may break a "same section" chain, so check
778 // if we need adding a new section item
779 if (m_sectionDelegate) {
780 ListItem *nextItem = itemAtIndex(modelIndex + 1);
781 if (nextItem && !nextItem->m_sectionItem) {
782 nextItem->m_sectionItem = getSectionItem(modelIndex + 1, true /* alredy inserted into m_visibleItems*/);
783 if (growUp) {
784 ListItem *firstItem = m_visibleItems.first();
785 firstItem->setY(firstItem->y() - nextItem->m_sectionItem->height());
786 adjustMinYExtent();
787 }
788 }
789 }
790 }
791 if (firstItemWithViewOnTop) {
792 ListItem *firstItem = m_visibleItems.first();
793 firstItem->setY(oldFirstValidIndexPos);
794 }
795 } else if (insert.index <= m_firstVisibleIndex) {
796 m_firstVisibleIndex += insert.count;
797 }
799 for (int i = insert.count - 1; i >= 0; --i) {
800 const int modelIndex = insert.index + i;
801 if (modelIndex <= m_asyncRequestedIndex) {
802 m_asyncRequestedIndex++;
803 }
804 }
805 }
807 if (m_firstVisibleIndex != oldFirstVisibleIndex) {
808 adjustMinYExtent();
809 }
811 layout();
812 polish();
813 m_contentHeightDirty = true;
816void ListViewWithPageHeader::itemGeometryChanged(QQuickItem *item, const QRectF &newGeometry, const QRectF &oldGeometry)
818 const qreal heightDiff = newGeometry.height() - oldGeometry.height();
819 if (heightDiff != 0) {
820 if (item == m_headerItem) {
821 headerHeightChanged(newGeometry.height(), oldGeometry.height(), oldGeometry.y());
822 } else {
823 if (oldGeometry.y() + oldGeometry.height() + m_clipItem->y() < contentY() && !m_visibleItems.isEmpty()) {
824 ListItem *firstItem = m_visibleItems.first();
825 firstItem->setY(firstItem->y() - heightDiff);
826 adjustMinYExtent();
827 layout();
828 }
829 }
830 polish();
831 m_contentHeightDirty = true;
832 }
835void ListViewWithPageHeader::headerHeightChanged(qreal newHeaderHeight, qreal oldHeaderHeight, qreal oldHeaderY)
837 const qreal heightDiff = newHeaderHeight - oldHeaderHeight;
838 if (m_headerItemShownHeight > 0) {
839 // If the header is shown because of the clip
840 // Change its size
841 m_headerItemShownHeight += heightDiff;
842 m_headerItemShownHeight = qBound(static_cast<qreal>(0.), m_headerItemShownHeight, newHeaderHeight);
843 updateClipItem();
844 adjustMinYExtent();
845 } else {
846 if (oldHeaderY + oldHeaderHeight > contentY()) {
847 // If the header is shown because its position
848 // Change its size
849 ListItem *firstItem = m_visibleItems.first();
850 firstItem->setY(firstItem->y() + heightDiff);
851 layout();
852 } else {
853 // If the header is not on screen, just change the start of the list
854 // so the viewport is not changed
855 adjustMinYExtent();
856 }
857 }
861void ListViewWithPageHeader::adjustMinYExtent()
863 if (m_visibleItems.isEmpty()) {
864 m_minYExtent = 0;
865 } else {
866 qreal nonCreatedHeight = 0;
867 if (m_firstVisibleIndex != 0) {
868 // Calculate the average height of items to estimate the position of the list start
869 const int visibleItems = m_visibleItems.count();
870 qreal visibleItemsHeight = 0;
871 foreach(ListItem *item, m_visibleItems) {
872 visibleItemsHeight += item->height();
873 }
874 nonCreatedHeight = m_firstVisibleIndex * visibleItemsHeight / visibleItems;
875// qDebug() << m_firstVisibleIndex << visibleItemsHeight << visibleItems << nonCreatedHeight;
876 }
877 m_minYExtent = nonCreatedHeight - m_visibleItems.first()->y() - m_clipItem->y() + (m_headerItem ? m_headerItem->height() : 0);
878 }
881ListViewWithPageHeader::ListItem *ListViewWithPageHeader::itemAtIndex(int modelIndex) const
883 const int visibleIndexedModelIndex = modelIndex - m_firstVisibleIndex;
884 if (visibleIndexedModelIndex >= 0 && visibleIndexedModelIndex < m_visibleItems.count())
885 return m_visibleItems[visibleIndexedModelIndex];
887 return nullptr;
890void ListViewWithPageHeader::layout()
892 if (!m_visibleItems.isEmpty()) {
893 const qreal visibleFrom = contentY() - m_clipItem->y() + m_headerItemShownHeight;
894 const qreal visibleTo = contentY() + height() - m_clipItem->y();
896 qreal pos = m_visibleItems.first()->y();
898// qDebug() << "ListViewWithPageHeader::layout Updating positions and heights. contentY" << contentY() << "minYExtent" << minYExtent();
899 int firstReallyVisibleItem = -1;
900 int modelIndex = m_firstVisibleIndex;
901 foreach(ListItem *item, m_visibleItems) {
902 const bool cull = pos + item->height() < visibleFrom || pos >= visibleTo;
903 item->setCulled(cull);
904 item->setY(pos);
905 if (!cull && firstReallyVisibleItem == -1) {
906 firstReallyVisibleItem = modelIndex;
907 if (m_topSectionItem) {
908 // Update the top sticky section header
909 const QString section = m_delegateModel->stringValue(modelIndex, m_sectionProperty);
910 QQmlContext *context = QQmlEngine::contextForObject(m_topSectionItem)->parentContext();
911 context->setContextProperty(QLatin1String("section"), section);
913 // Positing the top section sticky item is a two step process
914 // First we set it either we cull it (because it doesn't need to be sticked to the top)
915 // or stick it to the top
916 // Then after the loop we'll make sure that if there's another section just below it
917 // pushed the sticky section up to make it disappear
918 const qreal topSectionStickPos = m_headerItemShownHeight + contentY() - m_clipItem->y();
919 if (topSectionStickPos <= pos) {
920 QQuickItemPrivate::get(m_topSectionItem)->setCulled(true);
921 QQuickItemPrivate::get(item->m_sectionItem)->setCulled(false);
922 } else {
923 QQuickItemPrivate::get(m_topSectionItem)->setCulled(false);
924 m_topSectionItem->setY(topSectionStickPos);
925 if (item->m_sectionItem) {
926 QQuickItemPrivate::get(item->m_sectionItem)->setCulled(true);
927 }
928 }
929 }
930 }
931 QQmlContext *context = QQmlEngine::contextForObject(item->m_item)->parentContext();
932 if (!cull && pos < visibleFrom) {
933 context->setContextProperty(QLatin1String("heightToClip"), visibleFrom - pos);
934 } else {
935 context->setContextProperty(QLatin1String("heightToClip"), QVariant::fromValue<int>(0));
936 }
937// qDebug() << "ListViewWithPageHeader::layout" << item->m_item;
938 pos += item->height();
939 ++modelIndex;
940 }
942 // Second step of section sticky item positioning
943 // Look at the next section header, check if it's pushing up the sticky one
944 if (m_topSectionItem) {
945 if (firstReallyVisibleItem >= 0) {
946 for (int i = firstReallyVisibleItem - m_firstVisibleIndex + 1; i < m_visibleItems.count(); ++i) {
947 ListItem *item = m_visibleItems[i];
948 if (item->m_sectionItem) {
949 if (m_topSectionItem->y() + m_topSectionItem->height() > item->y()) {
950 m_topSectionItem->setY(item->y() - m_topSectionItem->height());
951 }
952 break;
953 }
954 }
955 }
956 }
957 }
960void ListViewWithPageHeader::updatePolish()
962 if (!model())
963 return;
965 layout();
967 refill();
969 if (m_contentHeightDirty) {
970 qreal contentHeight;
971 const int modelCount = model()->rowCount();
972 const int visibleItems = m_visibleItems.count();
973 const int lastValidIndex = m_firstVisibleIndex + visibleItems - 1;
974 if (lastValidIndex == modelCount - 1) {
975 ListItem *item = m_visibleItems.last();
976 contentHeight = item->y() + item->height() + m_clipItem->y();
977 if (m_firstVisibleIndex != 0) {
978 // Make sure that if we are shrinking we tell the view we still fit
979 m_minYExtent = qMax(m_minYExtent, -(contentHeight - height()));
980 }
981 } else {
982 contentHeight = m_headerItem ? m_headerItem->height() : 0;
983 if (!m_visibleItems.isEmpty()) {
984 foreach(ListItem *item, m_visibleItems) {
985 contentHeight += item->height();
986 }
987 const int unknownSizes = modelCount - visibleItems;
988 contentHeight += unknownSizes * contentHeight / visibleItems;
989 }
990 }
992 m_contentHeightDirty = false;
993 setContentHeight(contentHeight);
994 }
997#include "moc_listviewwithpageheader.cpp"