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

Proposed by Stefano Verzegnassi
Status: Merged
Approved by: Alan Pope 🍺🐧🐱 πŸ¦„
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
Riccardo Padovani (community) Approve
Ubuntu Document Viewer Developers 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

Fixed an error while importing new code in PdfView

76. By Stefano Verzegnassi

Increment version number

Revision history for this message
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote :
review: Needs Fixing (continuous-integration)
77. By Stefano Verzegnassi

Fix broken package

78. By Stefano Verzegnassi

Revert previous commit, try to fix deb package

Revision history for this message
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote :
review: Needs Fixing (continuous-integration)
79. By Stefano Verzegnassi

Reduce PdfView cachebuffer by half

Revision history for this message
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
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
Revision history for this message
Stefano Verzegnassi (verzegnassi-stefano) wrote :

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

Merge with trunk - Updated pot

Revision history for this message
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote :
review: Needs Fixing (continuous-integration)
81. By Stefano Verzegnassi

Merge with trunk - solved conflict in debian/control

Revision history for this message
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote :
review: Needs Fixing (continuous-integration)
82. By Stefano Verzegnassi

Any comma is important, thank you Jenkins for such lesson

Revision history for this message
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote :
review: Needs Fixing (continuous-integration)
83. By Stefano Verzegnassi

Fixed broken debian package building

84. By Stefano Verzegnassi

Fixed broken autopilot tests

Revision history for this message
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote :
review: Needs Fixing (continuous-integration)
85. By Stefano Verzegnassi

Fixed last bits of autopilot test

Revision history for this message
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote :
review: Approve (continuous-integration)

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