on 0896483 help sign in
unity8/plugins/ListViewWithPageHeader/listviewwithpageheader.cpp
613 lines 23 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#include "listviewwithpageheader.h"
19#include <QDebug>
20#include <qqmlinfo.h>
21#include <private/qquickvisualdatamodel_p.h>
22#include <private/qqmlglobal_p.h>
23#include <private/qquickitem_p.h>
24// #include <private/qquickrectangle_p.h>
26ListViewWithPageHeader::ListViewWithPageHeader()
27 : m_delegateModel(nullptr)
28 , m_delegateValidated(false)
29 , m_firstVisibleIndex(-1)
30 , m_minYExtent(0)
31 , m_contentHeightDirty(false)
32 , m_headerItem(nullptr)
33 , m_previousContentY(0)
34 , m_headerItemShownHeight(0)
36 m_clipItem = new QQuickItem(contentItem());
37// m_clipItem = new QQuickRectangle(contentItem());
38// ((QQuickRectangle*)m_clipItem)->setColor(Qt::gray);
40 connect(this, SIGNAL(contentWidthChanged()), this, SLOT(onContentWidthChanged()));
41 connect(this, SIGNAL(contentHeightChanged()), this, SLOT(onContentHeightChanged()));
44ListViewWithPageHeader::~ListViewWithPageHeader()
48QAbstractItemModel *ListViewWithPageHeader::model() const
50 return m_delegateModel ? m_delegateModel->model().value<QAbstractItemModel *>() : nullptr;
53void ListViewWithPageHeader::setModel(QAbstractItemModel *model)
55 if (model != this->model()) {
56 if (!m_delegateModel) {
57 createDelegateModel();
58 } else {
59 disconnect(m_delegateModel, SIGNAL(modelUpdated(QQuickChangeSet,bool)), this, SLOT(onModelUpdated(QQuickChangeSet,bool)));
60 }
61 m_delegateModel->setModel(QVariant::fromValue<QAbstractItemModel *>(model));
62 connect(m_delegateModel, SIGNAL(modelUpdated(QQuickChangeSet,bool)), this, SLOT(onModelUpdated(QQuickChangeSet,bool)));
63 Q_EMIT modelChanged();
64 // TODO?
65// Q_EMIT contentHeightChanged();
66// Q_EMIT contentYChanged();
67 }
70QQmlComponent *ListViewWithPageHeader::delegate() const
72 return m_delegateModel ? m_delegateModel->delegate() : nullptr;
75void ListViewWithPageHeader::setDelegate(QQmlComponent *delegate)
77 if (delegate != this->delegate()) {
78 if (!m_delegateModel) {
79 createDelegateModel();
80 }
82 // Cleanup the existing items
83 foreach(QQuickItem *item, m_visibleItems)
84 releaseItem(item);
85 m_visibleItems.clear();
86 m_firstVisibleIndex = -1;
87 QQuickFlickable::setContentY(0);
88 m_clipItem->setY(0);
90 m_delegateModel->setDelegate(delegate);
92 Q_EMIT delegateChanged();
93 m_delegateValidated = false;
94 m_contentHeightDirty = true;
95 polish();
96 }
99QQuickItem *ListViewWithPageHeader::header() const
101 return m_headerItem;
104void ListViewWithPageHeader::setHeader(QQuickItem *headerItem)
106 if (m_headerItem != headerItem) {
107 qreal oldHeaderHeight = 0;
108 qreal oldHeaderY = 0;
109 if (m_headerItem) {
110 oldHeaderHeight = m_headerItem->height();
111 oldHeaderY = m_headerItem->y();
112 QQuickItemPrivate::get(m_headerItem)->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
113 }
114 m_headerItem = headerItem;
115 if (m_headerItem) {
116 m_headerItem->setParentItem(contentItem());
117 m_headerItem->setZ(1);
118 QQuickItemPrivate::get(m_headerItem)->addItemChangeListener(this, QQuickItemPrivate::Geometry);
119 }
120 qreal newHeaderHeight = m_headerItem ? m_headerItem->height() : 0;
121 if (!m_visibleItems.isEmpty() && newHeaderHeight != oldHeaderHeight) {
122 headerHeightChanged(newHeaderHeight, oldHeaderHeight, oldHeaderY);
123 polish();
124 m_contentHeightDirty = true;
125 }
126 Q_EMIT headerChanged();
127 }
130void ListViewWithPageHeader::positionAtBeginning()
132 qreal headerHeight = (m_headerItem ? m_headerItem->height() : 0);
133 if (m_firstVisibleIndex != 0) {
134 // This could be optimized by trying to reuse the interesection
135 // of items that may end up intersecting between the existing
136 // m_visibleItems and the items we are creating in the next loop
137 foreach(QQuickItem *item, m_visibleItems)
138 releaseItem(item);
139 m_visibleItems.clear();
140 m_firstVisibleIndex = -1;
142 // Create the item 0, it will be already correctly positioned at createItem()
143 m_clipItem->setY(0);
144 QQuickItem *item = createItem(0, false);
145 // Create the subsequent items
146 int modelIndex = 1;
147 qreal pos = item->y() + item->height();
148 const qreal buffer = height() / 2;
149 const qreal bufferTo = height() + buffer;
150 while (modelIndex < m_delegateModel->count() && pos <= bufferTo) {
151 if (!(item = createItem(modelIndex, false)))
152 break;
153 pos += item->height();
154 ++modelIndex;
155 }
157 m_previousContentY = m_visibleItems.first()->y() - headerHeight;
158 }
159 QQuickFlickable::setContentY(m_visibleItems.first()->y() + m_clipItem->y() - headerHeight);
162void ListViewWithPageHeader::showHeader()
164 // TODO
167qreal ListViewWithPageHeader::minYExtent() const
169// qDebug() << "ListViewWithPageHeader::minYExtent" << m_minYExtent;
170 return m_minYExtent;
173void ListViewWithPageHeader::componentComplete()
175 if (m_delegateModel)
176 m_delegateModel->componentComplete();
178 QQuickFlickable::componentComplete();
180 polish();
183void ListViewWithPageHeader::viewportMoved(Qt::Orientations orient)
185 QQuickFlickable::viewportMoved(orient);
186// qDebug() << "ListViewWithPageHeader::viewportMoved" << contentY();
187 qreal diff = m_previousContentY - contentY();
188 if (m_headerItem) {
189 auto oldHeaderItemShownHeight = m_headerItemShownHeight;
190 if (contentY() < -m_minYExtent) {
191 // Stick the header item to the top when dragging down
192 m_headerItem->setY(contentY());
193 } else {
194 // We are going down (but it's not because of the rebound at the end)
195 // (but the header was not shown by it's own position)
196 // or the header is partially shown
197 const bool scrolledUp = m_previousContentY > contentY();
198 const bool notRebounding = contentY() + height() < contentHeight();
199 const bool notShownByItsOwn = contentY() + diff > m_headerItem->y() + m_headerItem->height();
201 if (!scrolledUp && contentY() == -m_minYExtent) {
202 m_headerItemShownHeight = 0;
203 m_headerItem->setY(contentY());
204 } else if ((scrolledUp && notRebounding && notShownByItsOwn) || (m_headerItemShownHeight > 0)) {
205 m_headerItemShownHeight += diff;
206 if (contentY() == -m_minYExtent) {
207 m_headerItemShownHeight = 0;
208 } else {
209 m_headerItemShownHeight = qBound(0., m_headerItemShownHeight, m_headerItem->height());
210 }
211 if (m_headerItemShownHeight > 0) {
212 m_headerItem->setY(contentY() - m_headerItem->height() + m_headerItemShownHeight);
213 } else {
214 m_headerItem->setY(-m_minYExtent);
215 }
216 }
217 }
218 // We will be changing the clip item, need to accomadate for it
219 // otherwise we move the firstItem down/up twice
220 diff += oldHeaderItemShownHeight - m_headerItemShownHeight;
221 }
222 if (!m_visibleItems.isEmpty()) {
223 updateClipItem();
224 QQuickItem *firstItem = m_visibleItems.first();
225 firstItem->setY(firstItem->y() + diff);
226 }
227 m_previousContentY = contentY();
228 polish();
231void ListViewWithPageHeader::setContentY(qreal /*pos*/)
233 qWarning() << "ListViewWithPageHeader::setContentY unsupported feature";
236void ListViewWithPageHeader::createDelegateModel()
238 m_delegateModel = new QQuickVisualDataModel(qmlContext(this), this);
239 connect(m_delegateModel, SIGNAL(createdItem(int,QQuickItem*)), this, SLOT(itemCreated(int,QQuickItem*)));
240 if (isComponentComplete())
241 m_delegateModel->componentComplete();
244void ListViewWithPageHeader::refill()
246 if (!isComponentComplete()) {
247 qWarning() << "Incomplete ListViewWithPageHeader::refill";
248 return;
249 }
251 const qreal buffer = height() / 2;
252 const qreal from = contentY();
253 const qreal to = from + height();
254 const qreal bufferFrom = from - buffer;
255 const qreal bufferTo = to + buffer;
257 bool added = addVisibleItems(from, to, false);
258 bool removed = removeNonVisibleItems(bufferFrom, bufferTo);
259 added |= addVisibleItems(bufferFrom, bufferTo, true);
261 if (added || removed) {
262 m_contentHeightDirty = true;
263 }
266bool ListViewWithPageHeader::addVisibleItems(qreal fillFrom, qreal fillTo, bool asynchronous)
268 if (!delegate())
269 return false;
271 if (m_delegateModel->count() == 0)
272 return false;
274 QQuickItem *item;
275// qDebug() << "ListViewWithPageHeader::addVisibleItems" << fillFrom << fillTo << asynchronous;
277 int modelIndex = 0;
278 qreal pos = 0;
279 if (!m_visibleItems.isEmpty()) {
280 modelIndex = m_firstVisibleIndex + m_visibleItems.count();
281 item = m_visibleItems.last();
282 pos = item->y() + item->height() + m_clipItem->y();
283 }
284 bool changed = false;
285// qDebug() << (modelIndex < m_delegateModel->count()) << pos << fillTo;
286 while (modelIndex < m_delegateModel->count() && pos <= fillTo) {
287// qDebug() << "refill: append item" << modelIndex << "pos" << pos << "asynchronous" << asynchronous;
288 if (!(item = createItem(modelIndex, asynchronous)))
289 break;
290 pos += item->height();
291 ++modelIndex;
292 changed = true;
293 }
295 modelIndex = 0;
296 pos = 0;
297 if (!m_visibleItems.isEmpty()) {
298 modelIndex = m_firstVisibleIndex - 1;
299 item = m_visibleItems.first();
300 pos = item->y() + m_clipItem->y();
301 }
302 while (modelIndex >= 0 && pos > fillFrom) {
303// qDebug() << "refill: prepend item" << modelIndex << "pos" << pos << "fillFrom" << fillFrom << "asynchronous" << asynchronous;
304 if (!(item = createItem(modelIndex, asynchronous)))
305 break;
306 pos -= item->height();
307 --modelIndex;
308 changed = true;
309 }
311 return changed;
314void ListViewWithPageHeader::releaseItem(QQuickItem *item)
316 QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(item);
317 itemPrivate->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
318 QQuickVisualModel::ReleaseFlags flags = m_delegateModel->release(item);
319 if (flags & QQuickVisualModel::Destroyed) {
320 item->setParentItem(nullptr);
321 }
324bool ListViewWithPageHeader::removeNonVisibleItems(qreal bufferFrom, qreal bufferTo)
326// qDebug() << "ListViewWithPageHeader::removeNonVisibleItems" << bufferFrom << bufferTo;
327 bool changed = false;
329 bool foundVisible = false;
330 int i = 0;
331 int removedItems = 0;
332 while (i < m_visibleItems.count()) {
333 QQuickItem *item = m_visibleItems[i];
334 const qreal pos = item->y() + m_clipItem->y();
335// qDebug() << i << pos << (pos + item->height()) << bufferFrom << bufferTo;
336 if (pos + item->height() < bufferFrom || pos > bufferTo) {
337// qDebug() << "Releasing" << i << (pos + item->height() < bufferFrom) << pos + item->height() << bufferFrom << (pos > bufferTo) << pos << bufferTo;
338 releaseItem(item);
339 m_visibleItems.removeAt(i);
340 changed = true;
341 ++removedItems;
342 } else {
343 if (!foundVisible) {
344 foundVisible = true;
345 const int itemIndex = m_firstVisibleIndex + removedItems + i;
346 m_firstVisibleIndex = itemIndex;
347 }
348 ++i;
349 }
350 }
352 return changed;
355QQuickItem *ListViewWithPageHeader::createItem(int modelIndex, bool asynchronous)
357// qDebug() << "CREATE ITEM" << modelIndex;
358 if (asynchronous && m_asyncRequestedIndexes.contains(modelIndex))
359 return nullptr;
361 m_asyncRequestedIndexes.remove(modelIndex);
362 QQuickItem *item = m_delegateModel->item(modelIndex, asynchronous);
363 if (!item) {
364 m_asyncRequestedIndexes << modelIndex;
365 return 0;
366 } else {
367// qDebug() << "ListViewWithPageHeader::createItem::We have the item" << modelIndex << item;
368 QQuickItemPrivate::get(item)->addItemChangeListener(this, QQuickItemPrivate::Geometry);
369 QQuickItem *prevItem = itemAtIndex(modelIndex - 1);
370 if (prevItem) {
371 item->setY(prevItem->y() + prevItem->height());
372 } else {
373 QQuickItem *nextItem = itemAtIndex(modelIndex + 1);
374 if (nextItem) {
375 item->setY(nextItem->y() - item->height());
376 } else if (modelIndex == 0 && m_headerItem) {
377 item->setY(m_headerItem->height());
378 }
379 }
380 QQuickItemPrivate::get(item)->setCulled(item->y() + item->height() + m_clipItem->y() < contentY() || item->y() + m_clipItem->y() >= contentY() + height());
381 if (m_visibleItems.isEmpty()) {
382 m_visibleItems << item;
383 } else {
384 m_visibleItems.insert(modelIndex - m_firstVisibleIndex, item);
385 }
386 if (m_firstVisibleIndex < 0 || modelIndex < m_firstVisibleIndex) {
387 m_firstVisibleIndex = modelIndex;
388 adjustMinYExtent();
389 }
390 return item;
391 }
394void ListViewWithPageHeader::itemCreated(int modelIndex, QQuickItem *item)
396// qDebug() << "ListViewWithPageHeader::itemCreated" << modelIndex << item;
398 item->setParentItem(m_clipItem);
399 if (m_asyncRequestedIndexes.remove(modelIndex)) {
400 createItem(modelIndex, false);
401 }
404void ListViewWithPageHeader::updateClipItem()
406 m_clipItem->setHeight(height() - m_headerItemShownHeight);
407 m_clipItem->setY(contentY() + m_headerItemShownHeight);
408 m_clipItem->setClip(m_headerItemShownHeight > 0);
411void ListViewWithPageHeader::onContentHeightChanged()
413 updateClipItem();
416void ListViewWithPageHeader::onContentWidthChanged()
418 m_clipItem->setWidth(contentItem()->width());
421void ListViewWithPageHeader::onModelUpdated(const QQuickChangeSet &changeSet, bool /*reset*/)
423 // TODO Do something with reset
424// qDebug() << "ListViewWithPageHeader::onModelUpdated" << changeSet << reset;
425 foreach(const QQuickChangeSet::Remove &remove, changeSet.removes()) {
426// qDebug() << "ListViewWithPageHeader::onModelUpdated Remove" << remove;
427 if (remove.index + remove.count > m_firstVisibleIndex && remove.index < m_firstVisibleIndex + m_visibleItems.count()) {
428 const qreal oldFirstValidIndexPos = m_visibleItems.first()->y();
429 // If all the items we are removing are either not created or culled
430 // we have to grow down to avoid viewport changing
431 bool growDown = true;
432 for (int i = 0; growDown && i < remove.count; ++i) {
433 const int modelIndex = remove.index + i;
434 QQuickItem *item = itemAtIndex(modelIndex);
435 if (item && !QQuickItemPrivate::get(item)->culled) {
436 growDown = false;
437 }
438 }
439 for (int i = remove.count - 1; i >= 0; --i) {
440 const int visibleIndex = remove.index + i - m_firstVisibleIndex;
441 if (visibleIndex >= 0 && visibleIndex < m_visibleItems.count()) {
442 QQuickItem *item = m_visibleItems[visibleIndex];
443 releaseItem(item);
444 m_visibleItems.removeAt(visibleIndex);
445 }
446 }
447 if (growDown) {
448 adjustMinYExtent();
449 } else if (remove.index <= m_firstVisibleIndex && !m_visibleItems.isEmpty()) {
450 // We removed the first item that is the one that positions the rest
451 // position the new first item correctly
452 m_visibleItems.first()->setY(oldFirstValidIndexPos);
453 }
454 } else if (remove.index + remove.count <= m_firstVisibleIndex) {
455 m_firstVisibleIndex -= remove.count;
456 }
457 }
459 foreach(const QQuickChangeSet::Insert &insert, changeSet.inserts()) {
460// qDebug() << "ListViewWithPageHeader::onModelUpdated Insert" << insert;
461 const bool insertingInValidIndexes = insert.index > m_firstVisibleIndex && insert.index < m_firstVisibleIndex + m_visibleItems.count();
462 const bool firstItemWithViewOnTop = insert.index == 0 && m_firstVisibleIndex == 0 && m_visibleItems.first()->y() + m_clipItem->y() > contentY();
463 if (insertingInValidIndexes || firstItemWithViewOnTop)
464 {
465 // If the items we are adding won't be really visible
466 // we grow up instead of down to not change the viewport
467 bool growUp = false;
468 if (!firstItemWithViewOnTop) {
469 for (int i = 0; i < m_visibleItems.count(); ++i) {
470 if (!QQuickItemPrivate::get(m_visibleItems[i])->culled) {
471 if (insert.index <= m_firstVisibleIndex + i) {
472 growUp = true;
473 }
474 break;
475 }
476 }
477 }
479 const qreal oldFirstValidIndexPos = m_visibleItems.first()->y();
480 for (int i = insert.count - 1; i >= 0; --i) {
481 const int modelIndex = insert.index + i;
482 QQuickItem *item = createItem(modelIndex, false);
483 if (growUp) {
484 QQuickItem *firstItem = m_visibleItems.first();
485 firstItem->setY(firstItem->y() - item->height());
486 adjustMinYExtent();
487 }
488 }
489 if (firstItemWithViewOnTop) {
490 QQuickItem *firstItem = m_visibleItems.first();
491 firstItem->setY(oldFirstValidIndexPos);
492 }
493 } else if (insert.index <= m_firstVisibleIndex) {
494 m_firstVisibleIndex += insert.count;
495 }
496 }
498 polish();
499 m_contentHeightDirty = true;
502void ListViewWithPageHeader::itemGeometryChanged(QQuickItem *item, const QRectF &newGeometry, const QRectF &oldGeometry)
504 const qreal heightDiff = newGeometry.height() - oldGeometry.height();
505 if (heightDiff != 0) {
506 if (item == m_headerItem) {
507 headerHeightChanged(newGeometry.height(), oldGeometry.height(), oldGeometry.y());
508 } else {
509 if (oldGeometry.y() + oldGeometry.height() + m_clipItem->y() < contentY() && !m_visibleItems.isEmpty()) {
510 item->setY(item->y() - heightDiff);
511 if (m_visibleItems.first() == item) {
512 adjustMinYExtent();
513 }
514 }
515 }
516 polish();
517 m_contentHeightDirty = true;
518 }
521void ListViewWithPageHeader::headerHeightChanged(qreal newHeaderHeight, qreal oldHeaderHeight, qreal oldHeaderY)
523 const qreal heightDiff = newHeaderHeight - oldHeaderHeight;
524 if (m_headerItemShownHeight > 0) {
525 // If the header is shown because of the clip
526 // Change its size
527 m_headerItemShownHeight += heightDiff;
528 m_headerItemShownHeight = qBound(0., m_headerItemShownHeight, newHeaderHeight);
529 updateClipItem();
530 adjustMinYExtent();
531 } else {
532 if (oldHeaderY + oldHeaderHeight > contentY()) {
533 // If the header is shown because its position
534 // Change its size
535 QQuickItem *firstItem = m_visibleItems.first();
536 firstItem->setY(firstItem->y() + heightDiff);
537 } else {
538 // If the header is not on screen, just change the start of the list
539 // so the viewport is not changed
540 adjustMinYExtent();
541 }
542 }
546void ListViewWithPageHeader::adjustMinYExtent()
548 if (m_visibleItems.isEmpty()) {
549 m_minYExtent = 0;
550 } else {
551 m_minYExtent = -m_visibleItems.first()->y() - m_clipItem->y() + (m_headerItem ? m_headerItem->height() : 0);
552 }
555QQuickItem *ListViewWithPageHeader::itemAtIndex(int modelIndex) const
557 const int visibleIndexedModelIndex = modelIndex - m_firstVisibleIndex;
558 if (visibleIndexedModelIndex >= 0 && visibleIndexedModelIndex < m_visibleItems.count())
559 return m_visibleItems[visibleIndexedModelIndex];
561 return nullptr;
564void ListViewWithPageHeader::updatePolish()
566 if (!model())
567 return;
569 if (!m_visibleItems.isEmpty()) {
570 const qreal visibleFrom = contentY() - m_clipItem->y();
571 const qreal visibleTo = contentY() + height() - m_clipItem->y();
573 qreal pos = m_visibleItems.first()->y();
575// qDebug() << "ListViewWithPageHeader::updatePolish Updating positions and heights. contentY" << contentY() << "minYExtent" << minYExtent();
576 foreach(QQuickItem *item, m_visibleItems) {
577 QQuickItemPrivate::get(item)->setCulled(pos + item->height() < visibleFrom || pos >= visibleTo);
578 item->setY(pos);
579// qDebug() << "ListViewWithPageHeader::updatePolish" << /*index << */pos + m_clipItem->y() << /*QQuickItemPrivate::get(item)->culled <<*/ (pos + item->height() < visibleFrom) << (pos > visibleTo);
580 pos += item->height();
581 }
582 }
584 refill();
586 if (m_contentHeightDirty) {
587 qreal contentHeight;
588 const int modelCount = model()->rowCount();
589 const int visibleItems = m_visibleItems.count();
590 const int lastValidIndex = m_firstVisibleIndex + visibleItems - 1;
591 if (lastValidIndex == modelCount - 1) {
592 QQuickItem *item = m_visibleItems.last();
593 contentHeight = item->y() + item->height() + m_clipItem->y();
594 if (m_firstVisibleIndex != 0) {
595 // Make sure that if we are shrinking we tell the view we still fit
596 m_minYExtent = qMax(m_minYExtent, -(contentHeight - height()));
597 }
598 } else {
599 contentHeight = m_headerItem ? m_headerItem->height() : 0;
600 if (!m_visibleItems.isEmpty()) {
601 foreach(QQuickItem *item, m_visibleItems) {
602 contentHeight += item->height();
603 }
604 const int unknownSizes = modelCount - visibleItems;
605 contentHeight += unknownSizes * contentHeight / visibleItems;
606 }
607 }
609 m_contentHeightDirty = false;
610 setContentHeight(contentHeight);
611 }