← Back to team overview

ubuntu-touch-coreapps-reviewers team mailing list archive

[Merge] lp:~verzegnassi-stefano/ubuntu-docviewer-app/reboot-lok-zoom into lp:ubuntu-docviewer-app/reboot

 

Stefano Verzegnassi has proposed merging lp:~verzegnassi-stefano/ubuntu-docviewer-app/reboot-lok-zoom into lp:ubuntu-docviewer-app/reboot.

Commit message:
Added zooming support (two modes: automatic and manual)

Requested reviews:
  Ubuntu Document Viewer Developers (ubuntu-docviewer-dev)

For more details, see:
https://code.launchpad.net/~verzegnassi-stefano/ubuntu-docviewer-app/reboot-lok-zoom/+merge/267982

Added zooming support (two modes: automatic and manual)

For who will review this MP: please pay attention on invalidation/removal of tiles, which is probably the most "critical" piece of code added with this MP.

UI Design:
TBD, I hope to get some feedback from the UX team.
A bottom panel has been added in the loView page, and it automatically disappear following the behaviour of the page header. This seems a good concept on desktop/tablet, but it may require some change when running on a phone.

The code of the ZoomSelector (QML component) does not look good, I know, but it's anyway good for prototyping. Since that component could even be replaced in future (we don't know yet), I decided not to improve it.
-- 
Your team Ubuntu Document Viewer Developers is requested to review the proposed merge of lp:~verzegnassi-stefano/ubuntu-docviewer-app/reboot-lok-zoom into lp:ubuntu-docviewer-app/reboot.
=== modified file 'src/app/main.cpp'
--- src/app/main.cpp	2015-03-03 16:49:48 +0000
+++ src/app/main.cpp	2015-08-13 18:21:53 +0000
@@ -19,6 +19,7 @@
 
 // Uncomment if you need to use QML analyzer
 // #define QT_QML_DEBUG
+// #include <QtQuick>
 
 #include "docviewer-application.h"
 #include <QDebug>

=== added file 'src/app/qml/loView/BottomPanel.qml'
--- src/app/qml/loView/BottomPanel.qml	1970-01-01 00:00:00 +0000
+++ src/app/qml/loView/BottomPanel.qml	2015-08-13 18:21:53 +0000
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2015 Canonical, Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.3
+import Ubuntu.Components 1.1
+import Ubuntu.Components.ListItems 1.0 as ListItems
+
+Panel {
+    id: bottomPanel
+    anchors {
+        left: parent.left
+        bottom: parent.bottom
+        right: parent.right
+    }
+
+    readonly property bool shouldBeVisible: loPage.header.y > -1
+    onShouldBeVisibleChanged: {
+        if (shouldBeVisible)
+            bottomPanel.open()
+        else
+            bottomPanel.close()
+    }
+
+    height: units.gu(6)
+
+    Rectangle {
+        anchors.fill: parent
+        color: "white"
+
+        ListItems.ThinDivider {
+            anchors {
+                left: parent.left
+                right: parent.right
+                top: parent.top
+            }
+        }
+    }
+}

=== modified file 'src/app/qml/loView/LOView.qml'
--- src/app/qml/loView/LOView.qml	2015-07-04 16:00:33 +0000
+++ src/app/qml/loView/LOView.qml	2015-08-13 18:21:53 +0000
@@ -27,7 +27,7 @@
 
     // Disable header auto-hide.
     // TODO: Show/hide header if a user taps the page
-    flickable: null
+    flickable: loView
 
     // TRANSLATORS: the first argument (%1) refers to the page currently shown on the screen,
     // while the second one (%2) refers to the total pages count.
@@ -72,6 +72,29 @@
         }*/
     }
 
+    BottomPanel {
+        id: bottomPanel
+
+        // TODO: if we'll still be using a bottom panel, when we'll switch to
+        // UITK1.3, we could use the ActionBar component.
+        Row {
+            anchors.fill: parent
+            layoutDirection: Qt.RightToLeft
+
+            PanelButton {
+                iconName: "zoom-out"
+                onClicked: loView.zoomFactor -= 0.1
+            }
+
+            PanelButton {
+                iconName: "zoom-in"
+                onClicked: loView.zoomFactor += 0.1
+            }
+
+            ZoomSelector {}
+        }
+    }
+
     // *** HEADER ***
     state: "default"
     states: LOViewDefaultHeader {

=== modified file 'src/app/qml/loView/LOViewDefaultHeader.qml'
--- src/app/qml/loView/LOViewDefaultHeader.qml	2015-06-26 14:56:23 +0000
+++ src/app/qml/loView/LOViewDefaultHeader.qml	2015-08-13 18:21:53 +0000
@@ -90,7 +90,6 @@
         Action {
             iconName: "search"
             // onTriggered: pageMain.state = "search"
-            //Disable it until we provide search in Poppler plugin.
             enabled: false
         },
 
@@ -98,7 +97,7 @@
             objectName:"gotopage"
             iconName: "browser-tabs"
             text: i18n.tr("Go to page...")
-            onTriggered: PopupUtils.open(Qt.resolvedUrl("LOViewGotoDialog.qml"), targetPage)
+            enabled: false
         },
 
         Action {

=== removed file 'src/app/qml/loView/LOViewDelegate.qml'
--- src/app/qml/loView/LOViewDelegate.qml	2015-06-24 12:04:16 +0000
+++ src/app/qml/loView/LOViewDelegate.qml	1970-01-01 00:00:00 +0000
@@ -1,95 +0,0 @@
-/*
- * Copyright (C) 2013-2015 Canonical, Ltd.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; version 3.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-import QtQuick 2.3
-import Ubuntu.Components 1.1
-
-Rectangle {
-    id: loPage
-
-    property int index: model.index
-    property bool _previewFetched: false
-
-    property alias status: pageImg.status
-
-    width: parent.width
-    height: width * (model.height / model.width)
-    color: "#E6E6E6"
-
-    // Preview page rendering. Used as placeholder while zooming the page.
-    // We generate the low resolution preview from the texture of the PDF page,
-    // so that we can keep page rendering as fast as possible.
-    ShaderEffectSource {
-        id: previewImg
-        anchors.fill: parent
-
-        // We cannot change its opacity or visibility, otherwise the texture will be refreshed,
-        // even if live is false.
-        live: false
-        textureSize: Qt.size(256, 256 * (model.height / model.width))
-    }
-
-    Image {
-        id: pageImg
-        anchors.fill: parent
-
-        source: "image://libreoffice/page/" + index;
-        sourceSize.width: loPage.width
-
-        onStatusChanged: {
-            // This is supposed to run the first time PdfViewDelegate gets the page rendering.
-            if (!_previewFetched) {
-                if (status == Image.Ready) {
-                    previewImg.sourceItem = pageImg
-                    // Re-assign sourceItem property, so the texture is not updated when Image status changes.
-                    previewImg.sourceItem = loPage
-                }
-            }
-        }
-
-        // Request a new page rendering. The order, which pages are requested with, depends on the distance from the currentPage
-        Timer {
-            id: _zoomTimer
-            interval: {
-                var diff = Math.abs(loView.currentPageIndex - model.index)
-                var prov = loDocument.providersNumber * 0.5
-
-                if (diff < prov)
-                    return 0
-                else
-                    return (diff - prov) * 10
-            }
-
-            onTriggered: {
-                pageImg.sourceSize.width = loPage.width;
-            }
-        }
-    }
-
-    // Page rendering depends on the width of PdfViewDelegate.
-    // Because of this, we have multiple callings to ImageProvider while zooming.
-    // Just avoid it.
-    Connections {
-        target: pinchy
-
-        onPinchStarted: _zoomTimer.stop();
-        onPinchUpdated: {
-            // This ensures that page image is not reloaded when the maximumScale or minimumScale has already been reached.
-            if ( !(_zoomHelper.scale >= 2.5 && pinch.scale > 1.0) && !(_zoomHelper.scale <= 1.0 && pinch.scale < 1.0) )
-                pageImg.sourceSize.width = 0;
-        }
-        onPinchFinished: _zoomTimer.restart();
-    }
-}

=== removed file 'src/app/qml/loView/LOViewGotoDialog.qml'
--- src/app/qml/loView/LOViewGotoDialog.qml	2015-06-24 12:04:16 +0000
+++ src/app/qml/loView/LOViewGotoDialog.qml	1970-01-01 00:00:00 +0000
@@ -1,60 +0,0 @@
-/*
- * Copyright (C) 2014-2015 Canonical, Ltd.
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; version 3.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program.  If not, see <http://www.gnu.org/licenses/>.
- */
-
-import QtQuick 2.3
-import Ubuntu.Components 1.1
-import Ubuntu.Components.Popups 1.0
-
-Dialog {
-    id: goToPageDialog
-    objectName:"LOViewGotoDialog"
-
-    title: i18n.tr("Go to page")
-    text: i18n.tr("Choose a page between 1 and %1").arg(loView.count)
-
-    TextField {
-        id: goToPageTextField
-        objectName:"goToPageTextField"
-
-        width: parent.width
-
-        hasClearButton: true
-        inputMethodHints: Qt.ImhFormattedNumbersOnly
-        validator: IntValidator{ bottom: 1; top: loView.count }
-
-        Keys.onReturnPressed: goToPage()
-        Component.onCompleted: forceActiveFocus()
-    }
-
-    Button {
-        objectName:"GOButton"
-        text: i18n.tr("GO!")
-        color: UbuntuColors.green
-
-        enabled: goToPageTextField.acceptableInput
-        onClicked: goToPage()
-    }
-
-    Button {
-        text: i18n.tr("Cancel")
-        onClicked: PopupUtils.close(goToPageDialog)
-    }
-
-    function goToPage() {
-        loView.positionAtIndex((goToPageTextField.text - 1))
-        PopupUtils.close(goToPageDialog)
-    }
-}

=== added file 'src/app/qml/loView/PanelButton.qml'
--- src/app/qml/loView/PanelButton.qml	1970-01-01 00:00:00 +0000
+++ src/app/qml/loView/PanelButton.qml	2015-08-13 18:21:53 +0000
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2015 Canonical, Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.3
+import Ubuntu.Components 1.1
+
+AbstractButton {
+    width: units.gu(4); height: parent.height
+
+    property alias iconName: icon.name
+    property alias iconSource: icon.source
+
+    Icon {
+        id: icon
+        anchors.centerIn: parent
+        width: units.gu(2.5); height: width
+    }
+}

=== added file 'src/app/qml/loView/ZoomSelector.qml'
--- src/app/qml/loView/ZoomSelector.qml	1970-01-01 00:00:00 +0000
+++ src/app/qml/loView/ZoomSelector.qml	2015-08-13 18:21:53 +0000
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2015 Canonical, Ltd.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; version 3.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import QtQuick 2.3
+import Ubuntu.Components 1.1
+import Ubuntu.Components.Popups 1.0
+import DocumentViewer.LibreOffice 1.0 as LibreOffice
+
+TextField {
+    id: textField
+    anchors.verticalCenter: parent.verticalCenter
+    width: units.gu(24)
+
+    property var values: ["auto", 0.5, 0.7, 0.85, 1.0, 1.25, 1.5, 1.75, 2.0, 3.0, 4.0]
+    property var labels: [
+        i18n.tr("Automatic (Fit width)"),
+        "50%", "70%", "85%", "100%", "125%",
+        "150%", "175%","200%", "300%", "400%"
+    ]
+
+    property bool expanded: false
+
+    hasClearButton: true
+    inputMethodHints: Qt.ImhFormattedNumbersOnly
+    validator: IntValidator{ bottom: 50; top: 400 }
+
+    onAccepted: {
+        loView.zoomFactor = parseInt(text) / 100
+        focus = false
+    }
+
+    secondaryItem: AbstractButton {
+        visible: !textField.highlighted
+        id: listButton
+        height: parent.height
+        width: visible ? height : 0
+
+        Rectangle {
+            anchors.left: parent.left
+            anchors.verticalCenter: parent.verticalCenter
+            height: parent.height - units.gu(1)
+
+            width: units.dp(1)
+            color: "Grey"
+            opacity: 0.5
+        }
+
+        Icon {
+            id: _upArrow
+
+            width: units.gu(2)
+            height: width
+            anchors.centerIn: parent
+
+            name: "go-down"
+            color: "Grey"
+            rotation: textField.expanded ? 180 : 0
+
+            Behavior on rotation {
+                UbuntuNumberAnimation {}
+            }
+        }
+
+        onClicked: {
+            textField.expanded = true
+            PopupUtils.open(zoomSelectorDialog, listButton)
+        }
+    }
+
+    onHighlightedChanged: {
+        if (highlighted) {
+            text = parseInt(loView.zoomFactor * 100)
+        } else text = ""
+    }
+
+    Label {
+        anchors.verticalCenter: parent.verticalCenter
+        anchors.left: parent.left
+        anchors.leftMargin: units.gu(2)
+        visible: !textField.highlighted
+        text: loView.zoomMode == LibreOffice.View.FitToWidth ? i18n.tr("Automatic (%1%)").arg(parseInt(loView.zoomFactor*100))
+                                                             : i18n.tr("Zoom: %1%").arg(parseInt(loView.zoomFactor*100))
+    }
+
+    Component {
+        id: zoomSelectorDialog
+        Popover {
+            id: zoomSelectorDialogue
+            autoClose: false
+            contentHeight: units.gu(24)
+            contentWidth: units.gu(24)
+            Component.onDestruction: textField.expanded = false
+
+            // We don't use 'autoClose' property, since we want to propagate
+            // mouse/touch events to other items (e.g. when zoomSelectorDialogue
+            // is visible, and user taps the zoom+ button on its right, we want
+            // the zoom button to receive the event).
+            InverseMouseArea {
+                anchors.fill: parent
+                propagateComposedEvents: true
+
+                onPressed: {
+                    mouse.accepted = false
+                    PopupUtils.close(zoomSelectorDialogue)
+                }
+            }
+
+            OptionSelector {
+                expanded: true
+                width: parent.width
+                containerHeight: units.gu(24)
+                model: textField.labels
+                selectedIndex: {
+                    if (loView.zoomMode == LibreOffice.View.FitToWidth)
+                        return 0
+
+                    for (var i=0; i<textField.values.length; i++) {
+                        if (values[i] == loView.zoomFactor)
+                            return i
+                    }
+                    return -1
+                }
+
+                onSelectedIndexChanged: {
+                    if (selectedIndex == 0) {
+                        loView.adjustZoomToWidth();
+                        return;
+                    }
+
+                    loView.zoomFactor = textField.values[selectedIndex]
+                    PopupUtils.close(zoomSelectorDialogue)
+                }
+            }
+        }
+    }
+}

=== modified file 'src/plugin/libreofficetoolkit-qml-plugin/LOViewer.qml'
--- src/plugin/libreofficetoolkit-qml-plugin/LOViewer.qml	2015-07-26 17:33:51 +0000
+++ src/plugin/libreofficetoolkit-qml-plugin/LOViewer.qml	2015-08-13 18:21:53 +0000
@@ -23,12 +23,22 @@
     property alias document:    view.document
     property alias zoomFactor:  view.zoomFactor
     property alias cacheBuffer: view.cacheBuffer
-
-    contentHeight: view.height * view.zoomFactor
-    contentWidth: view.width * view.zoomFactor
+    property alias zoomMode:    view.zoomMode
+
+    function adjustZoomToWidth()
+    {
+        view.adjustZoomToWidth();
+    }
+
+    // zoomFactor is not used here to set contentSize, since it's all managed
+    // internally, in the LibreOffice.View component.
+    contentHeight: view.height
+    contentWidth: view.width
 
     boundsBehavior: Flickable.StopAtBounds
 
+    Component.onCompleted: adjustZoomToWidth()
+
     LibreOffice.View {
         id: view
 

=== modified file 'src/plugin/libreofficetoolkit-qml-plugin/lodocument.cpp'
--- src/plugin/libreofficetoolkit-qml-plugin/lodocument.cpp	2015-07-23 01:05:20 +0000
+++ src/plugin/libreofficetoolkit-qml-plugin/lodocument.cpp	2015-08-13 18:21:53 +0000
@@ -102,15 +102,15 @@
 
 // Paint a tile, with size=canvasSize, of the part of the document defined by
 // the rect tileSize.
-QImage LODocument::paintTile(QSize canvasSize, QRect tileSize)
+QImage LODocument::paintTile(QSize canvasSize, QRect tileSize, qreal zoom)
 {
     QImage result = QImage(canvasSize.width(), canvasSize.height(),  QImage::Format_RGB32);
     m_document->paintTile(result.bits(),
                           canvasSize.width(), canvasSize.height(),
-                          Twips::convertPixelsToTwips(tileSize.x()),
-                          Twips::convertPixelsToTwips(tileSize.y()),
-                          Twips::convertPixelsToTwips(tileSize.width()),
-                          Twips::convertPixelsToTwips(tileSize.height()));
+                          Twips::convertPixelsToTwips(tileSize.x(), zoom),
+                          Twips::convertPixelsToTwips(tileSize.y(), zoom),
+                          Twips::convertPixelsToTwips(tileSize.width(), zoom),
+                          Twips::convertPixelsToTwips(tileSize.height(), zoom));
 
     return result.rgbSwapped();
 }

=== modified file 'src/plugin/libreofficetoolkit-qml-plugin/lodocument.h'
--- src/plugin/libreofficetoolkit-qml-plugin/lodocument.h	2015-07-23 01:05:20 +0000
+++ src/plugin/libreofficetoolkit-qml-plugin/lodocument.h	2015-08-13 18:21:53 +0000
@@ -52,7 +52,7 @@
     DocumentType documentType() const;
 
     QSize documentSize() const;
-    QImage paintTile(QSize canvasSize, QRect tileSize);
+    QImage paintTile(QSize canvasSize, QRect tileSize, qreal zoom = 1.0);
 
     Q_INVOKABLE bool saveAs(QString url, QString format, QString filterOptions);
 

=== modified file 'src/plugin/libreofficetoolkit-qml-plugin/loview.cpp'
--- src/plugin/libreofficetoolkit-qml-plugin/loview.cpp	2015-07-26 17:33:51 +0000
+++ src/plugin/libreofficetoolkit-qml-plugin/loview.cpp	2015-08-13 18:21:53 +0000
@@ -28,6 +28,13 @@
 // TODO: Use a QQuickItem and implement painting through
 // updatePaintNode(QSGNode * oldNode, UpdatePaintNodeData * data)
 
+static qreal zoomValueToFitWidth;
+
+static qreal getZoomToFitWidth(const qreal &width, int documentWidth)
+{
+    return qreal(width / Twips::convertTwipsToPixels(documentWidth, 1.0));
+}
+
 LOView::LOView(QQuickItem *parent)
     : QQuickPaintedItem(parent)
     , m_parentFlickable(nullptr)
@@ -38,6 +45,11 @@
     , m_bufferArea(0, 0, 0, 0)
 {
     Q_UNUSED(parent)   
+    m_updateTimer.setSingleShot(true);
+
+    // FIXME: TESTING
+    setOpaquePainting(true);
+    setFillColor(Qt::white);
 
     connect(this, SIGNAL(documentChanged()), this, SLOT(updateViewSize()));
     connect(this, SIGNAL(zoomFactorChanged()), this, SLOT(updateViewSize()));
@@ -100,22 +112,38 @@
     Q_EMIT documentChanged();
 }
 
-// Not used yet.
 qreal LOView::zoomFactor() const
 {
     return m_zoomFactor;
 }
 
-// Not used yet.
-void LOView::setZoomFactor(qreal zoom)
+void LOView::setZoomFactor(const qreal zoom)
 {
     if (m_zoomFactor == zoom)
         return;
 
     m_zoomFactor = zoom;
+
+    if (this->zoomFactor() != zoomValueToFitWidth)
+        this->setZoomMode(LOView::Manual);
+
     Q_EMIT zoomFactorChanged();
 }
 
+LOView::ZoomMode LOView::zoomMode() const
+{
+    return m_zoomMode;
+}
+
+void LOView::setZoomMode(const ZoomMode zoomMode)
+{
+    if (m_zoomMode == zoomMode)
+        return;
+
+    m_zoomMode = zoomMode;
+    Q_EMIT zoomModeChanged();
+}
+
 int LOView::cacheBuffer() const
 {
     return m_cacheBuffer;
@@ -130,6 +158,37 @@
     Q_EMIT cacheBufferChanged();
 }
 
+void LOView::adjustZoomToWidth()
+{
+    this->setZoomMode(LOView::FitToWidth);
+
+    zoomValueToFitWidth = getZoomToFitWidth(parentFlickable()->width(),
+                                            document()->documentSize().width());
+
+    this->setZoomFactor(zoomValueToFitWidth);
+    qDebug() << "Adjust zoom to width - value:" << zoomValueToFitWidth;
+}
+
+bool LOView::updateZoomIfAutomatic()
+{
+    // This function is only used in LOView::updateVisibleRect()
+    // It returns a bool, so that we can stop the execution of that function,
+    // which will be triggered again when we'll automatically update the zoom value.
+    if (this->zoomMode() == LOView::FitToWidth) {
+        zoomValueToFitWidth = getZoomToFitWidth(parentFlickable()->width(),
+                                                document()->documentSize().width());
+
+        if (this->zoomFactor() != zoomValueToFitWidth) {
+            this->setZoomFactor(zoomValueToFitWidth);
+
+            qDebug() << "Adjust automatic zoom to width - value:" << zoomValueToFitWidth;
+            return true;
+        }
+    }
+
+    return false;
+}
+
 // Update the size of LOView, according to the size of the loaded document.
 void LOView::updateViewSize()
 {
@@ -139,8 +198,8 @@
     QSize docSize = m_document->documentSize();
 
     // FIXME: Area may become too large, resulting in a black texture.
-    this->setWidth(Twips::convertTwipsToPixels(docSize.width()) * m_zoomFactor);
-    this->setHeight(Twips::convertTwipsToPixels(docSize.height()) * m_zoomFactor);
+    this->setWidth(Twips::convertTwipsToPixels(docSize.width(), m_zoomFactor));
+    this->setHeight(Twips::convertTwipsToPixels(docSize.height(), m_zoomFactor));
 
     // TODO: Consider to use connections to widthChanged and heightChanged
     this->updateVisibleRect();
@@ -154,6 +213,30 @@
     if (!m_parentFlickable)
         return;
 
+    // Changes in parentFlickable width/height trigger directly LOView::updateVisibleRect(),
+    // since they don't imply a change in the zoom factor - i.e. LOView::updateViewSize().
+    // Anyway, this class also handle an automatic zoom when the parentFlickable has been
+    // resized, so we need to take care of it.
+    // updateZoomIfAutomatic() returns a bool, which is true when the zoomFactor is
+    // set to a new value.
+    // If that happens, stop the execution of this function, since the change of
+    // zoomFactor will trigger the updateViewSize() function, which triggers this
+    // function again.
+    if (this->updateZoomIfAutomatic())
+        return;
+
+    // Check if current tiles have a different zoom value
+    if (!m_tiles.isEmpty()) {
+        TileItem* tile = m_tiles.first();
+
+        if (tile->zoomFactor() != this->zoomFactor()) {
+            m_tiles.clear();
+#ifdef DEBUG_VERBOSE
+            qDebug() << "Zoom value of tiles is different than the current zoom value. Erasing cache...";
+#endif
+        }
+    }
+
     // Update information about the visible area
     m_visibleArea.setRect(m_parentFlickable->property("contentX").toInt(),
                           m_parentFlickable->property("contentY").toInt(),
@@ -194,7 +277,7 @@
     int visiblesToWidth         = qCeil(qreal(m_visibleArea.right()) / TILE_SIZE);
     int visiblesToHeight        = qCeil(qreal(m_visibleArea.bottom()) / TILE_SIZE);
 
-    // Get indexes for tiles in the visible area
+    // Get indexes for tiles in the buffer area
     int bufferFromWidth         = int(m_bufferArea.left() / TILE_SIZE);
     int bufferFromHeight        = int(m_bufferArea.top() / TILE_SIZE);
     int bufferToWidth           = qCeil(qreal(m_bufferArea.right()) / TILE_SIZE);
@@ -223,7 +306,7 @@
         qDebug() << "Creating tile indexed as" << index;
 #endif
 
-        auto tile = new TileItem(rect, m_document);
+        auto tile = new TileItem(rect, m_document, m_zoomFactor);
         connect(tile, SIGNAL(textureChanged()), this, SLOT(update()));
         tile->requestTexture();
 
@@ -232,7 +315,7 @@
     }
 #ifdef DEBUG_VERBOSE
     else {
-        qDebug() << "tile" << x << "x" << y << "already exists";
+        qDebug() << "tile" << index << "already exists";
     }
 #endif
 }

=== modified file 'src/plugin/libreofficetoolkit-qml-plugin/loview.h'
--- src/plugin/libreofficetoolkit-qml-plugin/loview.h	2015-07-26 17:33:51 +0000
+++ src/plugin/libreofficetoolkit-qml-plugin/loview.h	2015-08-13 18:21:53 +0000
@@ -26,17 +26,23 @@
 class LOView : public QQuickPaintedItem
 {
     Q_OBJECT
+    Q_ENUMS(ZoomMode)
     Q_PROPERTY(QQuickItem* parentFlickable READ parentFlickable WRITE setParentFlickable NOTIFY parentFlickableChanged)
     Q_PROPERTY(LODocument* document        READ document        WRITE setDocument        NOTIFY documentChanged)
 
-    // TODO: Implement zoom!
     Q_PROPERTY(qreal       zoomFactor      READ zoomFactor      WRITE setZoomFactor      NOTIFY zoomFactorChanged)
+    Q_PROPERTY(ZoomMode    zoomMode        READ zoomMode                                 NOTIFY zoomModeChanged)
     Q_PROPERTY(int         cacheBuffer     READ cacheBuffer     WRITE setCacheBuffer     NOTIFY cacheBufferChanged)
 
 public:
     LOView(QQuickItem *parent = 0);
     ~LOView();
 
+    enum ZoomMode {
+        FitToWidth,
+        Manual
+    };
+
     void        paint(QPainter *painter);
 
     QQuickItem* parentFlickable() const;
@@ -46,15 +52,20 @@
     void        setDocument(LODocument* doc);
 
     qreal       zoomFactor() const;
-    void        setZoomFactor(qreal zoom);
+    void        setZoomFactor(const qreal zoom);
+
+    ZoomMode    zoomMode() const;
 
     int         cacheBuffer() const;
     void        setCacheBuffer(int cacheBuffer);
 
+    Q_INVOKABLE void adjustZoomToWidth();
+
 Q_SIGNALS:
     void parentFlickableChanged();
     void documentChanged();
     void zoomFactorChanged();
+    void zoomModeChanged();
     void cacheBufferChanged();
 
 private Q_SLOTS:
@@ -67,6 +78,7 @@
     LODocument*             m_document;
 
     qreal                   m_zoomFactor;
+    ZoomMode                m_zoomMode;
     int                     m_cacheBuffer;
 
     QRect                   m_visibleArea;
@@ -76,6 +88,9 @@
 
     QMap<int, TileItem*>    m_tiles;
 
+    void                    setZoomMode(const ZoomMode zoomMode);
+    bool                    updateZoomIfAutomatic();
+
     void                    generateTiles(int x1, int y1, int x2, int y2, int tilesPerWidth);
     void                    createTile(int index, QRect rect);
 };

=== modified file 'src/plugin/libreofficetoolkit-qml-plugin/tileitem.cpp'
--- src/plugin/libreofficetoolkit-qml-plugin/tileitem.cpp	2015-07-22 16:44:39 +0000
+++ src/plugin/libreofficetoolkit-qml-plugin/tileitem.cpp	2015-08-13 18:21:53 +0000
@@ -21,6 +21,7 @@
 #include "config.h"
 
 #include <QThreadPool>
+#include <QMutex>
 #include <QDebug>
 
 #ifdef DEBUG_TILE_BENCHMARK
@@ -31,17 +32,21 @@
  * TileItem class *
  ******************/
 
-TileItem::TileItem(const QRect &area, LODocument *document)
-    : m_painted(false)
+TileItem::TileItem(const QRect &area, LODocument *document, const qreal &zoom)
+    : m_zoomFactor(1.0)
+    , m_painted(false)
     , m_document(nullptr)
 {
     this->setArea(area);
+    this->setZoomFactor(zoom);
     this->setDocument(document);
 }
 
 // Destructor
 TileItem::~TileItem()
 {
+    delete m_task;
+
     this->releaseTexture();
 }
 
@@ -64,6 +69,20 @@
     return m_texture;
 }
 
+qreal TileItem::zoomFactor() const
+{
+    return m_zoomFactor;
+}
+
+void TileItem::setZoomFactor(const qreal &zoom)
+{
+    if (m_zoomFactor == zoom)
+        return;
+
+    m_zoomFactor = zoom;
+    Q_EMIT zoomFactorChanged();
+}
+
 bool TileItem::isPainted() const
 {
     return m_painted;
@@ -94,11 +113,11 @@
 
 void TileItem::requestTexture()
 {
-    auto task = new RenderTask(this->area(), this->document());
-    connect(task, SIGNAL(renderCompleted(QImage)), this, SLOT(updateTexture(QImage)));
+    m_task = new RenderTask(this->area(), this->document(), this->zoomFactor());
+    connect(m_task, SIGNAL(renderCompleted(QImage)), this, SLOT(updateTexture(QImage)), Qt::QueuedConnection);
 
-    task->setAutoDelete(true);
-    QThreadPool::globalInstance()->start(task);
+    m_task->setAutoDelete(true);
+    QThreadPool::globalInstance()->start(m_task);
 }
 
 // Free memory used by the texture
@@ -111,6 +130,14 @@
     Q_EMIT textureChanged();
 }
 
+void TileItem::cancelRenderTask()
+{
+    if (!m_task)
+        return;
+
+    QMetaObject::invokeMethod(m_task, "cancel", Qt::QueuedConnection);
+}
+
 // This is a slot, connect to renderCompleted() signal from RenderTask class.
 void TileItem::updateTexture(QImage t)
 {
@@ -122,13 +149,14 @@
  * RenderTask class *
  ********************/
 
-RenderTask::RenderTask(const QRect &area, LODocument* document)
+RenderTask::RenderTask(const QRect &area, LODocument* document, const qreal &zoom)
+    : m_aborted(false)
 {
     this->setArea(area);
+    this->setZoomFactor(zoom);
     this->setDocument(document);
 }
 
-
 QRect RenderTask::area() const
 {
     return m_area;
@@ -143,6 +171,20 @@
     Q_EMIT areaChanged();
 }
 
+qreal RenderTask::zoomFactor() const
+{
+    return m_zoomFactor;
+}
+
+void RenderTask::setZoomFactor(const qreal &zoom)
+{
+    if (m_zoomFactor == zoom)
+        return;
+
+    m_zoomFactor = zoom;
+    Q_EMIT zoomFactorChanged();
+}
+
 LODocument* RenderTask::document() const
 {
     return m_document;
@@ -160,13 +202,24 @@
 // Render the texture for this tile.
 void RenderTask::run()
 {
+    QMutex mutex;
+    mutex.lock();
+
+    if (m_aborted) {
+        mutex.unlock();
+        return;
+    }
+
+    mutex.unlock();
+
 #ifdef DEBUG_TILE_BENCHMARK
     QElapsedTimer renderTimer;
     renderTimer.start();
 #endif
 
     QImage render = this->document()->paintTile(this->area().size(),
-                                                this->area());
+                                                this->area(),
+                                                this->zoomFactor());
 
     Q_EMIT renderCompleted(render);
 

=== modified file 'src/plugin/libreofficetoolkit-qml-plugin/tileitem.h'
--- src/plugin/libreofficetoolkit-qml-plugin/tileitem.h	2015-07-13 23:49:43 +0000
+++ src/plugin/libreofficetoolkit-qml-plugin/tileitem.h	2015-08-13 18:21:53 +0000
@@ -24,12 +24,44 @@
 
 class LODocument;
 
+class RenderTask : public QObject, public QRunnable
+{
+    Q_OBJECT
+
+public:
+    RenderTask(const QRect &area, LODocument* document, const qreal &zoom);
+
+    QRect area() const;
+    void setArea(const QRect &area);
+
+    qreal zoomFactor() const;
+    void setZoomFactor(const qreal &zoom);
+
+    LODocument* document() const;
+    void setDocument(LODocument* document);
+
+    void run();
+    void cancel() { m_aborted = true; }
+
+Q_SIGNALS:
+    void areaChanged();
+    void zoomFactorChanged();
+    void documentChanged();
+    void renderCompleted(QImage t);
+
+private:
+    QRect m_area;
+    qreal m_zoomFactor;
+    LODocument* m_document;
+    bool m_aborted;
+};
+
 class TileItem : public QObject
 {
     Q_OBJECT
 
 public:
-    TileItem(const QRect &area, LODocument* document);
+    TileItem(const QRect &area, LODocument* document, const qreal &zoom);
     ~TileItem();
 
     QRect area() const;
@@ -37,6 +69,9 @@
 
     QImage texture() const;
 
+    qreal zoomFactor() const;
+    void setZoomFactor(const qreal &zoom);
+
     bool isPainted() const;
     void setPainted(bool isPainted);
 
@@ -47,9 +82,13 @@
     void requestTexture();
     void releaseTexture();
 
+    // TODO: Do we want to make it "markAsInvalid()", so we can request a new Runnable? If so, we don't need to return them...
+    void cancelRenderTask();
+
 Q_SIGNALS:
     void areaChanged();
     void textureChanged();
+    void zoomFactorChanged();
     void isPaintedChanged();
     void documentChanged();
 
@@ -59,34 +98,11 @@
 private:
     QRect m_area;
     QImage m_texture;
+    qreal m_zoomFactor;
     bool m_painted;
 
     LODocument* m_document;
-};
-
-class RenderTask : public QObject, public QRunnable
-{
-    Q_OBJECT
-
-public:
-    RenderTask(const QRect &area, LODocument* document);
-
-    QRect area() const;
-    void setArea(const QRect &area);
-
-    LODocument* document() const;
-    void setDocument(LODocument* document);
-
-    void run();
-
-Q_SIGNALS:
-    void areaChanged();
-    void documentChanged();
-    void renderCompleted(QImage t);
-
-private:
-    QRect m_area;
-    LODocument* m_document;
+    RenderTask* m_task;
 };
 
 #endif // TILEITEM_H


Follow ups