Merge lp:~verzegnassi-stefano/ubuntu-docviewer-app/20-enable-zoom into lp:ubuntu-docviewer-app/trunk
- 20-enable-zoom
- Merge into trunk
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 | ||||
Related bugs: |
|
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.
- 75. By Stefano Verzegnassi
-
Fixed an error while importing new code in PdfView
- 76. By Stefano Verzegnassi
-
Increment version number
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote : | # |
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote : | # |
FAILED: Continuous integration, rev:76
http://
Executed test runs:
FAILURE: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 77. By Stefano Verzegnassi
-
Fix broken package
- 78. By Stefano Verzegnassi
-
Revert previous commit, try to fix deb package
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote : | # |
FAILED: Continuous integration, rev:78
http://
Executed test runs:
FAILURE: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 79. By Stefano Verzegnassi
-
Reduce PdfView cachebuffer by half
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote : | # |
FAILED: Continuous integration, rev:79
http://
Executed test runs:
FAILURE: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
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!
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
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote : | # |
FAILED: Continuous integration, rev:80
http://
Executed test runs:
FAILURE: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 81. By Stefano Verzegnassi
-
Merge with trunk - solved conflict in debian/control
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote : | # |
FAILED: Continuous integration, rev:81
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 82. By Stefano Verzegnassi
-
Any comma is important, thank you Jenkins for such lesson
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote : | # |
FAILED: Continuous integration, rev:82
http://
Executed test runs:
FAILURE: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 83. By Stefano Verzegnassi
-
Fixed broken debian package building
- 84. By Stefano Verzegnassi
-
Fixed broken autopilot tests
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote : | # |
FAILED: Continuous integration, rev:84
http://
Executed test runs:
UNSTABLE: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
- 85. By Stefano Verzegnassi
-
Fixed last bits of autopilot test
Ubuntu Phone Apps Jenkins Bot (ubuntu-phone-apps-jenkins-bot) wrote : | # |
PASSED: Continuous integration, rev:85
http://
Executed test runs:
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Preview Diff
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))) |
FAILED: Continuous integration, rev:74 91.189. 93.70:8080/ job/ubuntu- docviewer- app-ci/ 136/ 91.189. 93.70:8080/ job/generic- mediumtests- vivid/912/ console 91.189. 93.70:8080/ job/generic- mediumtests- vivid/912/ artifact/ work/output/ *zip*/output. zip 91.189. 93.70:8080/ job/ubuntu- docviewer- app-vivid- amd64-ci/ 39
http://
Executed test runs:
FAILURE: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild: 91.189. 93.70:8080/ job/ubuntu- docviewer- app-ci/ 136/rebuild
http://