Stefano Verzegnassi has proposed merging lp:~verzegnassi-stefano/ubuntu-docviewer-app/reboot-qsg-impress-support into lp:ubuntu-docviewer-app/reboot with lp:~verzegnassi-stefano/ubuntu-docviewer-app/reboot-lok-qsg-zoom as a prerequisite.
Commit message: * [loviewer] Improved support for presentation document type * [loviewer] Added keyboard shortcuts. * [loviewer] Added an image provider for slides thumbnails, sync'd with RenderEngine * [loviewer] Conditional layout for the presentation view: use a bottom edge or a sidebar to show the list of slides * [loviewer] Moved zoom controls into a separate page head * Updated translation template Requested reviews: Ubuntu Document Viewer Developers (ubuntu-docviewer-dev) For more details, see: https://code.launchpad.net/~verzegnassi-stefano/ubuntu-docviewer-app/reboot-qsg-impress-support/+merge/272146 * [loviewer] Improved support for presentation document type * [loviewer] Added keyboard shortcuts. (PgUp, PgDown, Ctrl+Home, Ctrl+End, Up, Down, Left, Right, Ctrl+Plus, Ctrl+Minus) See src/app/qml/loView/KeybHelper.js * [loviewer] Added an image provider for slides thumbnails, sync'd with RenderEngine * [loviewer] Conditional layout for the presentation view: use a bottom edge or a sidebar to show the list of slides * [loviewer] Moved zoom controls into a separate page head * Updated translation template -- Your team Ubuntu Document Viewer Developers is requested to review the proposed merge of lp:~verzegnassi-stefano/ubuntu-docviewer-app/reboot-qsg-impress-support into lp:ubuntu-docviewer-app/reboot.
=== modified file 'po/com.ubuntu.docviewer.pot' --- po/com.ubuntu.docviewer.pot 2015-09-26 13:46:19 +0000 +++ po/com.ubuntu.docviewer.pot 2015-09-26 13:46:19 +0000 @@ -8,7 +8,7 @@ msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2015-09-22 21:00+0200\n" +"POT-Creation-Date: 2015-09-23 18:40+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Language-Team: LANGUAGE <[email protected]>\n" @@ -254,7 +254,7 @@ #: ../src/app/qml/documentPage/DocumentPageSearchHeader.qml:27 #: ../src/app/qml/loView/LOViewDefaultHeader.qml:79 -#: ../src/app/qml/loView/LOViewPage.qml:186 +#: ../src/app/qml/loView/LOViewPage.qml:191 #: ../src/app/qml/pdfView/PdfViewDefaultHeader.qml:61 #: ../src/app/qml/textView/TextViewDefaultHeader.qml:61 msgid "Back" @@ -355,8 +355,8 @@ msgid "Enable night mode" msgstr "" -#: ../src/app/qml/loView/LOViewPage.qml:41 -#: ../src/app/qml/loView/LOViewPage.qml:184 +#: ../src/app/qml/loView/LOViewPage.qml:34 +#: ../src/app/qml/loView/LOViewPage.qml:189 msgid "Slides" msgstr "" @@ -372,6 +372,11 @@ msgid "Zoom out" msgstr "" +#: ../src/app/qml/loView/SlideControllerPanel.qml:52 +#, qt-format +msgid "Slide %1 of %2" +msgstr "" + #: ../src/app/qml/loView/ZoomSelector.qml:29 msgid "Automatic (Fit width)" msgstr "" === modified file 'src/app/qml/loView/KeybHelper.js' --- src/app/qml/loView/KeybHelper.js 2015-09-26 13:46:19 +0000 +++ src/app/qml/loView/KeybHelper.js 2015-09-26 13:46:19 +0000 @@ -16,42 +16,129 @@ function parseEvent(event) { var pixelDiff = 5; - + + var view = loPageContentLoader.item.loView + var isPresentation = view.document.documentType === LO.Document.PresentationDocument + if (event.key == Qt.Key_PageUp) { - if (loDocument.documentType == LO.Document.PresentationDocument) - loDocument.currentPart -= 1 + if (isPresentation) + view.document.currentPart -= 1 else - loPage.moveView("vertical", -loView.height) + view.moveView("vertical", -view.height) return; } if (event.key == Qt.Key_PageDown) { - if (loDocument.documentType == LO.Document.PresentationDocument) - loDocument.currentPart += 1 + if (isPresentation) + view.document.currentPart += 1 else - loPage.moveView("vertical", loView.height) + view.moveView("vertical", view.height) return; } + if (event.key == Qt.Key_Home) { + if (event.modifiers & Qt.ControlModifier) { + view.contentX = 0 + view.contentY = 0 + view.document.currentPart = 0 + } else { + view.contentX = 0 + view.contentY = 0 + } + } + + if (event.key == Qt.Key_End) { + if (event.modifiers & Qt.ControlModifier) { + view.contentX = view.contentWidth - view.width + view.contentY = view.contentHeight - view.height + console.log(view.document.currentPart, view.document.partsCount - 1) + view.document.currentPart = view.document.partsCount - 1 + } else { + view.contentX = view.contentWidth - view.width + view.contentY = view.contentHeight - view.height + } + } + if (event.key == Qt.Key_Up) { - loPage.moveView("vertical", -pixelDiff) + view.moveView("vertical", -pixelDiff) return; } if (event.key == Qt.Key_Down) { - loPage.moveView("vertical", pixelDiff) + view.moveView("vertical", pixelDiff) return; } if (event.key == Qt.Key_Left) { - loPage.moveView("horizontal", -pixelDiff) + view.moveView("horizontal", -pixelDiff) return; } if (event.key == Qt.Key_Right) { - loPage.moveView("horizontal", pixelDiff) + view.moveView("horizontal", pixelDiff) return; } + + if (event.key == Qt.Key_Plus) { + if (event.modifiers & Qt.ControlModifier) { + view.zoomFactor = Math.max(4.0, view.zoomFactor + 0.25) + } + } + + if (event.key == Qt.Key_Minus) { + if (event.modifiers & Qt.ControlModifier) { + view.zoomFactor = Math.min(0.5, view.zoomFactor - 0.25) + } + } + + + /* + if (event.key == Qt.Key_C) { + if (event.modifiers & Qt.ControlModifier) { + + } + } + */ + + /* + if (event.key == Qt.Key_X) { + if (event.modifiers & Qt.ControlModifier) { + + } + } + */ + + /* + if (event.key == Qt.Key_V) { + if (event.modifiers & Qt.ControlModifier) { + + } + } + */ + + /* + if (event.key == Qt.Key_A) { + if (event.modifiers & Qt.ControlModifier) { + + } + } + */ + + /* + if (event.key == Qt.Key_L) { + if (event.modifiers & Qt.ControlModifier) { + // TODO: Go to page dialog + } + } + */ + + /* + if (event.key == Qt.Key_Question) { + if (event.modifiers & (Qt.ControlModifier | Qt.ShiftModifier)) { + // TODO: Keyboard shortcuts + } + } + */ } === modified file 'src/app/qml/loView/LOViewPage.qml' --- src/app/qml/loView/LOViewPage.qml 2015-09-26 13:46:19 +0000 +++ src/app/qml/loView/LOViewPage.qml 2015-09-26 13:46:19 +0000 @@ -31,6 +31,7 @@ readonly property bool wideWindow: width > units.gu(120) + bottomEdgeTitle: i18n.tr("Slides") bottomEdgeEnabled: { if (!loPageContentLoader.loaded) return false @@ -38,7 +39,6 @@ // else return loPageContentLoader.item.loDocument.documentType == LO.Document.PresentationDocument && !wideWindow } - bottomEdgeTitle: i18n.tr("Slides") Loader { id: loPageContentLoader @@ -70,23 +70,14 @@ Component { id: loPageContentComponent - Item { + FocusScope { id: loPageContent anchors.fill: parent + property alias loDocument: loView.document property alias loView: loView property alias bottomEdgePartsPage: bottomEdgePartsPage - function moveView(axis, diff) { - if (axis == "vertical") { - var maxContentY = Math.max(0, loView.contentHeight - loView.height) - loView.contentY = Math.max(0, Math.min(loView.contentY + diff, maxContentY )) - } else { - var maxContentX = Math.max(0, loView.contentWidth - loView.width) - loView.contentX = Math.max(0, Math.min(loView.contentX + diff, maxContentX )) - } - } - Layouts { id: layouts anchors.fill: parent @@ -108,8 +99,8 @@ left: parent.left } - model: partsModel - visible: model + model: LO.PartsModel { document: loPageContent.loDocument } + visible: model && loDocument.documentType == LO.Document.PresentationDocument width: visible ? units.gu(40) : 0 } @@ -120,7 +111,16 @@ top: parent.top bottom: bottomBarLayoutItem.top } - ItemLayout { item: "loView"; anchors.fill: parent } + + ItemLayout { + item: "loView" + anchors.fill: parent + + // Keyboard events + focus: true + Keys.onPressed: KeybHelper.parseEvent(event) + Component.onCompleted: loPageContent.forceActiveFocus() + } } Item { @@ -153,10 +153,15 @@ clip: true documentPath: file.path + // Keyboard events + focus: true + Keys.onPressed: KeybHelper.parseEvent(event) + Component.onCompleted: { // WORKAROUND: Fix for wrong grid unit size flickDeceleration = 1500 * units.gridUnit / 8 maximumFlickVelocity = 2500 * units.gridUnit / 8 + loPageContent.forceActiveFocus() } Scrollbar { flickableItem: loView; parent: loView.parent } @@ -191,6 +196,7 @@ flickable: null PartsView { + property bool belongsToNestedPage: true anchors.fill: parent model: LO.PartsModel { document: loPageContent.loDocument } } === modified file 'src/app/qml/loView/PartsView.qml' --- src/app/qml/loView/PartsView.qml 2015-09-26 13:46:19 +0000 +++ src/app/qml/loView/PartsView.qml 2015-09-26 13:46:19 +0000 @@ -29,6 +29,7 @@ property bool expanded: true currentIndex: view.model ? view.model.document.currentPart : -1 + highlightMoveDuration: UbuntuAnimation.SnapDuration delegate: ListItemWithActions { id: delegate @@ -45,7 +46,12 @@ onClicked: { view.model.document.currentPart = model.index - pageStack.pop(); + + // Check if the view has been included in a nested page (e.g. + // bottomEdge). If so, close that page and return to the + // main viewer. + if (view.hasOwnProperty("belongsToNestedPage")) + pageStack.pop(); } } @@ -57,6 +63,9 @@ Layout.fillHeight: true Layout.preferredWidth: height fillMode: Image.PreserveAspectFit + // Do not store a cache of the thumbnail, so that we don't show + // thumbnails of a previously loaded document. + cache: false source: "image://lok/part/" + model.index } === modified file 'src/app/qml/loView/SlideControllerPanel.qml' --- src/app/qml/loView/SlideControllerPanel.qml 2015-09-26 13:46:19 +0000 +++ src/app/qml/loView/SlideControllerPanel.qml 2015-09-26 13:46:19 +0000 @@ -34,14 +34,24 @@ Row { anchors.centerIn: parent - spacing: units.gu(2) - - AbstractButton { - width: units.gu(4); height: parent.height - onClicked: loPageContentLoader.item.loDocument.currentPart -= 1 - - Icon { - id: icon + //spacing: units.gu(2) + + AbstractButton { + width: units.gu(4); height: parent.height + onClicked: loPageContentLoader.item.loView.goFirstPart() + + Icon { + anchors.centerIn: parent + width: units.gu(2.5); height: width + name: "go-first" + } + } + + AbstractButton { + width: units.gu(4); height: parent.height + onClicked: loPageContentLoader.item.loView.goPreviousPart() + + Icon { anchors.centerIn: parent width: units.gu(2.5); height: width name: "go-previous" @@ -49,13 +59,12 @@ } Label { - text: "%1 of %2".arg(loPageContentLoader.item.loDocument.currentPart + 1) - .arg(loPageContentLoader.item.loDocument.partsCount) + text: i18n.tr("Slide %1 of %2").arg(loPageContentLoader.item.loDocument.currentPart + 1).arg(loPageContentLoader.item.loDocument.partsCount) } AbstractButton { width: units.gu(4); height: parent.height - onClicked: loPageContentLoader.item.loDocument.currentPart += 1 + onClicked: loPageContentLoader.item.loView.goNextPart() Icon { anchors.centerIn: parent @@ -63,5 +72,16 @@ name: "go-next" } } + + AbstractButton { + width: units.gu(4); height: parent.height + onClicked: loPageContentLoader.item.loView.goLastPart() + + Icon { + anchors.centerIn: parent + width: units.gu(2.5); height: width + name: "go-last" + } + } } } === modified file 'src/plugin/libreofficetoolkit-qml-plugin/lodocument.cpp' --- src/plugin/libreofficetoolkit-qml-plugin/lodocument.cpp 2015-09-26 13:46:19 +0000 +++ src/plugin/libreofficetoolkit-qml-plugin/lodocument.cpp 2015-09-26 13:46:19 +0000 @@ -36,6 +36,7 @@ LODocument::LODocument() : m_path("") + , m_currentPart(-1) , m_document(nullptr) { // This space is intentionally empty. @@ -61,21 +62,18 @@ } int LODocument::currentPart() { - if (!m_document) - return int(-1); - - return m_document->getPart(); + return m_currentPart; } - + void LODocument::setCurrentPart(int index) { if (!m_document) return; - - if (this->currentPart() == index || index < 0 || index > partsCount() - 1) + + if (m_currentPart == index || index < 0 || index > partsCount() - 1) return; - - m_document->setPart(index); + + m_currentPart = index; Q_EMIT currentPartChanged(); } @@ -97,6 +95,8 @@ m_docType = DocumentType(m_document->getDocumentType()); Q_EMIT documentTypeChanged(); + setCurrentPart(m_document->getPart()); + m_document->initializeForRendering(); qDebug() << "Document loaded successfully !"; @@ -127,6 +127,12 @@ // the rect tileSize. QImage LODocument::paintTile(const QSize& canvasSize, const QRect& tileSize, const qreal &zoom) { + if (!m_document) + return QImage(); + + if (m_currentPart != m_document->getPart()) + m_document->setPart(m_currentPart); + QImage result = QImage(canvasSize.width(), canvasSize.height(), QImage::Format_RGB32); #ifdef DEBUG_TILE_BENCHMARK @@ -148,6 +154,52 @@ return result.rgbSwapped(); } +QImage LODocument::paintThumbnail(int part, qreal size) +{ + if (!m_document) + return QImage(); + +#ifdef DEBUG_TILE_BENCHMARK + QElapsedTimer renderTimer; + renderTimer.start(); +#endif + + // This is used by LOPartsImageProvider to temporarily change the current part, + // in order to generate thumbnails. + + // FIXME: Sometimes docviewer crashes at m_document->getPart() when a + // document is being loaded. + if (m_document->getPart() != part) + m_document->setPart(part); + + qreal tWidth = this->documentSize().width(); + qreal tHeight = this->documentSize().height(); + + QSize resultSize; + + if (tWidth > tHeight) { + resultSize.setWidth(size); + resultSize.setHeight(size * tHeight / tWidth); + } else { + resultSize.setHeight(size); + resultSize.setWidth(size * tWidth / tHeight); + } + + QImage result = QImage(resultSize.width(), resultSize.height(), QImage::Format_RGB32); + m_document->paintTile(result.bits(), resultSize.width(), resultSize.height(), + 0, 0, tWidth, tHeight); + + // Restore the active part used for tile rendering. + if (m_currentPart != part) + m_document->setPart(m_currentPart); + +#ifdef DEBUG_TILE_BENCHMARK + qDebug() << "Time to render the thumbnail:" << renderTimer.elapsed() << "ms"; +#endif + + return result.rgbSwapped(); +} + int LODocument::partsCount() { if (!m_document) @@ -164,17 +216,6 @@ return QString::fromLatin1(m_document->getPartName(index)); } -// This is used by LOPartsImageProvider to temporarily change the current part, -// in order to generate thumbnails. -// FIXME: We need to disable tiled rendering when we're generating the thumbnail. -int LODocument::swapCurrentPart(int newPartIndex) -{ - int oldIndex = this->currentPart(); - - m_document->setPart(newPartIndex); - return oldIndex; -} - /* Export the file in a given format: * - url is a mandatory argument. * - format is optional. If not specified, lok will try to get it from the file === modified file 'src/plugin/libreofficetoolkit-qml-plugin/lodocument.h' --- src/plugin/libreofficetoolkit-qml-plugin/lodocument.h 2015-09-26 13:46:19 +0000 +++ src/plugin/libreofficetoolkit-qml-plugin/lodocument.h 2015-09-26 13:46:19 +0000 @@ -60,12 +60,10 @@ QSize documentSize() const; QImage paintTile(const QSize& canvasSize, const QRect& tileSize, const qreal& zoom = 1.0); + QImage paintThumbnail(int part, qreal size); int partsCount(); - QString getPartName(int index) const; - int swapCurrentPart(int newPartIndex); - void setPart(int index); Q_INVOKABLE bool saveAs(QString url, QString format, QString filterOptions); @@ -77,6 +75,7 @@ private: QString m_path; + int m_currentPart; DocumentType m_docType; bool loadDocument(const QString &pathNAme); === modified file 'src/plugin/libreofficetoolkit-qml-plugin/lopartsimageprovider.cpp' --- src/plugin/libreofficetoolkit-qml-plugin/lopartsimageprovider.cpp 2015-09-26 13:46:19 +0000 +++ src/plugin/libreofficetoolkit-qml-plugin/lopartsimageprovider.cpp 2015-09-26 13:46:19 +0000 @@ -16,62 +16,35 @@ #include "lopartsimageprovider.h" #include "lodocument.h" -#include "config.h" -#include "twips.h" - -#include <QDebug> +#include "renderengine.h" LOPartsImageProvider::LOPartsImageProvider(LODocument *document) : QQuickImageProvider(QQuickImageProvider::Image, QQuickImageProvider::ForceAsynchronousImageLoading) -{ - m_document = document; -} + , m_document(document) +{ } QImage LOPartsImageProvider::requestImage(const QString & id, QSize * size, const QSize & requestedSize) { Q_UNUSED(size) - Q_UNUSED(requestedSize) - - if (m_document->documentType() != LODocument::PresentationDocument) - return QImage(); - - // Here's the tricky magic. For getting a thumbnail of a document part - // (e.g. a specific slide in a Impress document), we need to change the - // current active part in LODocument, render the thumbnail, then re-set - // the previous value through lok::Document::setPath(index). + QString type = id.section("/", 0, 0); - if (type != "part") + if (requestedSize.isNull() || type != "part" || + m_document->documentType() != LODocument::PresentationDocument) return QImage(); + // Wait for any in-progress rendering to be completed + while (RenderEngine::instance()->activeTaskCount() != 0) { } + + // Lock the render engine + RenderEngine::instance()->setEnabled(false); + + // Render the part to QImage int partNumber = id.section("/", 1, 1).toInt(); - QImage result; - QSize partSize; - QSize resultSize; - - // Get the current part index and set the index of the part to be rendered. - int currentPart = m_document->swapCurrentPart(partNumber); - - // Get the size of the part - partSize = m_document->documentSize(); - partSize.setHeight(Twips::convertTwipsToPixels(partSize.height())); - partSize.setWidth(Twips::convertTwipsToPixels(partSize.width())); - - // Set the size of the rendered thumbnail - if (partSize.width() > partSize.height()) { - resultSize.setWidth(TILE_SIZE); - resultSize.setHeight(TILE_SIZE * partSize.height() / partSize.width()); - } else { - resultSize.setHeight(TILE_SIZE); - resultSize.setWidth(TILE_SIZE * partSize.width() / partSize.height()); - } - - // Render the part to QImage - result = m_document->paintTile(resultSize, QRect(QPoint(0, 0), partSize)); - - // Re-set the earlier current part - m_document->swapCurrentPart(currentPart); + QImage result = m_document->paintThumbnail(partNumber, 256.0); + + // Unlock the render engine + RenderEngine::instance()->setEnabled(true); return result; - } === modified file 'src/plugin/libreofficetoolkit-qml-plugin/qml/Viewer.qml' --- src/plugin/libreofficetoolkit-qml-plugin/qml/Viewer.qml 2015-09-26 13:46:19 +0000 +++ src/plugin/libreofficetoolkit-qml-plugin/qml/Viewer.qml 2015-09-26 13:46:19 +0000 @@ -32,6 +32,37 @@ view.adjustZoomToWidth(); } + function moveView(axis, diff) + { + if (axis == "vertical") { + var maxContentY = Math.max(0, rootFlickable.contentHeight - rootFlickable.height) + rootFlickable.contentY = Math.max(0, Math.min(rootFlickable.contentY + diff, maxContentY )) + } else { + var maxContentX = Math.max(0, rootFlickable.contentWidth - rootFlickable.width) + rootFlickable.contentX = Math.max(0, Math.min(rootFlickable.contentX + diff, maxContentX )) + } + } + + function goNextPart() + { + document.currentPart = Math.min(document.currentPart + 1, document.partsCount - 1) + } + + function goPreviousPart() + { + document.currentPart = Math.max(0, document.currentPart - 1) + } + + function goFirstPart() + { + document.currentPart = 0 + } + + function goLastPart() + { + document.currentPart = document.partsCount - 1 + } + onDocumentPathChanged: { if (documentPath) view.initializeDocument(documentPath) @@ -51,4 +82,14 @@ parentFlickable: rootFlickable } + + Connections { + target: view.document + + onCurrentPartChanged: { + // Position view at top-left corner + rootFlickable.contentX = 0 + rootFlickable.contentY = 0 + } + } } === modified file 'src/plugin/libreofficetoolkit-qml-plugin/renderengine.cpp' --- src/plugin/libreofficetoolkit-qml-plugin/renderengine.cpp 2015-09-26 13:46:19 +0000 +++ src/plugin/libreofficetoolkit-qml-plugin/renderengine.cpp 2015-09-26 13:46:19 +0000 @@ -6,10 +6,13 @@ RenderEngine::RenderEngine(): QObject(nullptr), - m_activeTaskCount(0) + m_activeTaskCount(0), + m_enabled(true) { int itc = QThread::idealThreadCount(); m_idealThreadCount = itc == -1 ? DefaultIdealThreadCount : itc; + + connect(this, SIGNAL(enabledChanged()), this, SLOT(doNextTask())); } void RenderEngine::enqueueTask(const QSharedPointer<LODocument>& doc, const QRect& area, const qreal &zoom, int id) @@ -43,7 +46,7 @@ qDebug() << " ---- doNextTask" << m_activeTaskCount << m_queue.count(); #endif - if (m_activeTaskCount >= m_idealThreadCount || !m_queue.count()) + if (m_activeTaskCount >= m_idealThreadCount || !m_queue.count() || !m_enabled) return; m_activeTaskCount++; === modified file 'src/plugin/libreofficetoolkit-qml-plugin/renderengine.h' --- src/plugin/libreofficetoolkit-qml-plugin/renderengine.h 2015-09-26 13:46:19 +0000 +++ src/plugin/libreofficetoolkit-qml-plugin/renderengine.h 2015-09-26 13:46:19 +0000 @@ -6,6 +6,7 @@ #include <QSharedPointer> #include <QHash> #include <QQueue> +#include <QAtomicInt> #include "lodocument.h" @@ -45,17 +46,33 @@ return s_instance; } + int activeTaskCount() { return m_activeTaskCount; } + + bool enabled() { return m_enabled.loadAcquire(); } + void setEnabled(bool enabled) { + if (m_enabled.loadAcquire() == enabled) + return; + + m_enabled.storeRelease(enabled); + Q_EMIT enabledChanged(); + } + Q_SIGNALS: void renderFinished(int id, QImage img); + void enabledChanged(); private: Q_INVOKABLE void internalRenderCallback(int id, QImage img); + +private slots: void doNextTask(); private: QQueue<EngineTask> m_queue; int m_activeTaskCount; int m_idealThreadCount; + + QAtomicInt m_enabled; }; #endif // RENDERENGINE_H
-- Mailing list: https://launchpad.net/~ubuntu-touch-coreapps-reviewers Post to : [email protected] Unsubscribe : https://launchpad.net/~ubuntu-touch-coreapps-reviewers More help : https://help.launchpad.net/ListHelp

