on xenial help sign in
Start to use the C++ ListViewWithPageHeader
commit0896483acfdd74c86c4507c3cc999186b83cf709 authorAlbert Astals <albert.astals@canonical.com> dateFri 7 Jun 2013, 15:52 (13 years ago) parentdc7b34f
15 files changed+867−221
Components/Carousel.qml+1 Components/ListViewWithPageHeader.qml−189 Dash/DashApps.qml+6−5 Dash/DashHome.qml+7−6 Dash/DashMusic.qml+6−5 Dash/DashPeople.qml+6−5 Dash/DashVideos.qml+7−6 Dash/GenericLensView.qml+6−5 plugins/CMakeLists.txt+1 plugins/ListViewWithPageHeader/CMakeLists.txt+40 plugins/ListViewWithPageHeader/listviewwithpageheader.cpp+613 plugins/ListViewWithPageHeader/listviewwithpageheader.h+113 plugins/ListViewWithPageHeader/plugin.cpp+26 plugins/ListViewWithPageHeader/plugin.h+33 plugins/ListViewWithPageHeader/qmldir+2
Components/Carousel.qml+1
@@ -21,6 +21,7 @@ import "carousel.js" as CarouselJS
2121Item {
2222 id: carousel
2323
24+ clip: true // FIXME ?
2425 property Component itemComponent
2526 property var model
2627 property alias minimumTileWidth: flickable.minimumTileWidth
Components/ListViewWithPageHeader.qmldeleted−189
@@ -1,189 +0,0 @@
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−import QtQuick 2.0
18−import "Math.js" as MathLocal
19−
20−Item {
21− id: root
22− property Item pageHeader: null
23− property Component sectionDelegate: null
24− property string sectionProperty: ""
25− property alias model: listView.model
26− property alias delegate: listView.delegate
27− property ListView view: listView
28− property alias moving: flicker.moving
29− property alias atYEnd: flicker.atYEnd
30− property bool clipListView: true
31−
32− readonly property real __headerHeight: (pageHeader) ? pageHeader.implicitHeight : 0
33− property real __headerVisibleHeight: __headerHeight
34− readonly property real __overshootHeight: (flicker.contentY < 0) ? -flicker.contentY : 0
35−
36−
37− // TODO move to AnimationController
38− ParallelAnimation {
39− id: headerAnimation
40− property real targetContentY
41− NumberAnimation {
42− target: root
43− property: "__headerVisibleHeight"
44− to: root.__headerHeight
45− duration: 200
46− easing.type: Easing.OutQuad
47− }
48− NumberAnimation {
49− target: listView
50− property: "contentY"
51− to: headerAnimation.targetContentY
52− duration: 200
53− easing.type: Easing.OutQuad
54− }
55− }
56−
57− function positionAtBeginning() {
58− __headerVisibleHeight = __headerHeight
59− flicker.contentY = 0
60− }
61−
62− function showHeader() {
63− headerAnimation.targetContentY = listView.contentY + (__headerHeight - __headerVisibleHeight)
64− headerAnimation.start()
65− }
66−
67− function flick(xVelocity, yVelocity) {
68− flicker.flick(xVelocity, yVelocity);
69− }
70−
71− onPageHeaderChanged: {
72− pageHeader.parent = pageHeaderContainer;
73− pageHeader.anchors.fill = pageHeaderContainer;
74− }
75−
76− Item {
77− id: pageHeaderClipper
78− parent: flicker // parent to Flickable so mouse click events passed through to the header component
79− anchors {
80− top: parent.top
81− left: parent.left
82− right: parent.right
83− }
84− height: __headerVisibleHeight + __overshootHeight
85−
86− Item {
87− id: pageHeaderContainer
88− anchors {
89− left: parent.left
90− right: parent.right
91− bottom: parent.bottom
92− }
93− height: __headerHeight + __overshootHeight
94− }
95− }
96−
97− ListView {
98− id: listView
99− parent: flicker // parent to Flickable so mouse click events passed through to List delegates
100− anchors {
101− left: parent.left
102− right: parent.right
103− top: parent.top
104− topMargin: __headerVisibleHeight
105− }
106− height: root.height
107−
108− // FIXME scrolling workaround, see below
109− cacheBuffer: height*10
110−
111− section.property: sectionProperty
112− section.criteria: ViewSection.FullString
113− section.labelPositioning: ViewSection.InlineLabels | ViewSection.CurrentLabelAtStart
114− section.delegate: sectionDelegate
115−
116− interactive: false
117− clip: root.clipListView
118−
119− property int __sectionDelegateHeight: __getHeight(section.delegate)
120− function __getHeight(component) {
121− // want height (minus allowed overlap) of the section delegate as is needed for clipping
122− if (component === null) return 0;
123− var object = component.createObject(); //FIXME: throws 'section' not defined error
124− var value = object.height - object.bottomBorderAllowedOverlap;
125− object.destroy();
126− return value;
127− }
128−
129− property real previousOriginY: 0
130− onOriginYChanged: {
131− var deltaOriginY = originY - previousOriginY;
132− previousOriginY = originY;
133− /* When originY changes, it causes the top of the flicker and the top of the list to fall
134− out of sync - and thus the contentY positioning will be broken. To correct for this we
135− manually change the flickable's contentY here */
136− flicker.contentY -= deltaOriginY;
137− }
138−
139− /* For case when list content greater than container height and list scrolled down so header
140− hidden. If content shrinks to be smaller than the container height, we want the header to
141− re-appear */
142− property real __previousContentHeight: 0
143− onContentHeightChanged: {
144− var deltaContentHeight = contentHeight - __previousContentHeight;
145− __previousContentHeight = contentHeight;
146− if (contentHeight < height && deltaContentHeight < 0 && __headerVisibleHeight < height - contentHeight) {
147− __headerVisibleHeight = Math.min(height - contentHeight, __headerHeight);
148− }
149− }
150− }
151−
152− Flickable {
153− id: flicker
154− anchors.fill: parent
155− contentHeight: listView.contentHeight + __headerHeight
156− maximumFlickVelocity: height * 10
157− flickDeceleration: height * 2
158− onContentYChanged: {
159− var deltaContentY = contentY - __previousContentY;
160− __previousContentY = contentY;
161−
162− // first decide if movement will prompt the page header to change height
163− if ((deltaContentY < 0 && __headerVisibleHeight >= 0) ||
164− (deltaContentY > 0 && __headerVisibleHeight <= __headerHeight)) {
165−
166− // calculate header height - but prevent bounce from changing it
167− if (contentY > 0 && contentY < contentHeight - height) {
168− __headerVisibleHeight = MathLocal.clamp(__headerVisibleHeight - deltaContentY, 0, __headerHeight);
169− }
170− }
171−
172− // now we move list position, taking into account page header height
173−
174− // BUG: With section headers enabled, the values of originY and contentY appear not
175− // correct at the exact point originY changes. originY changes when the ListView
176− // deletes/creates hidden delegates which are above the visible delegates.
177− // As a result of this bug, you experience jittering scrolling when rapidly moving
178− // around in large lists. See https://bugreports.qt-project.org/browse/QTBUG-27997
179− // A workaround is to use a large enough cacheBuffer to prevent deletions/creations
180− // so effectively originY is always zero.
181− var newContentY = flicker.contentY + listView.originY - __headerHeight + __headerVisibleHeight
182− if (newContentY < listView.contentHeight) {
183− listView.contentY = newContentY;
184− }
185− }
186−
187− property real __previousContentY: 0
188− }
189−}
Dash/DashApps.qml+6−5
@@ -17,6 +17,7 @@
1717import QtQuick 2.0
1818import Ubuntu.Components 0.1
1919import Utils 0.1
20+import ListViewWithPageHeader 0.1
2021import "../Components"
2122import "../Components/ListItems"
2223import "Apps"
@@ -167,11 +168,11 @@ LensView {
167168 }
168169 }
169170
170− sectionProperty: "category"
171− sectionDelegate: Header {
172− width: categoryView.width
173− text: categoryListModel.getCategory(section)
174− }
171+// sectionProperty: "category"
172+// sectionDelegate: Header {
173+// width: categoryView.width
174+// text: categoryListModel.getCategory(section)
175+// }
175176 pageHeader: PageHeader {
176177 id: pageHeader
177178 width: categoryView.width
Dash/DashHome.qml+7−6
@@ -17,6 +17,7 @@
1717import QtQuick 2.0
1818import Ubuntu.Components 0.1
1919import Utils 0.1
20+import ListViewWithPageHeader 0.1
2021import "../Components"
2122import "../Components/ListItems"
2223import "../Applications"
@@ -187,13 +188,13 @@ LensView {
187188 }
188189 }
189190
190− sectionProperty: "category"
191− sectionDelegate: Header {
192− width: listView.width
193− text: listView.model.getCategory(section)
194− }
191+// sectionProperty: "category"
192+// sectionDelegate: Header {
193+// width: listView.width
194+// text: listView.model.getCategory(section)
195+// }
195196 pageHeader: PageHeader {
196− width: listView.width
197+ width: parent.width
197198 text: i18n.tr("Home")
198199 }
199200 }
Dash/DashMusic.qml+6−5
@@ -16,6 +16,7 @@
1616
1717import QtQuick 2.0
1818import Ubuntu.Components 0.1
19+import ListViewWithPageHeader 0.1
1920import "../Components"
2021import "../Components/ListItems" as ListItems
2122import "Music"
@@ -81,11 +82,11 @@ LensView {
8182 }
8283 }
8384
84− sectionProperty: "name"
85− sectionDelegate: ListItems.Header {
86− width: categoryView.width
87− text: section
88− }
85+// sectionProperty: "name"
86+// sectionDelegate: ListItems.Header {
87+// width: categoryView.width
88+// text: section
89+// }
8990 pageHeader: PageHeader {
9091 id: pageHeader
9192 width: categoryView.width
Dash/DashPeople.qml+6−5
@@ -16,6 +16,7 @@
1616
1717import QtQuick 2.0
1818import Ubuntu.Components 0.1
19+import ListViewWithPageHeader 0.1
1920import Dee 3.0
2021import Unity 0.1
2122import Utils 0.1
@@ -105,11 +106,11 @@ LensView {
105106 }
106107 }
107108
108− sectionProperty: "name"
109− sectionDelegate: ListItems.Header {
110− width: categoryView.width
111− text: section
112− }
109+// sectionProperty: "name"
110+// sectionDelegate: ListItems.Header {
111+// width: categoryView.width
112+// text: section
113+// }
113114 pageHeader: PageHeader {
114115 id: pageHeader
115116 width: categoryView.width
Dash/DashVideos.qml+7−6
@@ -16,6 +16,7 @@
1616
1717import QtQuick 2.0
1818import Ubuntu.Components 0.1
19+import ListViewWithPageHeader 0.1
1920import "../Components"
2021import "../Components/ListItems" as ListItems
2122import "Video"
@@ -94,7 +95,7 @@ LensView {
9495 id: categoryView
9596 anchors.fill: parent
9697 model: lensView.categories
97− clipListView: !previewLoader.onScreen
98+// clipListView: !previewLoader.onScreen
9899
99100 onAtYEndChanged: if (atYEnd) endReached()
100101 onMovingChanged: if (moving && atYEnd) endReached()
@@ -136,11 +137,11 @@ LensView {
136137 }
137138 }
138139
139− sectionProperty: "name"
140− sectionDelegate: ListItems.Header {
141− width: categoryView.width
142− text: section
143− }
140+// sectionProperty: "name"
141+// sectionDelegate: ListItems.Header {
142+// width: categoryView.width
143+// text: section
144+// }
144145
145146 pageHeader: PageHeader {
146147 id: pageHeader
Dash/GenericLensView.qml+6−5
@@ -16,6 +16,7 @@
1616
1717import QtQuick 2.0
1818import Ubuntu.Components 0.1
19+import ListViewWithPageHeader 0.1
1920import "../Components"
2021import "../Components/ListItems" as ListItems
2122
@@ -79,11 +80,11 @@ LensView {
7980 }
8081 }
8182
82− sectionProperty: "name"
83− sectionDelegate: ListItems.Header {
84− width: categoryView.width
85− text: section
86− }
83+// sectionProperty: "name"
84+// sectionDelegate: ListItems.Header {
85+// width: categoryView.width
86+// text: section
87+// }
8788 pageHeader: PageHeader {
8889 id: pageHeader
8990 objectName: "pageHeader"
plugins/CMakeLists.txt+1
@@ -3,3 +3,4 @@ add_subdirectory(Utils)
33add_subdirectory(Unity)
44add_subdirectory(HudClient)
55add_subdirectory(LightDM)
6+add_subdirectory(ListViewWithPageHeader)
plugins/ListViewWithPageHeader/CMakeLists.txtnew file+40
@@ -0,0 +1,40 @@
1+include_directories(
2+ ${CMAKE_CURRENT_SOURCE_DIR}
3+ ${CMAKE_CURRENT_BINARY_DIR}
4+ ${Qt5Qml_PRIVATE_INCLUDE_DIRS}
5+ ${Qt5Quick_INCLUDE_DIRS}
6+ ${Qt5Quick_PRIVATE_INCLUDE_DIRS}
7+# FIXME There's no cmake var for this :-/
8+ /usr/include/qt5/QtV8/5.0.2/QtV8
9+)
10+
11+remove_definitions(-DQT_NO_KEYWORDS)
12+
13+set(QMLPLUGIN_SRC
14+ plugin.cpp
15+ listviewwithpageheader.cpp
16+ )
17+
18+add_library(ListViewWithPageHeader-qml MODULE
19+ ${QMLPLUGIN_SRC}
20+ )
21+
22+target_link_libraries(ListViewWithPageHeader-qml
23+ ${Qt5Gui_LIBRARIES}
24+ ${Qt5Quick_LIBRARIES}
25+ )
26+
27+qt5_use_modules(ListViewWithPageHeader-qml Qml Quick)
28+
29+# copy qmldir file into build directory for shadow builds
30+file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/qmldir"
31+ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}
32+ )
33+
34+install(TARGETS ListViewWithPageHeader-qml
35+ DESTINATION ${SHELL_APP_DIR}/plugins/ListViewWithPageHeader
36+ )
37+
38+install(FILES qmldir
39+ DESTINATION ${SHELL_APP_DIR}/plugins/ListViewWithPageHeader
40+ )
plugins/ListViewWithPageHeader/listviewwithpageheader.cppnew file+613
@@ -0,0 +1,613 @@
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+#include "listviewwithpageheader.h"
18+
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>
25+
26+ListViewWithPageHeader::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)
35+{
36+ m_clipItem = new QQuickItem(contentItem());
37+// m_clipItem = new QQuickRectangle(contentItem());
38+// ((QQuickRectangle*)m_clipItem)->setColor(Qt::gray);
39+
40+ connect(this, SIGNAL(contentWidthChanged()), this, SLOT(onContentWidthChanged()));
41+ connect(this, SIGNAL(contentHeightChanged()), this, SLOT(onContentHeightChanged()));
42+}
43+
44+ListViewWithPageHeader::~ListViewWithPageHeader()
45+{
46+}
47+
48+QAbstractItemModel *ListViewWithPageHeader::model() const
49+{
50+ return m_delegateModel ? m_delegateModel->model().value<QAbstractItemModel *>() : nullptr;
51+}
52+
53+void ListViewWithPageHeader::setModel(QAbstractItemModel *model)
54+{
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+ }
68+}
69+
70+QQmlComponent *ListViewWithPageHeader::delegate() const
71+{
72+ return m_delegateModel ? m_delegateModel->delegate() : nullptr;
73+}
74+
75+void ListViewWithPageHeader::setDelegate(QQmlComponent *delegate)
76+{
77+ if (delegate != this->delegate()) {
78+ if (!m_delegateModel) {
79+ createDelegateModel();
80+ }
81+
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);
89+
90+ m_delegateModel->setDelegate(delegate);
91+
92+ Q_EMIT delegateChanged();
93+ m_delegateValidated = false;
94+ m_contentHeightDirty = true;
95+ polish();
96+ }
97+}
98+
99+QQuickItem *ListViewWithPageHeader::header() const
100+{
101+ return m_headerItem;
102+}
103+
104+void ListViewWithPageHeader::setHeader(QQuickItem *headerItem)
105+{
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+ }
128+}
129+
130+void ListViewWithPageHeader::positionAtBeginning()
131+{
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;
141+
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+ }
156+
157+ m_previousContentY = m_visibleItems.first()->y() - headerHeight;
158+ }
159+ QQuickFlickable::setContentY(m_visibleItems.first()->y() + m_clipItem->y() - headerHeight);
160+}
161+
162+void ListViewWithPageHeader::showHeader()
163+{
164+ // TODO
165+}
166+
167+qreal ListViewWithPageHeader::minYExtent() const
168+{
169+// qDebug() << "ListViewWithPageHeader::minYExtent" << m_minYExtent;
170+ return m_minYExtent;
171+}
172+
173+void ListViewWithPageHeader::componentComplete()
174+{
175+ if (m_delegateModel)
176+ m_delegateModel->componentComplete();
177+
178+ QQuickFlickable::componentComplete();
179+
180+ polish();
181+}
182+
183+void ListViewWithPageHeader::viewportMoved(Qt::Orientations orient)
184+{
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();
200+
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();
229+}
230+
231+void ListViewWithPageHeader::setContentY(qreal /*pos*/)
232+{
233+ qWarning() << "ListViewWithPageHeader::setContentY unsupported feature";
234+}
235+
236+void ListViewWithPageHeader::createDelegateModel()
237+{
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();
242+}
243+
244+void ListViewWithPageHeader::refill()
245+{
246+ if (!isComponentComplete()) {
247+ qWarning() << "Incomplete ListViewWithPageHeader::refill";
248+ return;
249+ }
250+
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;
256+
257+ bool added = addVisibleItems(from, to, false);
258+ bool removed = removeNonVisibleItems(bufferFrom, bufferTo);
259+ added |= addVisibleItems(bufferFrom, bufferTo, true);
260+
261+ if (added || removed) {
262+ m_contentHeightDirty = true;
263+ }
264+}
265+
266+bool ListViewWithPageHeader::addVisibleItems(qreal fillFrom, qreal fillTo, bool asynchronous)
267+{
268+ if (!delegate())
269+ return false;
270+
271+ if (m_delegateModel->count() == 0)
272+ return false;
273+
274+ QQuickItem *item;
275+// qDebug() << "ListViewWithPageHeader::addVisibleItems" << fillFrom << fillTo << asynchronous;
276+
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+ }
294+
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+ }
310+
311+ return changed;
312+}
313+
314+void ListViewWithPageHeader::releaseItem(QQuickItem *item)
315+{
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+ }
322+}
323+
324+bool ListViewWithPageHeader::removeNonVisibleItems(qreal bufferFrom, qreal bufferTo)
325+{
326+// qDebug() << "ListViewWithPageHeader::removeNonVisibleItems" << bufferFrom << bufferTo;
327+ bool changed = false;
328+
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+ }
351+
352+ return changed;
353+}
354+
355+QQuickItem *ListViewWithPageHeader::createItem(int modelIndex, bool asynchronous)
356+{
357+// qDebug() << "CREATE ITEM" << modelIndex;
358+ if (asynchronous && m_asyncRequestedIndexes.contains(modelIndex))
359+ return nullptr;
360+
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+ }
392+}
393+
394+void ListViewWithPageHeader::itemCreated(int modelIndex, QQuickItem *item)
395+{
396+// qDebug() << "ListViewWithPageHeader::itemCreated" << modelIndex << item;
397+
398+ item->setParentItem(m_clipItem);
399+ if (m_asyncRequestedIndexes.remove(modelIndex)) {
400+ createItem(modelIndex, false);
401+ }
402+}
403+
404+void ListViewWithPageHeader::updateClipItem()
405+{
406+ m_clipItem->setHeight(height() - m_headerItemShownHeight);
407+ m_clipItem->setY(contentY() + m_headerItemShownHeight);
408+ m_clipItem->setClip(m_headerItemShownHeight > 0);
409+}
410+
411+void ListViewWithPageHeader::onContentHeightChanged()
412+{
413+ updateClipItem();
414+}
415+
416+void ListViewWithPageHeader::onContentWidthChanged()
417+{
418+ m_clipItem->setWidth(contentItem()->width());
419+}
420+
421+void ListViewWithPageHeader::onModelUpdated(const QQuickChangeSet &changeSet, bool /*reset*/)
422+{
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+ }
458+
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+ }
478+
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+ }
497+
498+ polish();
499+ m_contentHeightDirty = true;
500+}
501+
502+void ListViewWithPageHeader::itemGeometryChanged(QQuickItem *item, const QRectF &newGeometry, const QRectF &oldGeometry)
503+{
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+ }
519+}
520+
521+void ListViewWithPageHeader::headerHeightChanged(qreal newHeaderHeight, qreal oldHeaderHeight, qreal oldHeaderY)
522+{
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+ }
543+}
544+
545+
546+void ListViewWithPageHeader::adjustMinYExtent()
547+{
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+ }
553+}
554+
555+QQuickItem *ListViewWithPageHeader::itemAtIndex(int modelIndex) const
556+{
557+ const int visibleIndexedModelIndex = modelIndex - m_firstVisibleIndex;
558+ if (visibleIndexedModelIndex >= 0 && visibleIndexedModelIndex < m_visibleItems.count())
559+ return m_visibleItems[visibleIndexedModelIndex];
560+
561+ return nullptr;
562+}
563+
564+void ListViewWithPageHeader::updatePolish()
565+{
566+ if (!model())
567+ return;
568+
569+ if (!m_visibleItems.isEmpty()) {
570+ const qreal visibleFrom = contentY() - m_clipItem->y();
571+ const qreal visibleTo = contentY() + height() - m_clipItem->y();
572+
573+ qreal pos = m_visibleItems.first()->y();
574+
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+ }
583+
584+ refill();
585+
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+ }
608+
609+ m_contentHeightDirty = false;
610+ setContentHeight(contentHeight);
611+ }
612+
613+}
plugins/ListViewWithPageHeader/listviewwithpageheader.hnew file+113
@@ -0,0 +1,113 @@
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+#ifndef LISTVIEWWITHPAGEHEADER_H
18+#define LISTVIEWWITHPAGEHEADER_H
19+
20+#include <private/qquickitemchangelistener_p.h>
21+#include <private/qquickflickable_p.h>
22+
23+class QAbstractItemModel;
24+class QQuickChangeSet;
25+class QQuickVisualDataModel;
26+
27+class ListViewWithPageHeader : public QQuickFlickable, public QQuickItemChangeListener
28+{
29+ Q_OBJECT
30+ Q_PROPERTY(QAbstractItemModel *model READ model WRITE setModel NOTIFY modelChanged)
31+ Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged)
32+ Q_PROPERTY(QQuickItem *pageHeader READ header WRITE setHeader NOTIFY headerChanged)
33+
34+ friend class ListViewWithPageHeaderTest;
35+
36+public:
37+ ListViewWithPageHeader();
38+ ~ListViewWithPageHeader();
39+
40+ QAbstractItemModel *model() const;
41+ void setModel(QAbstractItemModel *model);
42+
43+ QQmlComponent *delegate() const;
44+ void setDelegate(QQmlComponent *delegate);
45+
46+ QQuickItem *header() const;
47+ void setHeader(QQuickItem *header);
48+
49+ Q_INVOKABLE void positionAtBeginning();
50+ Q_INVOKABLE void showHeader();
51+
52+Q_SIGNALS:
53+ void modelChanged();
54+ void delegateChanged();
55+ void headerChanged();
56+
57+protected:
58+ void componentComplete();
59+ void viewportMoved(Qt::Orientations orient);
60+ qreal minYExtent() const;
61+ void itemGeometryChanged(QQuickItem *item, const QRectF &newGeometry, const QRectF &oldGeometry);
62+ void updatePolish();
63+
64+private Q_SLOTS:
65+ void itemCreated(int modelIndex, QQuickItem *item);
66+ void onContentHeightChanged();
67+ void onContentWidthChanged();
68+ void onModelUpdated(const QQuickChangeSet &changeSet, bool reset);
69+
70+private:
71+ void setContentY(qreal pos);
72+
73+ void createDelegateModel();
74+
75+ void refill();
76+ bool addVisibleItems(qreal fillFrom, qreal fillTo, bool asynchronous);
77+ bool removeNonVisibleItems(qreal bufferFrom, qreal bufferTo);
78+ QQuickItem *createItem(int modelIndex, bool asynchronous);
79+
80+ void adjustMinYExtent();
81+ void updateClipItem();
82+ void headerHeightChanged(qreal newHeaderHeight, qreal oldHeaderHeight, qreal oldHeaderY);
83+ QQuickItem *itemAtIndex(int modelIndex) const; // Returns the item at modelIndex if has been created
84+ void releaseItem(QQuickItem *item);
85+
86+ QQuickVisualDataModel *m_delegateModel;
87+
88+ // List of indexes we are still waiting because we requested them asynchronously
89+ QSet<int> m_asyncRequestedIndexes;
90+
91+ // Used to only give a warning once if the delegate does not return objects
92+ bool m_delegateValidated;
93+
94+ // Visible indexes, [0] is m_firstValidIndex, [0+1] is m_firstValidIndex +1, ...
95+ QList<QQuickItem *> m_visibleItems;
96+ int m_firstVisibleIndex;
97+
98+ qreal m_minYExtent;
99+
100+ QQuickItem *m_clipItem;
101+
102+ // If any of the heights has changed
103+ // or new items have been added/removed
104+ bool m_contentHeightDirty;
105+
106+ QQuickItem *m_headerItem;
107+ qreal m_previousContentY;
108+ qreal m_headerItemShownHeight; // The height of header shown when the header is shown outside its topmost position
109+ // i.e. it's being shown after dragging down in the middle of the list
110+};
111+
112+
113+#endif
plugins/ListViewWithPageHeader/plugin.cppnew file+26
@@ -0,0 +1,26 @@
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+#include "plugin.h"
19+
20+#include "listviewwithpageheader.h"
21+
22+void ListViewWithPageHeaderPlugin::registerTypes(const char *uri)
23+{
24+ Q_ASSERT(uri == QLatin1String("ListViewWithPageHeader"));
25+ qmlRegisterType<ListViewWithPageHeader>(uri, 0, 1, "ListViewWithPageHeader");
26+}
plugins/ListViewWithPageHeader/plugin.hnew file+33
@@ -0,0 +1,33 @@
1+/*
2+ * Copyright (C) 2012 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+#ifndef LISTVIEWWITHPAGEHEADER_PLUGIN_H
19+#define LISTVIEWWITHPAGEHEADER_PLUGIN_H
20+
21+#include <QtQml/QQmlEngine>
22+#include <QtQml/QQmlExtensionPlugin>
23+
24+class ListViewWithPageHeaderPlugin : public QQmlExtensionPlugin
25+{
26+ Q_OBJECT
27+ Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
28+
29+public:
30+ void registerTypes(const char *uri);
31+};
32+
33+#endif
plugins/ListViewWithPageHeader/qmldirnew file+2
@@ -0,0 +1,2 @@
1+module ListViewWithPageHeader
2+plugin ListViewWithPageHeader-qml