Merge lp:~verzegnassi-stefano/ubuntu-docviewer-app/20-enable-zoom into lp:ubuntu-docviewer-app/trunk

Proposed by Stefano Verzegnassi on 2015-01-30
Status: Merged
Approved by: Alan Pope 🍺🐧🐱 πŸ¦„ on 2015-02-04
Approved revision: 85
Merged at revision: 69
Proposed branch: lp:~verzegnassi-stefano/ubuntu-docviewer-app/20-enable-zoom
Merge into: lp:ubuntu-docviewer-app/trunk
Prerequisite: lp:~verzegnassi-stefano/ubuntu-docviewer-app/10-prepare-poppler-plugin
Diff against target: 1649 lines (+1238/-135)
16 files modified
README (+4/-1)
debian/changelog (+6/-0)
debian/control (+5/-3)
manifest.json.in (+1/-1)
po/com.ubuntu.docviewer.pot (+4/-4)
src/app/main.cpp (+7/-0)
src/app/qml/PdfView.qml (+56/-76)
src/app/qml/PdfViewDelegate.qml (+69/-42)
src/app/qml/PdfViewGotoDialog.qml (+1/-1)
src/plugin/poppler-qml-plugin/CMakeLists.txt (+7/-1)
src/plugin/poppler-qml-plugin/pdfdocument.cpp (+17/-3)
src/plugin/poppler-qml-plugin/pdfdocument.h (+5/-0)
src/plugin/poppler-qml-plugin/plugin.cpp (+3/-1)
src/plugin/poppler-qml-plugin/verticalview.cpp (+884/-0)
src/plugin/poppler-qml-plugin/verticalview.h (+167/-0)
tests/autopilot/ubuntu_docviewer_app/tests/test_docviewer.py (+2/-2)
To merge this branch: bzr merge lp:~verzegnassi-stefano/ubuntu-docviewer-app/20-enable-zoom
Reviewer Review Type Date Requested Status
Ubuntu Phone Apps Jenkins Bot continuous-integration Approve on 2015-02-04
Riccardo Padovani (community) Approve on 2015-02-03
Ubuntu Document Viewer Developers 2015-01-30 Pending
Review via email: mp+248161@code.launchpad.net

Commit Message

Enable zoom in PDF view & multithreading support

Description of the Change

Enabled multithreading support (ImageProvider). At the moment is a bit "ugly" workaround, but it's necessary in order to have good performance with larger documents.

Added VerticalView class, instead of using a ListView. This allows us to have the advantages of a ListView (e.g. cacheBuffer) with a Flickable that can scroll in both directions.
The C++ class comes from Unity 8 (some code has been removed - around 500 lines - and some added).

By doing this, we can provide zoom with good performance.

To post a comment you must log in.
75. By Stefano Verzegnassi on 2015-01-30

Fixed an error while importing new code in PdfView

76. By Stefano Verzegnassi on 2015-01-30

Increment version number

77. By Stefano Verzegnassi on 2015-01-31

Fix broken package

78. By Stefano Verzegnassi on 2015-01-31

Revert previous commit, try to fix deb package

79. By Stefano Verzegnassi on 2015-02-02

Reduce PdfView cachebuffer by half

Riccardo Padovani (rpadovani) wrote :

Tested on vivid desktop, works well.

The workaround about the multithreading is simple and clever (but I hope they fix the bug upstream).

I took a look to code about Flickable, seems good, but I don't have the ability to understand it in its entirety. Anyway, works well.

Thanks for the huge work!

review: Approve

Thank you for the review, Riccardo!
Just waiting for fixing a bug about Debian packaging, and it will land soon (I hope) :D

80. By Stefano Verzegnassi on 2015-02-03

Merge with trunk - Updated pot

81. By Stefano Verzegnassi on 2015-02-04

Merge with trunk - solved conflict in debian/control

82. By Stefano Verzegnassi on 2015-02-04

Any comma is important, thank you Jenkins for such lesson

83. By Stefano Verzegnassi on 2015-02-04

Fixed broken debian package building

84. By Stefano Verzegnassi on 2015-02-04

Fixed broken autopilot tests

85. By Stefano Verzegnassi on 2015-02-04

Fixed last bits of autopilot test

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'README'
2--- README 2014-11-02 23:11:07 +0000
3+++ README 2015-02-04 14:35:20 +0000
4@@ -10,8 +10,11 @@
5 Install poppler's development files:
6 sudo apt-get install libpoppler-qt5-dev
7
8+Install Qt5 private development files:
9+sudo apt-get install qtdeclarative5-private-dev qtbase5-private-dev
10+
11 If you want to compile an arm click package, you need to install that package to the arm compilation environment. For example when using QtCreator for Ubuntu Touch, open Options -> Ubuntu -> Maintain, and then enter:
12
13-apt-get install libpoppler-qt5-dev:armhf
14+apt-get install libpoppler-qt5-dev:armhf qtdeclarative5-private-dev:armhf qtbase5-private-dev:armhf
15
16
17
18=== modified file 'debian/changelog'
19--- debian/changelog 2015-02-04 09:32:00 +0000
20+++ debian/changelog 2015-02-04 14:35:20 +0000
21@@ -1,3 +1,9 @@
22+ubuntu-docviewer-app (0.2.0) utopic; urgency=medium
23+
24+ * Enabled zoom in the PDF viewer
25+
26+ -- Stefano <verzegnassi.stefano@gmail.com> Fri, 30 Jan 2015 20:56:08 +0100
27+
28 ubuntu-docviewer-app (0.1.2) UNRELEASED; urgency=medium
29
30 *
31
32=== modified file 'debian/control'
33--- debian/control 2015-02-04 09:32:00 +0000
34+++ debian/control 2015-02-04 14:35:20 +0000
35@@ -15,6 +15,8 @@
36 qtdeclarative5-dev,
37 qtdeclarative5-dev-tools,
38 qtdeclarative5-qtquick2-plugin,
39+ qtdeclarative5-private-dev,
40+ qtbase5-private-dev,
41 qtdeclarative5-test-plugin
42 Standards-Version: 3.9.6
43 Section: misc
44@@ -24,9 +26,9 @@
45 Package: ubuntu-docviewer-app
46 Architecture: any
47 Depends: qtdeclarative5-qtquick2-plugin,
48- qtdeclarative5-ubuntu-ui-toolkit-plugin | qt-components-ubuntu,
49- ${misc:Depends},
50- ${shlibs:Depends}
51+ qtdeclarative5-ubuntu-ui-toolkit-plugin,
52+ qtdeclarative5-ubuntu-content1,
53+ ${misc:Depends}
54 Description: Document Viewer application
55 Core Document Viewer application
56
57
58=== modified file 'manifest.json.in'
59--- manifest.json.in 2015-01-21 15:41:42 +0000
60+++ manifest.json.in 2015-02-04 14:35:20 +0000
61@@ -12,7 +12,7 @@
62 "content-hub": "docviewer-content.json"
63 }
64 },
65- "version": "0.1.@BZR_REVNO@",
66+ "version": "0.2.@BZR_REVNO@",
67 "maintainer": "Ubuntu App Cats <ubuntu-touch-coreapps@lists.launchpad.net>",
68 "x-source": {
69 "vcs-bzr": "@BZR_SOURCE@",
70
71=== modified file 'po/com.ubuntu.docviewer.pot'
72--- po/com.ubuntu.docviewer.pot 2015-02-03 19:12:45 +0000
73+++ po/com.ubuntu.docviewer.pot 2015-02-04 14:35:20 +0000
74@@ -8,7 +8,7 @@
75 msgstr ""
76 "Project-Id-Version: \n"
77 "Report-Msgid-Bugs-To: \n"
78-"POT-Creation-Date: 2015-02-03 20:12+0100\n"
79+"POT-Creation-Date: 2015-02-04 12:30+0100\n"
80 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
81 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
82 "Language-Team: LANGUAGE <LL@li.org>\n"
83@@ -82,7 +82,7 @@
84 msgid "Close"
85 msgstr ""
86
87-#: ../src/app/qml/PdfView.qml:30 ../src/app/qml/PdfView.qml:99
88+#: ../src/app/qml/PdfView.qml:31
89 #, qt-format
90 msgid "Page %1 of %2"
91 msgstr ""
92@@ -127,7 +127,7 @@
93 msgstr ""
94
95 #: ../src/app/qml/WelcomePage.qml:23
96-#: /home/stefano/Progetti/doc-viewer/build-10-prepare-poppler-plugin-Desktop-Default/po/com.ubuntu.docviewer.desktop.in.in.h:1
97+#: /home/stefano/Progetti/doc-viewer/build-20-enable-zoom-Desktop-Default/po/com.ubuntu.docviewer.desktop.in.in.h:1
98 msgid "Document Viewer"
99 msgstr ""
100
101@@ -143,6 +143,6 @@
102 msgid "Open a file..."
103 msgstr ""
104
105-#: /home/stefano/Progetti/doc-viewer/build-10-prepare-poppler-plugin-Desktop-Default/po/com.ubuntu.docviewer.desktop.in.in.h:2
106+#: /home/stefano/Progetti/doc-viewer/build-20-enable-zoom-Desktop-Default/po/com.ubuntu.docviewer.desktop.in.in.h:2
107 msgid "documents;viewer;pdf;reader;"
108 msgstr ""
109
110=== modified file 'src/app/main.cpp'
111--- src/app/main.cpp 2015-01-16 16:46:46 +0000
112+++ src/app/main.cpp 2015-02-04 14:35:20 +0000
113@@ -22,6 +22,9 @@
114 * Stefano Verzegnassi <stefano92.100@gmail.com>
115 */
116
117+// Uncomment if you need to use QML analyzer
118+// #define QT_QML_DEBUG
119+
120 #include <QtGui/QGuiApplication>
121 #include <QtQuick/QQuickView>
122 #include <QtQml/QtQml>
123@@ -36,6 +39,10 @@
124 QQuickView view;
125 view.setResizeMode(QQuickView::SizeRootObjectToView);
126
127+ view.setPersistentOpenGLContext(false);
128+ view.setPersistentSceneGraph(false);
129+ view.engine()->rootContext()->setContextProperty("QQuickView", &view);
130+
131 // Set up import paths
132 QStringList importPathList = view.engine()->importPathList();
133 // Prepend the location of the plugin in the build dir,
134
135=== modified file 'src/app/qml/PdfView.qml'
136--- src/app/qml/PdfView.qml 2015-01-30 17:44:25 +0000
137+++ src/app/qml/PdfView.qml 2015-02-04 14:35:20 +0000
138@@ -24,93 +24,73 @@
139 id: pdfPage
140 title: Utils.getNameOfFile(file.path);
141
142- // Disable header auto-hide
143+ // Disable header auto-hide.
144+ // TODO: Show/hide header if a user taps the page
145 flickable: null
146
147- property string currentPage: i18n.tr("Page %1 of %2").arg(pdfView.currentIndex + 1).arg(pdfView.count)
148+ property string currentPage: i18n.tr("Page %1 of %2").arg(pdfView.currentPageIndex + 1).arg(pdfView.count)
149
150- // TODO: Restore zooming
151- ListView {
152+ PDF.VerticalView {
153 id: pdfView
154 objectName:"pdfView"
155
156- anchors {
157- fill: parent
158- leftMargin: units.gu(1)
159- rightMargin: units.gu(1)
160- }
161+ anchors.fill: parent
162 spacing: units.gu(2)
163
164 clip: true
165- focus: false
166 boundsBehavior: Flickable.StopAtBounds
167
168- cacheBuffer: height
169-
170- highlightFollowsCurrentItem: false
171- keyNavigationWraps: false
172-
173- header: Item { width: parent.width; height: units.gu(2) }
174- footer: Item { width: parent.width; height: units.gu(2) }
175-
176- model: PDF.Document {
177- id: poppler
178-
179- /* FIXME: Don't set 'path' property directly, but set it through onCompleted signal.
180- By doing otherwise, PDF pages are loaded two times, but only
181- the first delegates are working. Asking to the image provider
182- to get the second ones, makes the app instable.
183- (e.g. We have a PDF document with 10 pages. The plugin loads
184- them twice - 2x10 = 20 pages - but only the first 10 are shown.
185- While trying to get the 11th, the app crashes). */
186- Component.onCompleted: path = file.path
187-
188- onPagesLoaded: {
189- activity.running = false;
190-
191- pdfView.currentIndex = 0
192-
193- var title = getDocumentInfo("Title")
194- if (title !== "")
195- pdfPage.title = title
196- }
197- }
198-
199- delegate: PdfViewDelegate {}
200-
201- onWidthChanged: {
202- /* On resizing window, pages size changes but contentY is still the same.
203- For that reason, it shows the wrong page (which is settled at the same contentY).
204- We need to force flickable to show the current page. */
205- //pdfView.positionViewAtIndex(currentIndex, ListView.Contain)
206- }
207-
208- onContentYChanged: {
209- // FIXME: On wheeling up, ListView automatically center currentItem to the view.
210- // This causes some strange "jump" of ~200px in contentY
211- var i = pdfView.indexAt(pdfView.width * 0.5, contentY + (pdfView.height * 0.5))
212-
213- if (i < 0) {
214- // returned index could be -1 when the delegate spacing is shown at the center of the view (e.g. while scrolling pages)
215- i = pdfView.indexAt(pdfView.width * 0.5, contentY + (pdfView.height * 0.5) + units.gu(4))
216- }
217-
218- if (i !== -1) {
219- currentPage = i18n.tr("Page %1 of %2").arg(i + 1).arg(pdfView.count)
220-
221- if (!pdfView.flickingVertically) {
222- pdfView.currentIndex = i
223- }
224- }
225- }
226- }
227-
228- ActivityIndicator {
229- id: activity
230- anchors.centerIn: parent
231-
232- running: true
233- }
234+ cacheBuffer: height * poppler.providersNumber * _zoomHelper.scale * 0.5
235+
236+ flickDeceleration: 1500 * units.gridUnit / 8
237+ maximumFlickVelocity: 2500 * units.gridUnit / 8
238+
239+ model: poppler
240+ delegate: PdfViewDelegate {
241+ onWidthChanged: QQuickView.releaseResources()
242+ Component.onDestruction: QQuickView.releaseResources()
243+ }
244+
245+ contentWidth: parent.width * _zoomHelper.scale
246+ PinchArea {
247+ id: pinchy
248+ anchors.fill: parent
249+
250+ pinch {
251+ target: _zoomHelper
252+ minimumScale: 1.0
253+ maximumScale: 2.5
254+ }
255+
256+ onPinchStarted: pdfView.interactive = false;
257+
258+ // FIXME: On zooming, keep the same content position.
259+ // onPinchUpdated: {}
260+
261+ onPinchFinished: {
262+ pdfView.interactive = true;
263+ pdfView.returnToBounds();
264+ }
265+ }
266+
267+ Item { id: _zoomHelper }
268+ }
269+
270+ PDF.Document {
271+ id: poppler
272+
273+ property bool isLoading: true
274+
275+ Component.onCompleted: path = file.path
276+ onPagesLoaded: {
277+ isLoading = false;
278+
279+ var title = getDocumentInfo("Title")
280+ if (title !== "")
281+ pdfPage.title = title
282+ }
283+ }
284+
285
286 // *** HEADER ***
287 state: "default"
288
289=== modified file 'src/app/qml/PdfViewDelegate.qml'
290--- src/app/qml/PdfViewDelegate.qml 2015-01-31 10:31:44 +0000
291+++ src/app/qml/PdfViewDelegate.qml 2015-02-04 14:35:20 +0000
292@@ -1,5 +1,5 @@
293 /*
294- * Copyright (C) 2013-2014 Canonical, Ltd.
295+ * Copyright (C) 2013-2015 Canonical, Ltd.
296 *
297 * This program is free software; you can redistribute it and/or modify
298 * it under the terms of the GNU General Public License as published by
299@@ -13,56 +13,83 @@
300 * You should have received a copy of the GNU General Public License
301 * along with this program. If not, see <http://www.gnu.org/licenses/>.
302 */
303-
304 import QtQuick 2.3
305 import Ubuntu.Components 1.1
306-import QtGraphicalEffects 1.0
307
308 Rectangle {
309 id: pdfPage
310+
311+ property int index: model.index
312+ property bool _previewFetched: false
313+
314+ property alias status: pageImg.status
315+
316 width: parent.width
317-
318 height: width * (model.height / model.width)
319-
320- border {
321- width: 1
322- color: "#808080"
323+ color: "#E6E6E6"
324+
325+ // Preview page rendering. Used as placeholder while zooming the page.
326+ // We generate the low resolution preview from the texture of the PDF page,
327+ // so that we can keep page rendering as fast as possible.
328+ ShaderEffectSource {
329+ id: previewImg
330+ anchors.fill: parent
331+
332+ // We cannot change its opacity or visibility, otherwise the texture will be refreshed,
333+ // even if live is false.
334+ live: false
335+ textureSize: Qt.size(256, 256 * (model.height / model.width))
336 }
337
338 Image {
339- id: imagePage
340- anchors {
341- fill: parent
342- margins: 1
343- }
344- asynchronous: true
345- sourceSize.width: parent.width - 2
346- fillMode: Image.PreserveAspectCrop
347-
348- Component.onCompleted: source = "image://poppler/page/" + model.index
349- }
350-
351- Rectangle {
352- anchors.fill: parent
353- color: "white"
354- visible: imagePage.status === Image.Loading
355-
356- ActivityIndicator {
357- anchors.centerIn: parent
358- running: parent.visible
359- }
360- }
361-
362- DropShadow {
363- anchors.fill: parent
364- cached: true;
365- horizontalOffset: 0;
366- verticalOffset: 2;
367- radius: 8.0;
368- samples: 16;
369- color: "#80000000";
370- smooth: true;
371- source: parent;
372- z: -10
373+ id: pageImg
374+ anchors.fill: parent
375+
376+ source: "image://poppler" + (index % poppler.providersNumber) + "/page/" + index;
377+ sourceSize.width: pdfPage.width
378+
379+ onStatusChanged: {
380+ // This is supposed to run the first time PdfViewDelegate gets the page rendering.
381+ if (!_previewFetched) {
382+ if (status == Image.Ready) {
383+ previewImg.sourceItem = pageImg
384+ // Re-assign sourceItem property, so the texture is not updated when Image status changes.
385+ previewImg.sourceItem = pdfPage
386+ }
387+ }
388+ }
389+
390+ // Request a new page rendering. The order, which pages are requested with, depends on the distance from the currentPage
391+ Timer {
392+ id: _zoomTimer
393+ interval: {
394+ var diff = Math.abs(pdfView.currentPageIndex - model.index)
395+ var prov = poppler.providersNumber * 0.5
396+
397+ if (diff < prov)
398+ return 0
399+ else
400+ return (diff - prov) * 10
401+ }
402+
403+ onTriggered: {
404+ pageImg.sourceSize.width = pdfPage.width;
405+ }
406+ }
407+ }
408+
409+ // Page rendering depends on the width of PdfViewDelegate.
410+ // Because of this, we have multiple callings to ImageProvider while zooming.
411+ // Just avoid it.
412+ Connections {
413+ target: pinchy
414+
415+ onPinchStarted: _zoomTimer.stop();
416+ onPinchUpdated: {
417+ // This ensures that page image is not reloaded when the maximumScale or minimumScale has already been reached.
418+ if ( !(_zoomHelper.scale >= 2.5 && pinch.scale > 1.0) && !(_zoomHelper.scale <= 1.0 && pinch.scale < 1.0) )
419+ pageImg.sourceSize.width = 0;
420+ }
421+ onPinchFinished: _zoomTimer.restart();
422 }
423 }
424
425=== modified file 'src/app/qml/PdfViewGotoDialog.qml'
426--- src/app/qml/PdfViewGotoDialog.qml 2015-01-29 19:14:08 +0000
427+++ src/app/qml/PdfViewGotoDialog.qml 2015-02-04 14:35:20 +0000
428@@ -54,7 +54,7 @@
429 }
430
431 function goToPage() {
432- pdfView.positionViewAtIndex((goToPageTextField.text - 1), ListView.Beginning)
433+ pdfView.positionAtIndex((goToPageTextField.text - 1))
434 PopupUtils.close(goToPageDialog)
435 }
436 }
437
438=== modified file 'src/plugin/poppler-qml-plugin/CMakeLists.txt'
439--- src/plugin/poppler-qml-plugin/CMakeLists.txt 2015-02-03 18:50:36 +0000
440+++ src/plugin/poppler-qml-plugin/CMakeLists.txt 2015-02-04 14:35:20 +0000
441@@ -1,5 +1,10 @@
442 set(PLUGIN_DIR com/ubuntu/popplerqmlplugin)
443-include_directories(${CMAKE_CURRENT_SOURCE_DIR})
444+include_directories(
445+ ${CMAKE_CURRENT_SOURCE_DIR}
446+ ${CMAKE_CURRENT_BINARY_DIR}
447+ ${Qt5Quick_PRIVATE_INCLUDE_DIRS}
448+ ${Qt5Qml_PRIVATE_INCLUDE_DIRS}
449+)
450
451 #add the sources to compile
452 set(popplerqmlplugin_SRCS
453@@ -8,6 +13,7 @@
454 pdfimageprovider.cpp
455 pdfthread.cpp
456 pdfitem.cpp
457+ verticalview.cpp
458 )
459
460 add_library(popplerqmlplugin MODULE
461
462=== modified file 'src/plugin/poppler-qml-plugin/pdfdocument.cpp'
463--- src/plugin/poppler-qml-plugin/pdfdocument.cpp 2015-01-30 18:42:00 +0000
464+++ src/plugin/poppler-qml-plugin/pdfdocument.cpp 2015-02-04 14:35:20 +0000
465@@ -25,10 +25,12 @@
466 #include <QDebug>
467 #include <QQmlEngine>
468 #include <QQmlContext>
469+#include <QThread>
470
471 PdfDocument::PdfDocument(QAbstractListModel *parent):
472 QAbstractListModel(parent)
473 , m_path("")
474+ , m_providersNumber(-1)
475 {
476 qRegisterMetaType<PdfPagesList>("PdfPagesList");
477 }
478@@ -163,12 +165,24 @@
479
480 void PdfDocument::loadProvider()
481 {
482- qDebug() << "Loading image provider...";
483+ // WORKAROUND: QQuickImageProvider should create multiple threads to load more images at the same time.
484+ // [QTBUG-37998] QQuickImageProvider can block its separate thread with ForceAsynchronousImageLoading
485+ // Link: https://bugreports.qt.io/browse/QTBUG-37988
486+ int newProvidersNumber = QThread::idealThreadCount();
487+ if (newProvidersNumber != m_providersNumber) {
488+ m_providersNumber = newProvidersNumber;
489+ Q_EMIT providersNumberChanged();
490+ }
491+
492+ qDebug() << "Ideal number of image providers is:" << m_providersNumber;
493+
494+ qDebug() << "Loading image provider(s)...";
495 QQmlEngine *engine = QQmlEngine::contextForObject(this)->engine();
496
497- engine->addImageProvider(QLatin1String("poppler"), new PdfImageProvider(m_document));
498+ for (int i=0; i<m_providersNumber; i++)
499+ engine->addImageProvider(QLatin1String("poppler" + QByteArray::number(i)), new PdfImageProvider(m_document));
500
501- qDebug() << "Image provider loaded successfully !";
502+ qDebug() << "Image provider(s) loaded successfully !";
503 }
504
505 PdfDocument::~PdfDocument()
506
507=== modified file 'src/plugin/poppler-qml-plugin/pdfdocument.h'
508--- src/plugin/poppler-qml-plugin/pdfdocument.h 2015-01-30 18:10:21 +0000
509+++ src/plugin/poppler-qml-plugin/pdfdocument.h 2015-02-04 14:35:20 +0000
510@@ -31,6 +31,7 @@
511 Q_OBJECT
512 Q_DISABLE_COPY(PdfDocument)
513 Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
514+ Q_PROPERTY(int providersNumber READ providersNumber NOTIFY providersNumberChanged)
515
516 public:
517 enum Roles {
518@@ -44,6 +45,8 @@
519 QString path() const { return m_path; }
520 void setPath(QString &pathName);
521
522+ int providersNumber() const { return m_providersNumber; }
523+
524 QHash<int, QByteArray> roleNames() const;
525
526 int rowCount(const QModelIndex & parent = QModelIndex()) const;
527@@ -56,12 +59,14 @@
528 void pathChanged();
529 void error(const QString& errorMessage);
530 void pagesLoaded();
531+ void providersNumberChanged();
532
533 private slots:
534 void _q_populate(PdfPagesList pagesList);
535
536 private:
537 QString m_path;
538+ int m_providersNumber;
539
540 bool loadDocument(QString &pathNAme);
541 void loadProvider();
542
543=== modified file 'src/plugin/poppler-qml-plugin/plugin.cpp'
544--- src/plugin/poppler-qml-plugin/plugin.cpp 2015-01-30 17:44:25 +0000
545+++ src/plugin/poppler-qml-plugin/plugin.cpp 2015-02-04 14:35:20 +0000
546@@ -1,5 +1,5 @@
547 /*
548- * Copyright (C) 2013 Canonical, Ltd.
549+ * Copyright (C) 2013-2015 Canonical, Ltd.
550 *
551 * This program is free software: you can redistribute it and/or modify it
552 * under the terms of the GNU General Public License version 3, as published
553@@ -20,6 +20,7 @@
554
555 #include "plugin.h"
556 #include "pdfdocument.h"
557+#include "verticalview.h"
558
559 void PopplerPlugin::registerTypes(const char *uri)
560 {
561@@ -27,6 +28,7 @@
562
563 //@uri com.ubuntu.popplerqmlplugin
564 qmlRegisterType<PdfDocument>(uri, 1, 0, "Document");
565+ qmlRegisterType<VerticalView>(uri, 1, 0, "VerticalView");
566 }
567
568 void PopplerPlugin::initializeEngine(QQmlEngine *engine, const char *uri)
569
570=== added file 'src/plugin/poppler-qml-plugin/verticalview.cpp'
571--- src/plugin/poppler-qml-plugin/verticalview.cpp 1970-01-01 00:00:00 +0000
572+++ src/plugin/poppler-qml-plugin/verticalview.cpp 2015-02-04 14:35:20 +0000
573@@ -0,0 +1,884 @@
574+/*
575+ * Copyright (C) 2013-2015 Canonical, Ltd.
576+ *
577+ * This program is free software; you can redistribute it and/or modify
578+ * it under the terms of the GNU General Public License as published by
579+ * the Free Software Foundation; version 3.
580+ *
581+ * This program is distributed in the hope that it will be useful,
582+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
583+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
584+ * GNU General Public License for more details.
585+ *
586+ * You should have received a copy of the GNU General Public License
587+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
588+ */
589+
590+/*
591+ * Some documentation on how this thing works:
592+ *
593+ * A flickable has two very important concepts that define the top and
594+ * height of the flickable area.
595+ * The top is returned in minYExtent()
596+ * The height is set using setContentHeight()
597+ * By changing those two values we can make the list grow up or down
598+ * as needed. e.g. if we are in the middle of the list
599+ * and something that is above the viewport grows, since we do not
600+ * want to change the viewport because of that we just adjust the
601+ * minYExtent so that the list grows up.
602+ *
603+ * The implementation on the list relies on the delegateModel doing
604+ * most of the instantiation work. You call createItem() when you
605+ * need to create an item asking for it async or not. If returns null
606+ * it means the item will be created async and the model will call the
607+ * itemCreated slot with the item.
608+ *
609+ * updatePolish is the central point of dispatch for the work of the
610+ * class. It is called by the scene graph just before drawing the class.
611+ * In it we:
612+ * * Make sure all items are positioned correctly
613+ * * Add/Remove items if needed
614+ * * Update the content height if it was dirty
615+ *
616+ * m_visibleItems contains all the items we have created at the moment.
617+ * Actually not all of them are visible since it includes the ones
618+ * in the cache area we create asynchronously to help performance.
619+ * The first item in m_visibleItems has the m_firstVisibleIndex in
620+ * the model. If you actually want to know what is the first
621+ * item in the viewport you have to find the first non culled element
622+ * in m_visibleItems
623+ *
624+ * The first item of m_visibleItems is the one that defines the
625+ * positions of all the rest of items (see updatePolish()) and
626+ * this is why sometimes we move it even if it's not the item
627+ * that has triggered the function (i.e. in itemGeometryChanged())
628+ *
629+ * m_visibleItems is a list of ListItem. Each ListItem
630+ * will contain a item and potentially a sectionItem. The sectionItem
631+ * is only there when the list is using sectionDelegate+sectionProperty
632+ * and this is the first item of the section. Each ListItem is vertically
633+ * layouted with the sectionItem first and then the item.
634+ *
635+ * Note that minYExtent and height are not always totally accurate, since
636+ * we don't have the items created we can't guess their heights
637+ * so we can only guarantee the values are correct when the first/last
638+ * items of the list are visible, otherwise we just live with good enough
639+ * values that make the list scrollable
640+ *
641+ * There are a few things that are not really implemented or tested properly
642+ * which we don't use at the moment like changing the model, etc.
643+ * The known missing features are marked with TODOs along the code.
644+ */
645+
646+#include "verticalview.h"
647+
648+#include <QCoreApplication>
649+#include <QDebug>
650+#include <qqmlinfo.h>
651+#include <qqmlengine.h>
652+#pragma GCC diagnostic push
653+#pragma GCC diagnostic ignored "-pedantic"
654+#include <private/qqmldelegatemodel_p.h>
655+#include <private/qqmlglobal_p.h>
656+#include <private/qquickitem_p.h>
657+#include <private/qquickanimation_p.h>
658+#pragma GCC diagnostic pop
659+
660+qreal VerticalView::ListItem::height() const
661+{
662+ return m_item->height();
663+}
664+
665+qreal VerticalView::ListItem::y() const
666+{
667+ return m_item->y();
668+}
669+
670+void VerticalView::ListItem::setY(qreal newY)
671+{
672+ m_item->setY(newY);
673+}
674+
675+bool VerticalView::ListItem::culled() const
676+{
677+ return QQuickItemPrivate::get(m_item)->culled;
678+}
679+
680+void VerticalView::ListItem::setCulled(bool culled)
681+{
682+ QQuickItemPrivate::get(m_item)->setCulled(culled);
683+}
684+
685+VerticalView::VerticalView()
686+ : m_delegateModel(nullptr)
687+ , m_asyncRequestedIndex(-1)
688+ , m_delegateValidated(false)
689+ , m_firstVisibleIndex(-1)
690+ , m_currentPageIndex(-1)
691+ , m_minYExtent(0)
692+ , m_contentHeightDirty(false)
693+ , m_previousContentY(0)
694+ , m_inLayout(false)
695+ , m_cacheBuffer(0)
696+ , m_spacing(0)
697+{
698+ connect(this, SIGNAL(heightChanged()), this, SLOT(_q_heightChanged()));
699+ connect(this, SIGNAL(contentYChanged()), this, SLOT(_q_updateCurrentPageIndex()));
700+
701+ setFlickableDirection(QQuickFlickable::HorizontalAndVerticalFlick);
702+}
703+
704+VerticalView::~VerticalView()
705+{
706+}
707+
708+QAbstractItemModel *VerticalView::model() const
709+{
710+ return m_delegateModel ? m_delegateModel->model().value<QAbstractItemModel *>() : nullptr;
711+}
712+
713+void VerticalView::setModel(QAbstractItemModel *model)
714+{
715+ if (model != this->model()) {
716+ if (!m_delegateModel) {
717+ createDelegateModel();
718+ } else {
719+ disconnect(m_delegateModel, SIGNAL(modelUpdated(QQmlChangeSet,bool)), this, SLOT(_q_modelUpdated(QQmlChangeSet,bool)));
720+ }
721+ m_delegateModel->setModel(QVariant::fromValue<QAbstractItemModel *>(model));
722+ connect(m_delegateModel, SIGNAL(modelUpdated(QQmlChangeSet,bool)), this, SLOT(_q_modelUpdated(QQmlChangeSet,bool)));
723+ Q_EMIT modelChanged();
724+ polish();
725+ // TODO?
726+// Q_EMIT contentHeightChanged();
727+// Q_EMIT contentYChanged();
728+ }
729+}
730+
731+QQmlComponent *VerticalView::delegate() const
732+{
733+ return m_delegateModel ? m_delegateModel->delegate() : nullptr;
734+}
735+
736+void VerticalView::setDelegate(QQmlComponent *delegate)
737+{
738+ if (delegate != this->delegate()) {
739+ if (!m_delegateModel) {
740+ createDelegateModel();
741+ }
742+
743+ // Cleanup the existing items
744+ Q_FOREACH(ListItem *item, m_visibleItems)
745+ releaseItem(item);
746+ m_visibleItems.clear();
747+ m_firstVisibleIndex = -1;
748+ adjustMinYExtent();
749+ setContentY(0);
750+
751+ m_delegateModel->setDelegate(delegate);
752+
753+ Q_EMIT delegateChanged();
754+ m_delegateValidated = false;
755+ m_contentHeightDirty = true;
756+ polish();
757+ }
758+}
759+
760+int VerticalView::cacheBuffer() const
761+{
762+ return m_cacheBuffer;
763+}
764+
765+void VerticalView::setCacheBuffer(int cacheBuffer)
766+{
767+ if (cacheBuffer < 0) {
768+ qmlInfo(this) << "Cannot set a negative cache buffer";
769+ return;
770+ }
771+
772+ if (cacheBuffer != m_cacheBuffer) {
773+ m_cacheBuffer = cacheBuffer;
774+ Q_EMIT cacheBufferChanged();
775+ polish();
776+ }
777+}
778+
779+qreal VerticalView::spacing() const
780+{
781+ return m_spacing;
782+}
783+
784+void VerticalView::setSpacing(qreal spacing)
785+{
786+ if (spacing < 0) {
787+ qmlInfo(this) << "Cannot set a negative spacing";
788+ return;
789+ }
790+
791+ if (spacing != m_spacing) {
792+ m_spacing = spacing;
793+ Q_EMIT spacingChanged();
794+ polish();
795+ }
796+}
797+
798+int VerticalView::count() const
799+{
800+ if (m_delegateModel)
801+ return m_delegateModel->count();
802+ else
803+ return 0;
804+}
805+
806+int VerticalView::currentPageIndex() const
807+{
808+ return m_currentPageIndex;
809+}
810+
811+QQuickItem *VerticalView::currentPageItem() const
812+{
813+ return itemAt(m_currentPageIndex);
814+}
815+
816+void VerticalView::_q_updateCurrentPageIndex()
817+{
818+ if (!m_visibleItems.isEmpty()) {
819+ qreal pos = this->contentY() + (this->height() * 0.5);
820+
821+ int oldCurrentPageIndex = m_currentPageIndex;
822+ int i = 0;
823+
824+ Q_FOREACH(ListItem * item, m_visibleItems) {
825+ if (item->y() < pos && item->y() + item->height() > pos)
826+ break;
827+
828+ i++;
829+ }
830+
831+ // If spacing is set, there may be no page on posY position,
832+ // and the Q_FOREACH loop keep on running until the end.
833+ if (i != m_visibleItems.length())
834+ m_currentPageIndex = m_firstVisibleIndex + i;
835+
836+ if (m_currentPageIndex != oldCurrentPageIndex) {
837+ Q_EMIT currentPageIndexChanged();
838+ Q_EMIT currentPageItemChanged();
839+ }
840+
841+ }
842+}
843+
844+void VerticalView::positionAtBeginning()
845+{
846+ if (m_delegateModel->count() <= 0)
847+ return;
848+
849+ if (m_firstVisibleIndex != 0) {
850+ // TODO This could be optimized by trying to reuse the interesection
851+ // of items that may end up intersecting between the existing
852+ // m_visibleItems and the items we are creating in the next loop
853+ Q_FOREACH(ListItem *item, m_visibleItems)
854+ releaseItem(item);
855+ m_visibleItems.clear();
856+ m_firstVisibleIndex = -1;
857+
858+ // Create the item 0, it will be already correctly positioned at createItem()
859+ ListItem *item = createItem(0, false);
860+ // Create the subsequent items
861+ int modelIndex = 1;
862+ qreal pos = item->y() + item->height();
863+ const qreal bufferTo = height() + m_cacheBuffer;
864+ while (modelIndex < m_delegateModel->count() && pos <= bufferTo) {
865+ if (!(item = createItem(modelIndex, false)))
866+ break;
867+ pos += item->height();
868+ ++modelIndex;
869+ }
870+
871+ m_previousContentY = m_visibleItems.first()->y();
872+ }
873+ setContentY(m_visibleItems.first()->y());
874+}
875+
876+void VerticalView::positionAtIndex(int index)
877+{
878+ if (m_delegateModel->count() <= 0)
879+ return;
880+
881+ if (index < m_firstVisibleIndex || index > m_firstVisibleIndex + m_visibleItems.length()) {
882+ // TODO This could be optimized by trying to reuse the interesection
883+ // of items that may end up intersecting between the existing
884+ // m_visibleItems and the items we are creating in the next loop
885+ Q_FOREACH(ListItem *item, m_visibleItems)
886+ releaseItem(item);
887+ m_visibleItems.clear();
888+ m_firstVisibleIndex = -1;
889+
890+ // Create the item with the given index, it will be already correctly positioned at createItem()
891+ // Other items are created when createdItem() signal is emitted.
892+ createItem(index, false);
893+
894+ m_previousContentY = m_visibleItems.first()->y();
895+ }
896+
897+ setContentY(itemAt(index)->y());
898+}
899+
900+void VerticalView::positionAtEnd()
901+{
902+ if (m_delegateModel->count() <= 0)
903+ return;
904+
905+ if (m_firstVisibleIndex != m_delegateModel->count() - 1) {
906+ // TODO This could be optimized by trying to reuse the interesection
907+ // of items that may end up intersecting between the existing
908+ // m_visibleItems and the items we are creating in the next loop
909+ Q_FOREACH(ListItem *item, m_visibleItems)
910+ releaseItem(item);
911+ m_visibleItems.clear();
912+ m_firstVisibleIndex = -1;
913+
914+ // Create the item 0, it will be already correctly positioned at createItem()
915+ ListItem *item = createItem(m_delegateModel->count() - 1, false);
916+ // Create the prior items
917+ int modelIndex = m_delegateModel->count() - 2;
918+ qreal pos = item->y() + item->height();
919+ const qreal bufferFrom = contentY() - m_cacheBuffer;
920+ while (modelIndex > -1 && pos >= bufferFrom) {
921+ if (!(item = createItem(modelIndex, false)))
922+ break;
923+ pos += item->height();
924+ --modelIndex;
925+ }
926+
927+ m_previousContentY = m_visibleItems.first()->y();
928+ }
929+ setContentY(m_visibleItems.first()->y());
930+}
931+
932+static inline bool uFuzzyCompare(qreal r1, qreal r2)
933+{
934+ return qFuzzyCompare(r1, r2) || (qFuzzyIsNull(r1) && qFuzzyIsNull(r2));
935+}
936+
937+QQuickItem *VerticalView::itemAt(int modelIndex) const
938+{
939+ ListItem *item = itemAtIndex(modelIndex);
940+ if (item)
941+ return item->m_item;
942+ else
943+ return nullptr;
944+}
945+
946+qreal VerticalView::minYExtent() const
947+{
948+ return m_minYExtent;
949+}
950+
951+void VerticalView::componentComplete()
952+{
953+ if (m_delegateModel)
954+ m_delegateModel->componentComplete();
955+
956+ QQuickFlickable::componentComplete();
957+
958+ polish();
959+}
960+
961+void VerticalView::viewportMoved(Qt::Orientations orient)
962+{
963+ // Check we are not being taken down and don't paint anything
964+ // TODO Check if we still need this in 5.2
965+ // For reproduction just inifnite loop testDash or testDashContent
966+ if (!QQmlEngine::contextForObject(this)->parentContext())
967+ return;
968+
969+ QQuickFlickable::viewportMoved(orient);
970+ m_previousContentY = contentY();
971+ layout();
972+ polish();
973+}
974+
975+void VerticalView::createDelegateModel()
976+{
977+ m_delegateModel = new QQmlDelegateModel(qmlContext(this), this);
978+ connect(m_delegateModel, SIGNAL(createdItem(int,QObject*)), this, SLOT(_q_itemCreated(int,QObject*)));
979+ connect(m_delegateModel, SIGNAL(countChanged()), this, SIGNAL(countChanged()));
980+ if (isComponentComplete())
981+ m_delegateModel->componentComplete();
982+ updateWatchedRoles();
983+}
984+
985+void VerticalView::refill()
986+{
987+ if (m_inLayout) {
988+ return;
989+ }
990+ if (!isComponentComplete()) {
991+ return;
992+ }
993+
994+ const qreal from = contentY();
995+ const qreal to = from + height();
996+ const qreal bufferFrom = from - m_cacheBuffer;
997+ const qreal bufferTo = to + m_cacheBuffer;
998+
999+ bool added = addVisibleItems(from, to, false);
1000+ bool removed = removeNonVisibleItems(bufferFrom, bufferTo);
1001+ added |= addVisibleItems(bufferFrom, bufferTo, true);
1002+
1003+ if (added || removed) {
1004+ m_contentHeightDirty = true;
1005+ }
1006+}
1007+
1008+bool VerticalView::addVisibleItems(qreal fillFrom, qreal fillTo, bool asynchronous)
1009+{
1010+ if (!delegate())
1011+ return false;
1012+
1013+ if (m_delegateModel->count() == 0)
1014+ return false;
1015+
1016+ ListItem *item;
1017+ int modelIndex = 0;
1018+ qreal pos = 0;
1019+ if (!m_visibleItems.isEmpty()) {
1020+ modelIndex = m_firstVisibleIndex + m_visibleItems.count();
1021+ item = m_visibleItems.last();
1022+ pos = item->y() + item->height() + m_spacing;
1023+ }
1024+ bool changed = false;
1025+
1026+ while (modelIndex < m_delegateModel->count() && pos <= fillTo) {
1027+ if (!(item = createItem(modelIndex, asynchronous)))
1028+ break;
1029+ pos += item->height() + m_spacing;
1030+ ++modelIndex;
1031+ changed = true;
1032+ }
1033+
1034+ modelIndex = 0;
1035+ pos = 0;
1036+ if (!m_visibleItems.isEmpty()) {
1037+ modelIndex = m_firstVisibleIndex - 1;
1038+ item = m_visibleItems.first();
1039+ pos = item->y();
1040+ }
1041+ while (modelIndex >= 0 && pos > fillFrom) {
1042+ if (!(item = createItem(modelIndex, asynchronous)))
1043+ break;
1044+ pos -= item->height() + m_spacing;
1045+ --modelIndex;
1046+ changed = true;
1047+ }
1048+
1049+ return changed;
1050+}
1051+
1052+void VerticalView::reallyReleaseItem(ListItem *listItem)
1053+{
1054+ QQuickItem *item = listItem->m_item;
1055+ QQmlDelegateModel::ReleaseFlags flags = m_delegateModel->release(item);
1056+ if (flags & QQmlDelegateModel::Destroyed) {
1057+ item->setParentItem(nullptr);
1058+ }
1059+ delete listItem;
1060+}
1061+
1062+void VerticalView::releaseItem(ListItem *listItem)
1063+{
1064+ QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(listItem->m_item);
1065+ itemPrivate->removeItemChangeListener(this, QQuickItemPrivate::Geometry);
1066+ m_itemsToRelease << listItem;
1067+}
1068+
1069+void VerticalView::updateWatchedRoles()
1070+{
1071+ if (m_delegateModel) {
1072+ QList<QByteArray> roles;
1073+ m_delegateModel->setWatchedRoles(roles);
1074+ }
1075+}
1076+
1077+bool VerticalView::removeNonVisibleItems(qreal bufferFrom, qreal bufferTo)
1078+{
1079+ // Do not remove items if we are overshooting up or down, since we'll come back
1080+ // to the "stable" position and delete/create items without any reason
1081+ if (contentY() < -m_minYExtent) {
1082+ return false;
1083+ } else if (contentY() + height() > contentHeight()) {
1084+ return false;
1085+ }
1086+ bool changed = false;
1087+
1088+ bool foundVisible = false;
1089+ int i = 0;
1090+ int removedItems = 0;
1091+ const auto oldFirstVisibleIndex = m_firstVisibleIndex;
1092+ while (i < m_visibleItems.count()) {
1093+ ListItem *item = m_visibleItems[i];
1094+ const qreal pos = item->y() + m_spacing;
1095+ if (pos + item->height() < bufferFrom || pos > bufferTo) {
1096+ releaseItem(item);
1097+ m_visibleItems.removeAt(i);
1098+ changed = true;
1099+ ++removedItems;
1100+ } else {
1101+ if (!foundVisible) {
1102+ foundVisible = true;
1103+ const int itemIndex = m_firstVisibleIndex + removedItems + i;
1104+ m_firstVisibleIndex = itemIndex;
1105+ }
1106+ ++i;
1107+ }
1108+ }
1109+ if (!foundVisible) {
1110+ m_firstVisibleIndex = -1;
1111+ }
1112+ if (m_firstVisibleIndex != oldFirstVisibleIndex) {
1113+ adjustMinYExtent();
1114+ }
1115+
1116+ return changed;
1117+}
1118+
1119+VerticalView::ListItem *VerticalView::createItem(int modelIndex, bool asynchronous)
1120+{
1121+ if (asynchronous && m_asyncRequestedIndex != -1)
1122+ return nullptr;
1123+
1124+ m_asyncRequestedIndex = -1;
1125+ QObject* object = m_delegateModel->object(modelIndex, asynchronous);
1126+ QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
1127+ if (!item) {
1128+ if (object) {
1129+ m_delegateModel->release(object);
1130+ if (!m_delegateValidated) {
1131+ m_delegateValidated = true;
1132+ QObject* delegateObj = delegate();
1133+ qmlInfo(delegateObj ? delegateObj : this) << "Delegate must be of Item type";
1134+ }
1135+ } else {
1136+ m_asyncRequestedIndex = modelIndex;
1137+ }
1138+ return 0;
1139+ } else {
1140+ ListItem *listItem = new ListItem;
1141+ listItem->m_item = item;
1142+ QQuickItemPrivate::get(item)->addItemChangeListener(this, QQuickItemPrivate::Geometry);
1143+ ListItem *prevItem = itemAtIndex(modelIndex - 1);
1144+ bool lostItem = false; // Is an item that we requested async but because of model changes
1145+ // it is no longer attached to any of the existing items (has no prev nor next item)
1146+ // nor is the first item
1147+ if (prevItem) {
1148+ listItem->setY(prevItem->y() + prevItem->height() + m_spacing);
1149+ } else {
1150+ ListItem *currItem = itemAtIndex(modelIndex);
1151+ if (currItem) {
1152+ // There's something already in m_visibleItems at out index, meaning this is an insert, so attach to its top
1153+ listItem->setY(currItem->y() - listItem->height() - m_spacing);
1154+ } else {
1155+ ListItem *nextItem = itemAtIndex(modelIndex + 1);
1156+ if (nextItem) {
1157+ listItem->setY(nextItem->y() - listItem->height() - m_spacing);
1158+ } else if (modelIndex == 0) {
1159+ listItem->setY(560);
1160+ } else if (!m_visibleItems.isEmpty()) {
1161+ lostItem = true;
1162+ }
1163+ }
1164+ }
1165+ if (lostItem) {
1166+ listItem->setCulled(true);
1167+ releaseItem(listItem);
1168+ listItem = nullptr;
1169+ } else {
1170+ listItem->setCulled(listItem->y() + listItem->height() + m_spacing <= contentY() || listItem->y() >= contentY() + height());
1171+ if (m_visibleItems.isEmpty()) {
1172+ m_visibleItems << listItem;
1173+ } else {
1174+ m_visibleItems.insert(modelIndex - m_firstVisibleIndex, listItem);
1175+ }
1176+ if (m_firstVisibleIndex < 0 || modelIndex < m_firstVisibleIndex) {
1177+ m_firstVisibleIndex = modelIndex;
1178+ polish();
1179+ }
1180+ adjustMinYExtent();
1181+ m_contentHeightDirty = true;
1182+ }
1183+ return listItem;
1184+ }
1185+}
1186+
1187+void VerticalView::_q_itemCreated(int modelIndex, QObject *object)
1188+{
1189+ QQuickItem *item = qmlobject_cast<QQuickItem*>(object);
1190+ if (!item) {
1191+ qWarning() << "VerticalView::itemCreated got a non item for index" << modelIndex;
1192+ return;
1193+ }
1194+
1195+ // Check we are not being taken down and don't paint anything
1196+ // TODO Check if we still need this in 5.2
1197+ // For reproduction just inifnite loop testDash or testDashContent
1198+ if (!QQmlEngine::contextForObject(this)->parentContext())
1199+ return;
1200+
1201+ item->setParentItem(contentItem());
1202+ QQmlContext *context = QQmlEngine::contextForObject(item)->parentContext();
1203+ context->setContextProperty(QLatin1String("VerticalView"), this);
1204+ context->setContextProperty(QLatin1String("heightToClip"), QVariant::fromValue<int>(0));
1205+ if (modelIndex == m_asyncRequestedIndex) {
1206+ createItem(modelIndex, false);
1207+ refill();
1208+ }
1209+}
1210+
1211+void VerticalView::_q_heightChanged()
1212+{
1213+ polish();
1214+}
1215+
1216+void VerticalView::_q_modelUpdated(const QQmlChangeSet &changeSet, bool /*reset*/)
1217+{
1218+ // TODO Do something with reset
1219+ const auto oldFirstVisibleIndex = m_firstVisibleIndex;
1220+
1221+ Q_FOREACH(const QQmlChangeSet::Change &remove, changeSet.removes()) {
1222+ if (remove.index + remove.count > m_firstVisibleIndex && remove.index < m_firstVisibleIndex + m_visibleItems.count()) {
1223+ const qreal oldFirstValidIndexPos = m_visibleItems.first()->y();
1224+ // If all the items we are removing are either not created or culled
1225+ // we have to grow down to avoid viewport changing
1226+ bool growDown = true;
1227+ for (int i = 0; growDown && i < remove.count; ++i) {
1228+ const int modelIndex = remove.index + i;
1229+ ListItem *item = itemAtIndex(modelIndex);
1230+ if (item && !item->culled()) {
1231+ growDown = false;
1232+ }
1233+ }
1234+ for (int i = remove.count - 1; i >= 0; --i) {
1235+ const int visibleIndex = remove.index + i - m_firstVisibleIndex;
1236+ if (visibleIndex >= 0 && visibleIndex < m_visibleItems.count()) {
1237+ ListItem *item = m_visibleItems[visibleIndex];
1238+ releaseItem(item);
1239+ m_visibleItems.removeAt(visibleIndex);
1240+ }
1241+ }
1242+ if (growDown) {
1243+ adjustMinYExtent();
1244+ } else if (remove.index <= m_firstVisibleIndex) {
1245+ if (!m_visibleItems.isEmpty()) {
1246+ // We removed the first item that is the one that positions the rest
1247+ // position the new first item correctly
1248+ m_visibleItems.first()->setY(oldFirstValidIndexPos);
1249+ } else {
1250+ m_firstVisibleIndex = -1;
1251+ }
1252+ }
1253+ } else if (remove.index + remove.count <= m_firstVisibleIndex) {
1254+ m_firstVisibleIndex -= remove.count;
1255+ }
1256+ for (int i = remove.count - 1; i >= 0; --i) {
1257+ const int modelIndex = remove.index + i;
1258+ if (modelIndex == m_asyncRequestedIndex) {
1259+ m_asyncRequestedIndex = -1;
1260+ } else if (modelIndex < m_asyncRequestedIndex) {
1261+ m_asyncRequestedIndex--;
1262+ }
1263+ }
1264+ }
1265+
1266+ Q_FOREACH(const QQmlChangeSet::Change &insert, changeSet.inserts()) {
1267+ const bool insertingInValidIndexes = insert.index > m_firstVisibleIndex && insert.index < m_firstVisibleIndex + m_visibleItems.count();
1268+ const bool firstItemWithViewOnTop = insert.index == 0 && m_firstVisibleIndex == 0 && m_visibleItems.first()->y() > contentY();
1269+ if (insertingInValidIndexes || firstItemWithViewOnTop)
1270+ {
1271+ // If the items we are adding won't be really visible
1272+ // we grow up instead of down to not change the viewport
1273+ bool growUp = false;
1274+ if (!firstItemWithViewOnTop) {
1275+ for (int i = 0; i < m_visibleItems.count(); ++i) {
1276+ if (!m_visibleItems[i]->culled()) {
1277+ if (insert.index <= m_firstVisibleIndex + i) {
1278+ growUp = true;
1279+ }
1280+ break;
1281+ }
1282+ }
1283+ }
1284+
1285+ const qreal oldFirstValidIndexPos = m_visibleItems.first()->y();
1286+ for (int i = insert.count - 1; i >= 0; --i) {
1287+ const int modelIndex = insert.index + i;
1288+ ListItem *item = createItem(modelIndex, false);
1289+ if (growUp) {
1290+ ListItem *firstItem = m_visibleItems.first();
1291+ firstItem->setY(firstItem->y() - item->height());
1292+ }
1293+ }
1294+ if (firstItemWithViewOnTop) {
1295+ ListItem *firstItem = m_visibleItems.first();
1296+ firstItem->setY(oldFirstValidIndexPos);
1297+ }
1298+ adjustMinYExtent();
1299+ } else if (insert.index <= m_firstVisibleIndex) {
1300+ m_firstVisibleIndex += insert.count;
1301+ }
1302+
1303+ for (int i = insert.count - 1; i >= 0; --i) {
1304+ const int modelIndex = insert.index + i;
1305+ if (modelIndex <= m_asyncRequestedIndex) {
1306+ m_asyncRequestedIndex++;
1307+ }
1308+ }
1309+ }
1310+
1311+ if (m_firstVisibleIndex != oldFirstVisibleIndex) {
1312+ adjustMinYExtent();
1313+ }
1314+
1315+ layout();
1316+ polish();
1317+ m_contentHeightDirty = true;
1318+}
1319+
1320+void VerticalView::itemGeometryChanged(QQuickItem * /*item*/, const QRectF &newGeometry, const QRectF &oldGeometry)
1321+{
1322+ const qreal heightDiff = newGeometry.height() - oldGeometry.height();
1323+ if (heightDiff != 0) {
1324+ if (oldGeometry.y() + oldGeometry.height() <= contentY() && !m_visibleItems.isEmpty()) {
1325+ ListItem *firstItem = m_visibleItems.first();
1326+ firstItem->setY(firstItem->y() - heightDiff);
1327+ adjustMinYExtent();
1328+ layout();
1329+ }
1330+ refill();
1331+ adjustMinYExtent();
1332+ polish();
1333+ m_contentHeightDirty = true;
1334+ }
1335+}
1336+
1337+void VerticalView::adjustMinYExtent()
1338+{
1339+ if (m_visibleItems.isEmpty()) {
1340+ m_minYExtent = 0;
1341+ } else {
1342+ qreal nonCreatedHeight = 0;
1343+ if (m_firstVisibleIndex != 0) {
1344+ // Calculate the average height of items to estimate the position of the list start
1345+ const int visibleItems = m_visibleItems.count();
1346+ qreal visibleItemsHeight = 0;
1347+ Q_FOREACH(ListItem *item, m_visibleItems) {
1348+ visibleItemsHeight += item->height() + m_spacing;
1349+ }
1350+ nonCreatedHeight = m_firstVisibleIndex * visibleItemsHeight / visibleItems;
1351+ }
1352+ m_minYExtent = nonCreatedHeight - m_visibleItems.first()->y();
1353+ if (m_minYExtent != 0 && qFuzzyIsNull(m_minYExtent)) {
1354+ m_minYExtent = 0;
1355+ m_visibleItems.first()->setY(nonCreatedHeight);
1356+ }
1357+ }
1358+}
1359+
1360+VerticalView::ListItem *VerticalView::itemAtIndex(int modelIndex) const
1361+{
1362+ const int visibleIndexedModelIndex = modelIndex - m_firstVisibleIndex;
1363+ if (visibleIndexedModelIndex >= 0 && visibleIndexedModelIndex < m_visibleItems.count())
1364+ return m_visibleItems[visibleIndexedModelIndex];
1365+
1366+ return nullptr;
1367+}
1368+
1369+void VerticalView::layout()
1370+{
1371+ if (m_inLayout)
1372+ return;
1373+
1374+ m_inLayout = true;
1375+ if (!m_visibleItems.isEmpty()) {
1376+ const qreal visibleFrom = contentY();
1377+ const qreal visibleTo = contentY() + height();
1378+
1379+ qreal pos = m_visibleItems.first()->y();
1380+
1381+// qDebug() << "VerticalView::layout Updating positions and heights. contentY" << contentY() << "minYExtent" << minYExtent();
1382+ int firstReallyVisibleItem = -1;
1383+ int modelIndex = m_firstVisibleIndex;
1384+ Q_FOREACH(ListItem *item, m_visibleItems) {
1385+ const bool cull = pos + item->height() + m_spacing <= visibleFrom || pos >= visibleTo;
1386+ item->setCulled(cull);
1387+ item->setY(pos);
1388+ if (!cull && firstReallyVisibleItem == -1) {
1389+ firstReallyVisibleItem = modelIndex;
1390+ }
1391+ QQmlContext *context = QQmlEngine::contextForObject(item->m_item)->parentContext();
1392+ const qreal clipFrom = visibleFrom;
1393+ if (!cull && pos < clipFrom) {
1394+ context->setContextProperty(QLatin1String("heightToClip"), clipFrom - pos);
1395+ } else {
1396+ context->setContextProperty(QLatin1String("heightToClip"), QVariant::fromValue<int>(0));
1397+ }
1398+// qDebug() << "VerticalView::layout" << item->m_item;
1399+ pos += item->height() + m_spacing;
1400+ ++modelIndex;
1401+ }
1402+ }
1403+ m_inLayout = false;
1404+}
1405+
1406+void VerticalView::updatePolish()
1407+{
1408+ // Check we are not being taken down and don't paint anything
1409+ // TODO Check if we still need this in 5.2
1410+ // For reproduction just inifnite loop testDash or testDashContent
1411+ if (!QQmlEngine::contextForObject(this)->parentContext())
1412+ return;
1413+
1414+ Q_FOREACH(ListItem *item, m_itemsToRelease)
1415+ reallyReleaseItem(item);
1416+ m_itemsToRelease.clear();
1417+
1418+ if (!model())
1419+ return;
1420+
1421+ layout();
1422+
1423+ refill();
1424+
1425+ if (m_contentHeightDirty) {
1426+ qreal contentHeight;
1427+ if (m_visibleItems.isEmpty()) {
1428+ contentHeight = 0;
1429+ } else {
1430+ const int modelCount = model()->rowCount();
1431+ const int visibleItems = m_visibleItems.count();
1432+ const int lastValidIndex = m_firstVisibleIndex + visibleItems - 1;
1433+ qreal nonCreatedHeight = 0;
1434+ if (lastValidIndex != modelCount - 1) {
1435+ const int visibleItems = m_visibleItems.count();
1436+ qreal visibleItemsHeight = 0;
1437+ Q_FOREACH(ListItem *item, m_visibleItems) {
1438+ visibleItemsHeight += item->height() + m_spacing;
1439+ }
1440+ const int unknownSizes = modelCount - (m_firstVisibleIndex + visibleItems);
1441+ nonCreatedHeight = unknownSizes * visibleItemsHeight / visibleItems;
1442+ }
1443+ ListItem *item = m_visibleItems.last();
1444+ contentHeight = nonCreatedHeight + item->y() + item->height();
1445+ if (m_firstVisibleIndex != 0) {
1446+ // Make sure that if we are shrinking we tell the view we still fit
1447+ m_minYExtent = qMax(m_minYExtent, -(contentHeight - height()));
1448+ }
1449+ }
1450+
1451+ m_contentHeightDirty = false;
1452+ adjustMinYExtent();
1453+ setContentHeight(contentHeight);
1454+ }
1455+}
1456+
1457+#include "moc_verticalview.cpp"
1458
1459=== added file 'src/plugin/poppler-qml-plugin/verticalview.h'
1460--- src/plugin/poppler-qml-plugin/verticalview.h 1970-01-01 00:00:00 +0000
1461+++ src/plugin/poppler-qml-plugin/verticalview.h 2015-02-04 14:35:20 +0000
1462@@ -0,0 +1,167 @@
1463+/*
1464+ * Copyright (C) 2013-2015 Canonical, Ltd.
1465+ *
1466+ * This program is free software; you can redistribute it and/or modify
1467+ * it under the terms of the GNU General Public License as published by
1468+ * the Free Software Foundation; version 3.
1469+ *
1470+ * This program is distributed in the hope that it will be useful,
1471+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
1472+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
1473+ * GNU General Public License for more details.
1474+ *
1475+ * You should have received a copy of the GNU General Public License
1476+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
1477+ */
1478+
1479+#ifndef VERTICALVIEW_H
1480+#define VERTICALVIEW_H
1481+
1482+#include <private/qquickitemchangelistener_p.h>
1483+#include <private/qquickflickable_p.h>
1484+
1485+class QAbstractItemModel;
1486+class QQmlChangeSet;
1487+class QQmlDelegateModel;
1488+
1489+
1490+/**
1491+ Note for users of this class
1492+
1493+ VerticalView already loads delegates async when appropiate so if
1494+ your delegate uses a Loader you should not enable the asynchronous feature since
1495+ that will need to introduce sizing problems
1496+
1497+ With the double async it may happen what while we are scrolling down
1498+ we reach to a point where given the size of the just created delegate with loader not yet loaded (which will be very close to 0)
1499+ we are already "at the end" of the list, but then a few milliseconds later the loader finishes loading and we could
1500+ have kept scrolling. This is specially visible at the end of the list where you realize
1501+ that scrolling ended a bit before the end of the list but the speed of the flicking was good
1502+ to reach the end
1503+
1504+ By not having the second async we get a better sizing when the delegate is created and things work better
1505+*/
1506+
1507+class VerticalView : public QQuickFlickable, public QQuickItemChangeListener
1508+{
1509+ Q_OBJECT
1510+ Q_PROPERTY(QAbstractItemModel *model READ model WRITE setModel NOTIFY modelChanged)
1511+ Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged)
1512+ Q_PROPERTY(int cacheBuffer READ cacheBuffer WRITE setCacheBuffer NOTIFY cacheBufferChanged)
1513+ Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged)
1514+ Q_PROPERTY(int count READ count NOTIFY countChanged)
1515+ Q_PROPERTY(int currentPageIndex READ currentPageIndex NOTIFY currentPageIndexChanged)
1516+ Q_PROPERTY(QQuickItem *currentPageItem READ currentPageItem NOTIFY currentPageItemChanged)
1517+
1518+public:
1519+ VerticalView();
1520+ ~VerticalView();
1521+
1522+ QAbstractItemModel *model() const;
1523+ void setModel(QAbstractItemModel *model);
1524+
1525+ QQmlComponent *delegate() const;
1526+ void setDelegate(QQmlComponent *delegate);
1527+
1528+ int cacheBuffer() const;
1529+ void setCacheBuffer(int cacheBuffer);
1530+
1531+ qreal spacing() const;
1532+ void setSpacing(qreal spacing);
1533+
1534+ int count() const;
1535+
1536+ int currentPageIndex() const;
1537+ QQuickItem *currentPageItem() const;
1538+
1539+ Q_INVOKABLE void positionAtBeginning();
1540+ Q_INVOKABLE void positionAtIndex(int index);
1541+ Q_INVOKABLE void positionAtEnd();
1542+
1543+ Q_INVOKABLE QQuickItem *itemAt(int modelIndex) const;
1544+
1545+Q_SIGNALS:
1546+ void modelChanged();
1547+ void delegateChanged();
1548+ void cacheBufferChanged();
1549+ void spacingChanged();
1550+ void countChanged();
1551+ void currentPageIndexChanged();
1552+ void currentPageItemChanged();
1553+
1554+protected:
1555+ void componentComplete() override;
1556+ void viewportMoved(Qt::Orientations orient) override;
1557+ qreal minYExtent() const override;
1558+ void itemGeometryChanged(QQuickItem *item, const QRectF &newGeometry, const QRectF &oldGeometry) override;
1559+ void updatePolish() override;
1560+
1561+private Q_SLOTS:
1562+ void _q_itemCreated(int modelIndex, QObject *object);
1563+ void _q_heightChanged();
1564+ void _q_modelUpdated(const QQmlChangeSet &changeSet, bool reset);
1565+ void _q_updateCurrentPageIndex();
1566+
1567+private:
1568+ class ListItem
1569+ {
1570+ public:
1571+ qreal height() const;
1572+
1573+ qreal y() const;
1574+ void setY(qreal newY);
1575+
1576+ bool culled() const;
1577+ void setCulled(bool culled);
1578+
1579+ QQuickItem *m_item;
1580+ };
1581+
1582+ void createDelegateModel();
1583+
1584+ void layout();
1585+ void refill();
1586+ bool addVisibleItems(qreal fillFrom, qreal fillTo, bool asynchronous);
1587+ bool removeNonVisibleItems(qreal bufferFrom, qreal bufferTo);
1588+ ListItem *createItem(int modelIndex, bool asynchronous);
1589+
1590+ void adjustMinYExtent();
1591+ ListItem *itemAtIndex(int modelIndex) const; // Returns the item at modelIndex if has been created
1592+ void releaseItem(ListItem *item);
1593+ void reallyReleaseItem(ListItem *item);
1594+ void updateWatchedRoles();
1595+
1596+ QQmlDelegateModel *m_delegateModel;
1597+
1598+ // Index we are waiting because we requested it asynchronously
1599+ int m_asyncRequestedIndex;
1600+
1601+ // Used to only give a warning once if the delegate does not return objects
1602+ bool m_delegateValidated;
1603+
1604+ // Visible indexes, [0] is m_firstValidIndex, [0+1] is m_firstValidIndex +1, ...
1605+ QList<ListItem *> m_visibleItems;
1606+ int m_firstVisibleIndex;
1607+
1608+ int m_currentPageIndex;
1609+
1610+ qreal m_minYExtent;
1611+
1612+ // If any of the heights has changed
1613+ // or new items have been added/removed
1614+ bool m_contentHeightDirty;
1615+
1616+ qreal m_previousContentY;
1617+
1618+ bool m_inLayout;
1619+
1620+ int m_cacheBuffer;
1621+ qreal m_spacing;
1622+
1623+ // Qt 5.0 doesn't like releasing the items just after itemCreated
1624+ // so we delay the releasing until the next updatePolish
1625+ QList<ListItem *> m_itemsToRelease;
1626+};
1627+
1628+
1629+#endif // VERTICALVIEW_H
1630
1631=== modified file 'tests/autopilot/ubuntu_docviewer_app/tests/test_docviewer.py'
1632--- tests/autopilot/ubuntu_docviewer_app/tests/test_docviewer.py 2014-12-14 20:50:23 +0000
1633+++ tests/autopilot/ubuntu_docviewer_app/tests/test_docviewer.py 2015-02-04 14:35:20 +0000
1634@@ -63,7 +63,7 @@
1635 self.launch_app()
1636
1637 pdf = self.app.main_view.select_single(
1638- "QQuickListView", objectName="pdfView")
1639+ "VerticalView", objectName="pdfView")
1640 self.assertThat(pdf.contentHeight,
1641 Eventually(GreaterThan(0)))
1642
1643@@ -78,5 +78,5 @@
1644
1645 self.assertThat(
1646 self.app.main_view.select_single(
1647- "QQuickListView", objectName="pdfView").currentIndex,
1648+ "VerticalView", objectName="pdfView").currentPageIndex,
1649 Eventually(Equals(int(page_no) - 1)))

Subscribers

People subscribed via source and target branches