From 6104c716c7677cf705787a0be4ffb0adc2b8b5b8 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Wed, 29 Jan 2020 22:17:37 +0100 Subject: doc: rename QML type Document -> PdfDocument I would prefer to use the "import as" mechanism to allow the user to customize prefixes rather than hard-coding them, actually; but currently every QML type in the QtPDF module has a hard-coded Pdf prefix. Change-Id: I4c00e7891c58e028280599d6089a821b9e285c1a Reviewed-by: Leena Miettinen --- src/pdf/quick/qquickpdfdocument.cpp | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/qquickpdfdocument.cpp b/src/pdf/quick/qquickpdfdocument.cpp index 73b3d4537..6eb9d3ae4 100644 --- a/src/pdf/quick/qquickpdfdocument.cpp +++ b/src/pdf/quick/qquickpdfdocument.cpp @@ -43,14 +43,14 @@ QT_BEGIN_NAMESPACE /*! - \qmltype Document + \qmltype PdfDocument \instantiates QQuickPdfDocument \inqmlmodule QtQuick.Pdf \ingroup pdf \brief A representation of a PDF document. \since 5.15 - A Document provides access to PDF document meta-information. + PdfDocument provides access to PDF document meta-information. It is not necessary for rendering, as it is enough to use an \l Image with source set to the URL of the PDF. */ @@ -78,7 +78,7 @@ void QQuickPdfDocument::componentComplete() } /*! - \qmlproperty url Document::source + \qmlproperty url PdfDocument::source This property holds a URL pointing to the PDF file to be loaded. @@ -95,7 +95,7 @@ void QQuickPdfDocument::setSource(QUrl source) } /*! - \qmlproperty string Document::error + \qmlproperty string PdfDocument::error This property holds a translated string representation of the current error, if any. @@ -130,7 +130,7 @@ QString QQuickPdfDocument::error() const } /*! - \qmlproperty bool Document::password + \qmlproperty bool PdfDocument::password This property holds the document password. If the passwordRequired() signal is emitted, the UI should prompt the user and then set this @@ -146,13 +146,13 @@ void QQuickPdfDocument::setPassword(const QString &password) } /*! - \qmlproperty int Document::pageCount + \qmlproperty int PdfDocument::pageCount This property holds the number of pages the PDF contains. */ /*! - \qmlsignal Document::passwordRequired() + \qmlsignal PdfDocument::passwordRequired() This signal is emitted when the PDF requires a password in order to open. The UI in a typical PDF viewer should prompt the user for the password @@ -160,7 +160,7 @@ void QQuickPdfDocument::setPassword(const QString &password) */ /*! - \qmlmethod size Document::pagePointSize(int page) + \qmlmethod size PdfDocument::pagePointSize(int page) Returns the size of the given \a page in points. */ @@ -170,59 +170,59 @@ QSizeF QQuickPdfDocument::pagePointSize(int page) const } /*! - \qmlproperty string Document::title + \qmlproperty string PdfDocument::title This property holds the document's title. A typical viewer UI can bind this to the \c Window.title property. */ /*! - \qmlproperty string Document::author + \qmlproperty string PdfDocument::author This property holds the name of the person who created the document. */ /*! - \qmlproperty string Document::subject + \qmlproperty string PdfDocument::subject This property holds the subject of the document. */ /*! - \qmlproperty string Document::keywords + \qmlproperty string PdfDocument::keywords This property holds the keywords associated with the document. */ /*! - \qmlproperty string Document::creator + \qmlproperty string PdfDocument::creator If the document was converted to PDF from another format, this property holds the name of the software that created the original document. */ /*! - \qmlproperty string Document::producer + \qmlproperty string PdfDocument::producer If the document was converted to PDF from another format, this property holds the name of the software that converted it to PDF. */ /*! - \qmlproperty string Document::creationDate + \qmlproperty string PdfDocument::creationDate This property holds the date and time the document was created. */ /*! - \qmlproperty string Document::modificationDate + \qmlproperty string PdfDocument::modificationDate This property holds the date and time the document was most recently modified. */ /*! - \qmlproperty enum Document::status + \qmlproperty enum PdfDocument::status This property tells the current status of the document. The possible values are: -- cgit v1.2.3 From bc1d6ddeb5076f68e0a758725a20c3f2a6d081f0 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Mon, 20 Jan 2020 18:29:05 +0100 Subject: Add QPdfSelection and QQuickPdfSelection So now you can select text by mouse-drag and copy it to the clipboard. Task-number: QTBUG-77509 Change-Id: I689ee4158974de8bc541c319a5a5cc2f8f3c2ae6 Reviewed-by: Michal Klocek --- src/pdf/quick/plugin.cpp | 2 + src/pdf/quick/qml/PdfPageView.qml | 30 ++++ src/pdf/quick/qquickpdfsearchmodel_p.h | 1 + src/pdf/quick/qquickpdfselection.cpp | 268 +++++++++++++++++++++++++++++++++ src/pdf/quick/qquickpdfselection_p.h | 122 +++++++++++++++ src/pdf/quick/quick.pro | 2 + 6 files changed, 425 insertions(+) create mode 100644 src/pdf/quick/qquickpdfselection.cpp create mode 100644 src/pdf/quick/qquickpdfselection_p.h (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/plugin.cpp b/src/pdf/quick/plugin.cpp index 664ba51ab..72aa12d6a 100644 --- a/src/pdf/quick/plugin.cpp +++ b/src/pdf/quick/plugin.cpp @@ -40,6 +40,7 @@ #include #include "qquickpdfdocument_p.h" #include "qquickpdfsearchmodel_p.h" +#include "qquickpdfselection_p.h" QT_BEGIN_NAMESPACE @@ -81,6 +82,7 @@ public: qmlRegisterType(uri, 5, 15, "PdfDocument"); qmlRegisterType(uri, 5, 15, "PdfSearchModel"); + qmlRegisterType(uri, 5, 15, "PdfSelection"); qmlRegisterType(QUrl("qrc:/qt-project.org/qtpdf/qml/PdfPageView.qml"), uri, 5, 15, "PdfPageView"); } diff --git a/src/pdf/quick/qml/PdfPageView.qml b/src/pdf/quick/qml/PdfPageView.qml index b7f75f4c2..2f9c5ef99 100644 --- a/src/pdf/quick/qml/PdfPageView.qml +++ b/src/pdf/quick/qml/PdfPageView.qml @@ -51,10 +51,23 @@ Rectangle { property alias currentPage: image.currentFrame property alias pageCount: image.frameCount property alias searchString: searchModel.searchString + property alias selectedText: selection.text property alias status: image.status property real __pageScale: image.paintedWidth / document.pagePointSize(image.currentFrame).width + PdfSelection { + id: selection + document: paper.document + page: image.currentFrame + fromPoint: Qt.point(textSelectionDrag.centroid.pressPosition.x / paper.__pageScale, textSelectionDrag.centroid.pressPosition.y / paper.__pageScale) + toPoint: Qt.point(textSelectionDrag.centroid.position.x / paper.__pageScale, textSelectionDrag.centroid.position.y / paper.__pageScale) + hold: !textSelectionDrag.active && !tapHandler.pressed + } + function copySelectionToClipboard() { + selection.copyToClipboard() + } + PdfSearchModel { id: searchModel document: paper.document @@ -96,6 +109,14 @@ Rectangle { paths: searchModel.matchGeometry } } + ShapePath { + fillColor: "orange" + scale: Qt.size(paper.__pageScale, paper.__pageScale) + PathMultiline { + id: selectionBoundaries + paths: selection.geometry + } + } } PinchHandler { id: pinch @@ -116,4 +137,13 @@ Rectangle { acceptedButtons: Qt.MiddleButton snapMode: DragHandler.NoSnap } + DragHandler { + id: textSelectionDrag + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + target: null + } + TapHandler { + id: tapHandler + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + } } diff --git a/src/pdf/quick/qquickpdfsearchmodel_p.h b/src/pdf/quick/qquickpdfsearchmodel_p.h index 799ef825f..82a6289d0 100644 --- a/src/pdf/quick/qquickpdfsearchmodel_p.h +++ b/src/pdf/quick/qquickpdfsearchmodel_p.h @@ -99,5 +99,6 @@ private: QT_END_NAMESPACE QML_DECLARE_TYPE(QQuickPdfSearchModel) +QML_DECLARE_TYPE(QPdfSelection) #endif // QQUICKPDFSEARCHMODEL_P_H diff --git a/src/pdf/quick/qquickpdfselection.cpp b/src/pdf/quick/qquickpdfselection.cpp new file mode 100644 index 000000000..d313820ba --- /dev/null +++ b/src/pdf/quick/qquickpdfselection.cpp @@ -0,0 +1,268 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtPDF module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qquickpdfselection_p.h" +#include "qquickpdfdocument_p.h" +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +/*! + \qmltype PdfSelection + \instantiates QQuickPdfSelection + \inqmlmodule QtQuick.Pdf + \ingroup pdf + \brief A representation of a text selection within a PDF Document. + \since 5.15 + + PdfSelection provides the text string and its geometry within a bounding box + from one point to another. +*/ + +/*! + Constructs a SearchModel. +*/ +QQuickPdfSelection::QQuickPdfSelection(QObject *parent) + : QObject(parent) +{ +} + +QQuickPdfDocument *QQuickPdfSelection::document() const +{ + return m_document; +} + +void QQuickPdfSelection::setDocument(QQuickPdfDocument *document) +{ + if (m_document == document) + return; + + if (m_document) { + disconnect(m_document, &QQuickPdfDocument::sourceChanged, + this, &QQuickPdfSelection::resetPoints); + } + m_document = document; + emit documentChanged(); + resetPoints(); + connect(m_document, &QQuickPdfDocument::sourceChanged, + this, &QQuickPdfSelection::resetPoints); +} + +/*! + \qmlproperty list> PdfSelection::geometry + + A set of paths in a form that can be bound to the \c paths property of a + \l {QtQuick::PathMultiline}{PathMultiline} instance to render a batch of + rectangles around the text regions that are included in the selection: + + \qml + PdfDocument { + id: doc + } + PdfSelection { + id: selection + document: doc + fromPoint: textSelectionDrag.centroid.pressPosition + toPoint: textSelectionDrag.centroid.position + hold: !textSelectionDrag.active + } + Shape { + ShapePath { + PathMultiline { + paths: selection.geometry + } + } + } + DragHandler { + id: textSelectionDrag + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + target: null + } + \endqml + + \sa PathMultiline +*/ +QVector QQuickPdfSelection::geometry() const +{ + return m_geometry; +} + +void QQuickPdfSelection::resetPoints() +{ + bool wasHolding = m_hold; + m_hold = false; + setFromPoint(QPointF()); + setToPoint(QPointF()); + m_hold = wasHolding; +} + +/*! + \qmlproperty int PdfSelection::page + + The page number on which to search. + + \sa QtQuick::Image::currentFrame +*/ +int QQuickPdfSelection::page() const +{ + return m_page; +} + +void QQuickPdfSelection::setPage(int page) +{ + if (m_page == page) + return; + + m_page = page; + emit pageChanged(); + resetPoints(); +} + +/*! + \qmlproperty point PdfSelection::fromPoint + + The beginning location, in \l {https://en.wikipedia.org/wiki/Point_(typography)}{points} + from the upper-left corner of the page, from which to find selected text. + This can be bound to a scaled version of the \c centroid.pressPosition + of a \l DragHandler to begin selecting text from the position where the user + presses the mouse button and begins dragging, for example. +*/ +QPointF QQuickPdfSelection::fromPoint() const +{ + return m_fromPoint; +} + +void QQuickPdfSelection::setFromPoint(QPointF fromPoint) +{ + if (m_hold || m_fromPoint == fromPoint) + return; + + m_fromPoint = fromPoint; + emit fromPointChanged(); + updateResults(); +} + +/*! + \qmlproperty point PdfSelection::toPoint + + The ending location, in \l {https://en.wikipedia.org/wiki/Point_(typography)}{points} + from the upper-left corner of the page, from which to find selected text. + This can be bound to a scaled version of the \c centroid.position + of a \l DragHandler to end selection of text at the position where the user + is currently dragging the mouse, for example. +*/ +QPointF QQuickPdfSelection::toPoint() const +{ + return m_toPoint; +} + +void QQuickPdfSelection::setToPoint(QPointF toPoint) +{ + if (m_hold || m_toPoint == toPoint) + return; + + m_toPoint = toPoint; + emit toPointChanged(); + updateResults(); +} + +/*! + \qmlproperty bool PdfSelection::hold + + Controls whether to hold the existing selection regardless of changes to + \l fromPoint and \l toPoint. This property can be set to \c true when the mouse + or touchpoint is released, so that the selection is not lost due to the + point bindings changing. +*/ +bool QQuickPdfSelection::hold() const +{ + return m_hold; +} + +void QQuickPdfSelection::setHold(bool hold) +{ + if (m_hold == hold) + return; + + m_hold = hold; + emit holdChanged(); +} + +/*! + \qmlproperty string PdfSelection::string + + The string found. +*/ +QString QQuickPdfSelection::text() const +{ + return m_text; +} + +#if QT_CONFIG(clipboard) +/*! + \qmlmethod void PdfSelection::copyToClipboard() + + Copies plain text from the \l string property to the system clipboard. +*/ +void QQuickPdfSelection::copyToClipboard() const +{ + QGuiApplication::clipboard()->setText(m_text); +} +#endif + +void QQuickPdfSelection::updateResults() +{ + if (!m_document) + return; + QPdfSelection sel = m_document->document().getSelection(m_page, m_fromPoint, m_toPoint); + if (sel.text() != m_text) { + m_text = sel.text(); + if (QGuiApplication::clipboard()->supportsSelection()) + sel.copyToClipboard(QClipboard::Selection); + emit textChanged(); + } + + if (sel.bounds() != m_geometry) { + m_geometry = sel.bounds(); + emit geometryChanged(); + } +} + +QT_END_NAMESPACE diff --git a/src/pdf/quick/qquickpdfselection_p.h b/src/pdf/quick/qquickpdfselection_p.h new file mode 100644 index 000000000..a0e6d1a8d --- /dev/null +++ b/src/pdf/quick/qquickpdfselection_p.h @@ -0,0 +1,122 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtPDF module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQUICKPDFSELECTION_P_H +#define QQUICKPDFSELECTION_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QQuickPdfDocument; + +class QQuickPdfSelection : public QObject +{ + Q_OBJECT + Q_PROPERTY(QQuickPdfDocument *document READ document WRITE setDocument NOTIFY documentChanged) + Q_PROPERTY(int page READ page WRITE setPage NOTIFY pageChanged) + Q_PROPERTY(QPointF fromPoint READ fromPoint WRITE setFromPoint NOTIFY fromPointChanged) + Q_PROPERTY(QPointF toPoint READ toPoint WRITE setToPoint NOTIFY toPointChanged) + Q_PROPERTY(bool hold READ hold WRITE setHold NOTIFY holdChanged) + + Q_PROPERTY(QString text READ text NOTIFY textChanged) + Q_PROPERTY(QVector geometry READ geometry NOTIFY geometryChanged) + +public: + explicit QQuickPdfSelection(QObject *parent = nullptr); + + QQuickPdfDocument *document() const; + void setDocument(QQuickPdfDocument * document); + int page() const; + void setPage(int page); + QPointF fromPoint() const; + void setFromPoint(QPointF fromPoint); + QPointF toPoint() const; + void setToPoint(QPointF toPoint); + bool hold() const; + void setHold(bool hold); + + QString text() const; + QVector geometry() const; + +#if QT_CONFIG(clipboard) + Q_INVOKABLE void copyToClipboard() const; +#endif + +signals: + void documentChanged(); + void pageChanged(); + void fromPointChanged(); + void toPointChanged(); + void holdChanged(); + void textChanged(); + void geometryChanged(); + +private: + void resetPoints(); + void updateResults(); + +private: + QQuickPdfDocument *m_document = nullptr; + QPointF m_fromPoint; + QPointF m_toPoint; + QString m_text; + QVector m_geometry; + int m_page = 0; + bool m_hold = false; + + Q_DISABLE_COPY(QQuickPdfSelection) +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickPdfSelection) +\ +#endif // QQUICKPDFSELECTION_P_H diff --git a/src/pdf/quick/quick.pro b/src/pdf/quick/quick.pro index cda768369..d999ffb0b 100644 --- a/src/pdf/quick/quick.pro +++ b/src/pdf/quick/quick.pro @@ -16,10 +16,12 @@ SOURCES += \ plugin.cpp \ qquickpdfdocument.cpp \ qquickpdfsearchmodel.cpp \ + qquickpdfselection.cpp \ HEADERS += \ qquickpdfdocument_p.h \ qquickpdfsearchmodel_p.h \ + qquickpdfselection_p.h \ QT += pdf quick-private gui gui-private core core-private qml qml-private -- cgit v1.2.3 From ccbd6fbdbe071f42e1c060ca579786758701f358 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Wed, 15 Jan 2020 09:44:10 +0100 Subject: Add PdfLinkModel Internal links and web links populate the QALM, which can then be used to drive a Repeater to position highlight rectangles with TapHandlers, which will handle a click by jumping to the link destination. Fixes: QTBUG-77511 Change-Id: I3b5b96d6e82bfd578f31f631f24279173036a080 Reviewed-by: Leena Miettinen Reviewed-by: Michal Klocek --- src/pdf/quick/plugin.cpp | 2 + src/pdf/quick/qml/PdfPageView.qml | 26 +++++++ src/pdf/quick/qquickpdfdocument_p.h | 1 + src/pdf/quick/qquickpdflinkmodel.cpp | 132 +++++++++++++++++++++++++++++++++++ src/pdf/quick/qquickpdflinkmodel_p.h | 87 +++++++++++++++++++++++ src/pdf/quick/quick.pro | 2 + 6 files changed, 250 insertions(+) create mode 100644 src/pdf/quick/qquickpdflinkmodel.cpp create mode 100644 src/pdf/quick/qquickpdflinkmodel_p.h (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/plugin.cpp b/src/pdf/quick/plugin.cpp index 72aa12d6a..519ea43af 100644 --- a/src/pdf/quick/plugin.cpp +++ b/src/pdf/quick/plugin.cpp @@ -39,6 +39,7 @@ #include #include #include "qquickpdfdocument_p.h" +#include "qquickpdflinkmodel_p.h" #include "qquickpdfsearchmodel_p.h" #include "qquickpdfselection_p.h" @@ -81,6 +82,7 @@ public: qmlRegisterModule(uri, 2, QT_VERSION_MINOR); qmlRegisterType(uri, 5, 15, "PdfDocument"); + qmlRegisterType(uri, 5, 15, "PdfLinkModel"); qmlRegisterType(uri, 5, 15, "PdfSearchModel"); qmlRegisterType(uri, 5, 15, "PdfSelection"); diff --git a/src/pdf/quick/qml/PdfPageView.qml b/src/pdf/quick/qml/PdfPageView.qml index 2f9c5ef99..556cf1b7a 100644 --- a/src/pdf/quick/qml/PdfPageView.qml +++ b/src/pdf/quick/qml/PdfPageView.qml @@ -118,6 +118,32 @@ Rectangle { } } } + + Repeater { + model: PdfLinkModel { + id: linkModel + document: paper.document + page: image.currentFrame + } + delegate: Rectangle { + color: "transparent" + border.color: "lightgrey" + x: rect.x * paper.__pageScale + y: rect.y * paper.__pageScale + width: rect.width * paper.__pageScale + height: rect.height * paper.__pageScale + HoverHandler { cursorShape: Qt.PointingHandCursor } // 5.15 onward (QTBUG-68073) + TapHandler { + onTapped: { + if (page >= 0) + image.currentFrame = page + else + Qt.openUrlExternally(url) + } + } + } + } + PinchHandler { id: pinch minimumScale: 0.1 diff --git a/src/pdf/quick/qquickpdfdocument_p.h b/src/pdf/quick/qquickpdfdocument_p.h index 1ec7edb1a..9817b5eef 100644 --- a/src/pdf/quick/qquickpdfdocument_p.h +++ b/src/pdf/quick/qquickpdfdocument_p.h @@ -118,6 +118,7 @@ private: QUrl m_source; QPdfDocument m_doc; + friend class QQuickPdfLinkModel; friend class QQuickPdfSearchModel; friend class QQuickPdfSelection; diff --git a/src/pdf/quick/qquickpdflinkmodel.cpp b/src/pdf/quick/qquickpdflinkmodel.cpp new file mode 100644 index 000000000..a3f552d17 --- /dev/null +++ b/src/pdf/quick/qquickpdflinkmodel.cpp @@ -0,0 +1,132 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtPDF module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qquickpdflinkmodel_p.h" +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +/*! + \qmltype PdfLinkModel + \instantiates QQuickPdfLinkModel + \inqmlmodule QtQuick.Pdf + \ingroup pdf + \brief A representation of links within a PDF document. + \since 5.15 + + PdfLinkModel provides the geometry and the destination for each link + that the specified \l page contains. + + The available model roles are: + + \value rect + Bounding rectangle around the link. + \value url + If the link is a web link, the URL for that; otherwise an empty URL. + \value page + If the link is an internal link, the page number to which the link should jump; otherwise \c {-1}. + \value location + If the link is an internal link, the location on the page to which the link should jump. + \value zoom + If the link is an internal link, the intended zoom level on the destination page. + + Normally it will be used with \l {QtQuick::Repeater}{Repeater} to visualize + the links and provide the ability to click them: + + \qml + Repeater { + model: PdfLinkModel { + document: root.document + page: image.currentFrame + } + delegate: Rectangle { + color: "transparent" + border.color: "lightgrey" + x: rect.x + y: rect.y + width: rect.width + height: rect.height + HoverHandler { cursorShape: Qt.PointingHandCursor } + TapHandler { + onTapped: { + if (page >= 0) + image.currentFrame = page + else + Qt.openUrlExternally(url) + } + } + } + } + \endqml + + \note General-purpose PDF viewing capabilities are provided by + \l PdfPageView and \l PdfMultiPageView. PdfLinkModel is only needed + when building PDF view components from scratch. +*/ + +QQuickPdfLinkModel::QQuickPdfLinkModel(QObject *parent) + : QPdfLinkModel(parent) +{ +} + +/*! + \qmlproperty PdfDocument PdfLinkModel::document + + This property holds the PDF document in which links are to be found. +*/ +QQuickPdfDocument *QQuickPdfLinkModel::document() const +{ + return m_quickDocument; +} + +void QQuickPdfLinkModel::setDocument(QQuickPdfDocument *document) +{ + if (document == m_quickDocument) + return; + m_quickDocument = document; + QPdfLinkModel::setDocument(&document->m_doc); +} + +/*! + \qmlproperty int PdfLinkModel::page + + This property holds the page number on which links are to be found. +*/ + +QT_END_NAMESPACE diff --git a/src/pdf/quick/qquickpdflinkmodel_p.h b/src/pdf/quick/qquickpdflinkmodel_p.h new file mode 100644 index 000000000..23ad6c8c1 --- /dev/null +++ b/src/pdf/quick/qquickpdflinkmodel_p.h @@ -0,0 +1,87 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtPDF module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQUICKPDFLINKMODEL_P_H +#define QQUICKPDFLINKMODEL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qquickpdfdocument_p.h" +#include "../api/qpdflinkmodel_p.h" + +#include +#include + +QT_BEGIN_NAMESPACE + +class QQuickPdfLinkModel : public QPdfLinkModel +{ + Q_OBJECT + Q_PROPERTY(QQuickPdfDocument *document READ document WRITE setDocument NOTIFY documentChanged) + +public: + explicit QQuickPdfLinkModel(QObject *parent = nullptr); + + QQuickPdfDocument *document() const; + void setDocument(QQuickPdfDocument *document); + +signals: + void documentChanged(); + +private: + void updateResults(); + +private: + QQuickPdfDocument *m_quickDocument; + QVector m_linksGeometry; + + Q_DISABLE_COPY(QQuickPdfLinkModel) +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickPdfLinkModel) + +#endif // QQUICKPDFLINKMODEL_P_H diff --git a/src/pdf/quick/quick.pro b/src/pdf/quick/quick.pro index d999ffb0b..7d65091aa 100644 --- a/src/pdf/quick/quick.pro +++ b/src/pdf/quick/quick.pro @@ -15,11 +15,13 @@ RESOURCES += resources.qrc SOURCES += \ plugin.cpp \ qquickpdfdocument.cpp \ + qquickpdflinkmodel.cpp \ qquickpdfsearchmodel.cpp \ qquickpdfselection.cpp \ HEADERS += \ qquickpdfdocument_p.h \ + qquickpdflinkmodel_p.h \ qquickpdfsearchmodel_p.h \ qquickpdfselection_p.h \ -- cgit v1.2.3 From a8e4ad7726f1aa52624a0367558650cd4d899c79 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Thu, 28 Nov 2019 11:51:07 +0100 Subject: PdfPageView: Add zoom-to-fit and zoom-to-width features Change-Id: I40b92000a4def105d22a3bd10d0544b0b0f0fe1e Reviewed-by: Shawn Rutledge --- src/pdf/quick/qml/PdfPageView.qml | 57 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/qml/PdfPageView.qml b/src/pdf/quick/qml/PdfPageView.qml index 556cf1b7a..e20ebd1b4 100644 --- a/src/pdf/quick/qml/PdfPageView.qml +++ b/src/pdf/quick/qml/PdfPageView.qml @@ -56,6 +56,51 @@ Rectangle { property real __pageScale: image.paintedWidth / document.pagePointSize(image.currentFrame).width + function resetScale() { + image.sourceSize.width = 0 + image.sourceSize.height = 0 + paper.x = 0 + paper.y = 0 + paper.scale = 1 + } + + function scaleToWidth(width, height) { + var halfRotation = Math.abs(paper.rotation % 180) + image.sourceSize = Qt.size((halfRotation > 45 && halfRotation < 135) ? height : width, 0) + paper.x = 0 + paper.y = 0 + image.centerInSize = Qt.size(width, height) + image.centerOnLoad = true + image.vCenterOnLoad = (halfRotation > 45 && halfRotation < 135) + paper.scale = 1 + } + + function scaleToPage(width, height) { + var windowAspect = width / height + var halfRotation = Math.abs(paper.rotation % 180) + var pagePointSize = document.pagePointSize(image.currentFrame) + if (halfRotation > 45 && halfRotation < 135) { + // rotated 90 or 270º + var pageAspect = pagePointSize.height / pagePointSize.width + if (windowAspect > pageAspect) { + image.sourceSize = Qt.size(height, 0) + } else { + image.sourceSize = Qt.size(0, width) + } + } else { + var pageAspect = pagePointSize.width / pagePointSize.height + if (windowAspect > pageAspect) { + image.sourceSize = Qt.size(0, height) + } else { + image.sourceSize = Qt.size(width, 0) + } + } + image.centerInSize = Qt.size(width, height) + image.centerOnLoad = true + image.vCenterOnLoad = true + paper.scale = 1 + } + PdfSelection { id: selection document: paper.document @@ -79,13 +124,23 @@ Rectangle { source: document.status === PdfDocument.Ready ? document.source : "" asynchronous: true fillMode: Image.PreserveAspectFit + property bool centerOnLoad: false + property bool vCenterOnLoad: false + property size centerInSize + onStatusChanged: + if (status == Image.Ready && centerOnLoad) { + paper.x = (centerInSize.width - image.implicitWidth) / 2 + paper.y = vCenterOnLoad ? (centerInSize.height - image.implicitHeight) / 2 : 0 + centerOnLoad = false + vCenterOnLoad = false + } } function reRenderIfNecessary() { var newSourceWidth = image.sourceSize.width * paper.scale var ratio = newSourceWidth / image.sourceSize.width if (ratio > 1.1 || ratio < 0.9) { image.sourceSize.width = newSourceWidth - image.sourceSize.height = 1 + image.sourceSize.height = 0 paper.scale = 1 } } -- cgit v1.2.3 From 7cf69cb52d434f5e74619b0577104d05688b0c22 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Wed, 22 Jan 2020 00:56:16 +0100 Subject: Add PdfNavigationStack for forward/back navigation Works well enough to use, but needs autotests and at least one fix. Change-Id: I2114b9fb3b5ddf7cfe2106d4a4fbc7d74852c61d Reviewed-by: Shawn Rutledge --- src/pdf/quick/plugin.cpp | 2 + src/pdf/quick/qml/PdfPageView.qml | 27 +++-- src/pdf/quick/qquickpdfnavigationstack.cpp | 162 +++++++++++++++++++++++++++++ src/pdf/quick/qquickpdfnavigationstack_p.h | 95 +++++++++++++++++ src/pdf/quick/quick.pro | 2 + 5 files changed, 280 insertions(+), 8 deletions(-) create mode 100644 src/pdf/quick/qquickpdfnavigationstack.cpp create mode 100644 src/pdf/quick/qquickpdfnavigationstack_p.h (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/plugin.cpp b/src/pdf/quick/plugin.cpp index 519ea43af..3c8077ff2 100644 --- a/src/pdf/quick/plugin.cpp +++ b/src/pdf/quick/plugin.cpp @@ -40,6 +40,7 @@ #include #include "qquickpdfdocument_p.h" #include "qquickpdflinkmodel_p.h" +#include "qquickpdfnavigationstack_p.h" #include "qquickpdfsearchmodel_p.h" #include "qquickpdfselection_p.h" @@ -83,6 +84,7 @@ public: qmlRegisterType(uri, 5, 15, "PdfDocument"); qmlRegisterType(uri, 5, 15, "PdfLinkModel"); + qmlRegisterType(uri, 5, 15, "PdfNavigationStack"); qmlRegisterType(uri, 5, 15, "PdfSearchModel"); qmlRegisterType(uri, 5, 15, "PdfSelection"); diff --git a/src/pdf/quick/qml/PdfPageView.qml b/src/pdf/quick/qml/PdfPageView.qml index e20ebd1b4..041054e59 100644 --- a/src/pdf/quick/qml/PdfPageView.qml +++ b/src/pdf/quick/qml/PdfPageView.qml @@ -48,13 +48,18 @@ Rectangle { property var document: null property real renderScale: 1 property alias sourceSize: image.sourceSize - property alias currentPage: image.currentFrame + property alias currentPage: navigationStack.currentPage property alias pageCount: image.frameCount property alias searchString: searchModel.searchString property alias selectedText: selection.text property alias status: image.status + property alias backEnabled: navigationStack.backAvailable + property alias forwardEnabled: navigationStack.forwardAvailable + function back() { navigationStack.back() } + function forward() { navigationStack.forward() } + signal currentPageReallyChanged(page: int) - property real __pageScale: image.paintedWidth / document.pagePointSize(image.currentFrame).width + property real __pageScale: image.paintedWidth / document.pagePointSize(navigationStack.currentPage).width function resetScale() { image.sourceSize.width = 0 @@ -78,7 +83,7 @@ Rectangle { function scaleToPage(width, height) { var windowAspect = width / height var halfRotation = Math.abs(paper.rotation % 180) - var pagePointSize = document.pagePointSize(image.currentFrame) + var pagePointSize = document.pagePointSize(navigationStack.currentPage) if (halfRotation > 45 && halfRotation < 135) { // rotated 90 or 270º var pageAspect = pagePointSize.height / pagePointSize.width @@ -104,7 +109,7 @@ Rectangle { PdfSelection { id: selection document: paper.document - page: image.currentFrame + page: navigationStack.currentPage fromPoint: Qt.point(textSelectionDrag.centroid.pressPosition.x / paper.__pageScale, textSelectionDrag.centroid.pressPosition.y / paper.__pageScale) toPoint: Qt.point(textSelectionDrag.centroid.position.x / paper.__pageScale, textSelectionDrag.centroid.position.y / paper.__pageScale) hold: !textSelectionDrag.active && !tapHandler.pressed @@ -116,11 +121,17 @@ Rectangle { PdfSearchModel { id: searchModel document: paper.document - page: image.currentFrame + page: navigationStack.currentPage + } + + PdfNavigationStack { + id: navigationStack + onCurrentPageChanged: paper.currentPageReallyChanged(navigationStack.currentPage) } Image { id: image + currentFrame: navigationStack.currentPage source: document.status === PdfDocument.Ready ? document.source : "" asynchronous: true fillMode: Image.PreserveAspectFit @@ -145,7 +156,7 @@ Rectangle { } } onRenderScaleChanged: { - image.sourceSize.width = document.pagePointSize(image.currentFrame).width * renderScale + image.sourceSize.width = document.pagePointSize(navigationStack.currentPage).width * renderScale image.sourceSize.height = 0 paper.scale = 1 } @@ -178,7 +189,7 @@ Rectangle { model: PdfLinkModel { id: linkModel document: paper.document - page: image.currentFrame + page: navigationStack.currentPage } delegate: Rectangle { color: "transparent" @@ -191,7 +202,7 @@ Rectangle { TapHandler { onTapped: { if (page >= 0) - image.currentFrame = page + navigationStack.currentPage = page else Qt.openUrlExternally(url) } diff --git a/src/pdf/quick/qquickpdfnavigationstack.cpp b/src/pdf/quick/qquickpdfnavigationstack.cpp new file mode 100644 index 000000000..c19fae735 --- /dev/null +++ b/src/pdf/quick/qquickpdfnavigationstack.cpp @@ -0,0 +1,162 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtPDF module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qquickpdfnavigationstack_p.h" +#include + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(qLcNav, "qt.pdf.navigationstack") + +/*! + \qmltype PdfNavigationStack + \instantiates QQuickPdfNavigationStack + \inqmlmodule QtQuick.Pdf + \ingroup pdf + \brief History of the pages visited within a PDF Document. + \since 5.15 + + PdfNavigationStack remembers which pages the user has visited in a PDF + document, and provides the ability to traverse backward and forward. +*/ + +QQuickPdfNavigationStack::QQuickPdfNavigationStack(QObject *parent) + : QObject(parent) +{ +} + +/*! + \qmlmethod void PdfNavigationStack::forward() + + Goes back to the page that was being viewed before back() was called, and + then emits the \l currentPageJumped() signal. + + If \l currentPage was set by assignment or binding since the last time + \l back() was called, the forward() function does nothing, because there is + a branch in the timeline which causes the "future" to be lost. +*/ +void QQuickPdfNavigationStack::forward() +{ + if (m_nextHistoryIndex >= m_pageHistory.count()) + return; + bool backAvailableWas = backAvailable(); + bool forwardAvailableWas = forwardAvailable(); + m_changing = true; + setCurrentPage(m_pageHistory.at(++m_nextHistoryIndex)); + m_changing = false; + emit currentPageJumped(m_currentPage); + if (backAvailableWas != backAvailable()) + emit backAvailableChanged(); + if (forwardAvailableWas != forwardAvailable()) + emit forwardAvailableChanged(); +} + +/*! + \qmlmethod void PdfNavigationStack::back() + + Pops the stack and causes the \l currentPage property to change to the + most-recently-viewed page, and then emits the \l currentPageJumped() + signal. +*/ +void QQuickPdfNavigationStack::back() +{ + if (m_nextHistoryIndex <= 0) + return; + bool backAvailableWas = backAvailable(); + bool forwardAvailableWas = forwardAvailable(); + m_changing = true; + // TODO don't do that when going back after going forward + m_pageHistory.append(m_currentPage); + setCurrentPage(m_pageHistory.at(--m_nextHistoryIndex)); + m_changing = false; + emit currentPageJumped(m_currentPage); + if (backAvailableWas != backAvailable()) + emit backAvailableChanged(); + if (forwardAvailableWas != forwardAvailable()) + emit forwardAvailableChanged(); +} + +/*! + \qmlproperty int PdfNavigationStack::currentPage + + This property holds the current page that is being viewed. + + It should be set when the viewer's current page changes. Every time this + property is set, it pushes the current page number onto the stack, such + that the history of pages that have been viewed will grow. +*/ +void QQuickPdfNavigationStack::setCurrentPage(int currentPage) +{ + if (m_currentPage == currentPage) + return; + bool backAvailableWas = backAvailable(); + bool forwardAvailableWas = forwardAvailable(); + if (!m_changing) { + if (m_nextHistoryIndex >= 0 && m_nextHistoryIndex < m_pageHistory.count()) + m_pageHistory.remove(m_nextHistoryIndex, m_pageHistory.count() - m_nextHistoryIndex); + m_pageHistory.append(m_currentPage); + m_nextHistoryIndex = m_pageHistory.count(); + } + m_currentPage = currentPage; + emit currentPageChanged(); + if (backAvailableWas != backAvailable()) + emit backAvailableChanged(); + if (forwardAvailableWas != forwardAvailable()) + emit forwardAvailableChanged(); + qCDebug(qLcNav) << "current" << m_currentPage << "history" << m_pageHistory; +} + +bool QQuickPdfNavigationStack::backAvailable() const +{ + return m_nextHistoryIndex > 0; +} + +bool QQuickPdfNavigationStack::forwardAvailable() const +{ + return m_nextHistoryIndex < m_pageHistory.count(); +} + +/*! + \qmlsignal PdfNavigationStack::currentPageJumped(int page) + + This signal is emitted when either forward() or back() is called, to + distinguish navigational jumps from cases when the \l currentPage property + is set by means of a binding or assignment. Contrast with the + \c currentPageChanged signal, which is emitted in all cases, and does not + include the \c page argument. +*/ + +QT_END_NAMESPACE diff --git a/src/pdf/quick/qquickpdfnavigationstack_p.h b/src/pdf/quick/qquickpdfnavigationstack_p.h new file mode 100644 index 000000000..54713fabb --- /dev/null +++ b/src/pdf/quick/qquickpdfnavigationstack_p.h @@ -0,0 +1,95 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtPDF module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQUICKPDFNAVIGATIONSTACK_P_H +#define QQUICKPDFNAVIGATIONSTACK_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qquickpdfdocument_p.h" + +#include + +QT_BEGIN_NAMESPACE + +class QQuickPdfNavigationStack : public QObject +{ + Q_OBJECT + Q_PROPERTY(int currentPage READ currentPage WRITE setCurrentPage NOTIFY currentPageChanged) + Q_PROPERTY(bool backAvailable READ backAvailable NOTIFY backAvailableChanged) + Q_PROPERTY(bool forwardAvailable READ forwardAvailable NOTIFY forwardAvailableChanged) + +public: + explicit QQuickPdfNavigationStack(QObject *parent = nullptr); + + Q_INVOKABLE void forward(); + Q_INVOKABLE void back(); + + int currentPage() const { return m_currentPage; } + void setCurrentPage(int currentPage); + + bool backAvailable() const; + bool forwardAvailable() const; + +Q_SIGNALS: + void currentPageChanged(); + void currentPageJumped(int page); + void backAvailableChanged(); + void forwardAvailableChanged(); + +private: + QVector m_pageHistory; + int m_nextHistoryIndex = 0; + int m_currentPage = 0; + bool m_changing = false; + + Q_DISABLE_COPY(QQuickPdfNavigationStack) +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickPdfNavigationStack) + +#endif // QQUICKPDFNAVIGATIONSTACK_P_H diff --git a/src/pdf/quick/quick.pro b/src/pdf/quick/quick.pro index 7d65091aa..a3bdfb45e 100644 --- a/src/pdf/quick/quick.pro +++ b/src/pdf/quick/quick.pro @@ -16,12 +16,14 @@ SOURCES += \ plugin.cpp \ qquickpdfdocument.cpp \ qquickpdflinkmodel.cpp \ + qquickpdfnavigationstack.cpp \ qquickpdfsearchmodel.cpp \ qquickpdfselection.cpp \ HEADERS += \ qquickpdfdocument_p.h \ qquickpdflinkmodel_p.h \ + qquickpdfnavigationstack_p.h \ qquickpdfsearchmodel_p.h \ qquickpdfselection_p.h \ -- cgit v1.2.3 From bf3133033236afb34974fec63ac21e1749d503ad Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Mon, 20 Jan 2020 15:12:01 +0100 Subject: Add PdfMultiPageView So far it's a ListView with a page per delegate. Many features are working, but zooming and rotation are not working yet. Change-Id: I9ee7aa60ad4411bd8734fe2cd987a68906a5cf57 Reviewed-by: Shawn Rutledge --- src/pdf/quick/plugin.cpp | 1 + src/pdf/quick/qml/PdfMultiPageView.qml | 180 +++++++++++++++++++++++++++++++++ src/pdf/quick/quick.pro | 1 + src/pdf/quick/resources.qrc | 1 + 4 files changed, 183 insertions(+) create mode 100644 src/pdf/quick/qml/PdfMultiPageView.qml (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/plugin.cpp b/src/pdf/quick/plugin.cpp index 3c8077ff2..a831a09b6 100644 --- a/src/pdf/quick/plugin.cpp +++ b/src/pdf/quick/plugin.cpp @@ -89,6 +89,7 @@ public: qmlRegisterType(uri, 5, 15, "PdfSelection"); qmlRegisterType(QUrl("qrc:/qt-project.org/qtpdf/qml/PdfPageView.qml"), uri, 5, 15, "PdfPageView"); + qmlRegisterType(QUrl("qrc:/qt-project.org/qtpdf/qml/PdfMultiPageView.qml"), uri, 5, 15, "PdfMultiPageView"); } }; diff --git a/src/pdf/quick/qml/PdfMultiPageView.qml b/src/pdf/quick/qml/PdfMultiPageView.qml new file mode 100644 index 000000000..36b194812 --- /dev/null +++ b/src/pdf/quick/qml/PdfMultiPageView.qml @@ -0,0 +1,180 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** BSD License Usage +** Alternatively, you may use this file under the terms of the BSD license +** as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Pdf 5.15 +import QtQuick.Shapes 1.15 +import QtQuick.Window 2.15 + +Item { + // public API + // TODO 5.15: required property + property var document: undefined + property real renderScale: 1 + property string searchString + property string selectedText + property alias currentPage: listView.currentIndex + function copySelectionToClipboard() { + if (listView.currentItem !== null) + listView.currentItem.selection.copyToClipboard() + } + property alias backEnabled: navigationStack.backAvailable + property alias forwardEnabled: navigationStack.forwardAvailable + function back() { navigationStack.back() } + function forward() { navigationStack.forward() } + signal currentPageReallyChanged(page: int) + + id: root + ListView { + id: listView + anchors.fill: parent + model: root.document === undefined ? 0 : root.document.pageCount + spacing: 6 + highlightRangeMode: ListView.ApplyRange + highlightMoveVelocity: 2000 // TODO increase velocity when setting currentIndex somehow, too + onCurrentIndexChanged: { + navigationStack.currentPage = currentIndex + root.currentPageReallyChanged(currentIndex) + } + delegate: Rectangle { + id: paper + width: image.width + height: image.height + property alias selection: selection + property real __pageScale: image.paintedWidth / document.pagePointSize(index).width + Image { + id: image + source: document.source + currentFrame: index + asynchronous: true + fillMode: Image.PreserveAspectFit + width: document.pagePointSize(currentFrame).width + height: document.pagePointSize(currentFrame).height + } + Shape { + anchors.fill: parent + opacity: 0.25 + visible: image.status === Image.Ready + ShapePath { + strokeWidth: 1 + strokeColor: "blue" + fillColor: "cyan" + scale: Qt.size(paper.__pageScale, paper.__pageScale) + PathMultiline { + id: searchResultBoundaries + paths: searchModel.matchGeometry + } + } + ShapePath { + fillColor: "orange" + scale: Qt.size(paper.__pageScale, paper.__pageScale) + PathMultiline { + id: selectionBoundaries + paths: selection.geometry + } + } + } + PdfSearchModel { + id: searchModel + document: root.document + page: image.currentFrame + searchString: root.searchString + } + PdfSelection { + id: selection + document: root.document + page: image.currentFrame + fromPoint: Qt.point(textSelectionDrag.centroid.pressPosition.x / paper.__pageScale, textSelectionDrag.centroid.pressPosition.y / paper.__pageScale) + toPoint: Qt.point(textSelectionDrag.centroid.position.x / paper.__pageScale, textSelectionDrag.centroid.position.y / paper.__pageScale) + hold: !textSelectionDrag.active && !tapHandler.pressed + onTextChanged: root.selectedText = text + } + DragHandler { + id: textSelectionDrag + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + target: null + } + TapHandler { + id: tapHandler + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + } + Repeater { + model: PdfLinkModel { + id: linkModel + document: root.document + page: image.currentFrame + } + delegate: Rectangle { + color: "transparent" + border.color: "lightgrey" + x: rect.x * paper.__pageScale + y: rect.y * paper.__pageScale + width: rect.width * paper.__pageScale + height: rect.height * paper.__pageScale + HoverHandler { cursorShape: Qt.PointingHandCursor } // 5.15 only (QTBUG-68073) + TapHandler { + onTapped: { + if (page >= 0) + listView.currentIndex = page + else + Qt.openUrlExternally(url) + } + } + } + } + } + } + PdfNavigationStack { + id: navigationStack + onCurrentPageJumped: listView.currentIndex = page + onCurrentPageChanged: root.currentPageReallyChanged(navigationStack.currentPage) + } +} diff --git a/src/pdf/quick/quick.pro b/src/pdf/quick/quick.pro index a3bdfb45e..a0a39d414 100644 --- a/src/pdf/quick/quick.pro +++ b/src/pdf/quick/quick.pro @@ -6,6 +6,7 @@ IMPORT_VERSION = 1.0 #QMAKE_DOCS = $$PWD/doc/qtquickpdf.qdocconf PDF_QML_FILES = \ + qml/PdfMultiPageView.qml \ qml/PdfPageView.qml \ QML_FILES += $$PDF_QML_FILES qmldir diff --git a/src/pdf/quick/resources.qrc b/src/pdf/quick/resources.qrc index a3f34189c..282610d4c 100644 --- a/src/pdf/quick/resources.qrc +++ b/src/pdf/quick/resources.qrc @@ -1,5 +1,6 @@ + qml/PdfMultiPageView.qml qml/PdfPageView.qml -- cgit v1.2.3 From 1f785521ab6982e7395af223e28137d65f8ead12 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Tue, 4 Feb 2020 16:42:27 +0100 Subject: Temporarily use MouseArea for links in Pdf(Multi)PageView HoverHandler.cursorShape is new API in 5.15, so we have to use MouseArea for 5.14. This patch can be reverted as soon as 5.14 is no longer supported. Change-Id: I1c830215729038095ec33ece36a1a83108cbd835 Reviewed-by: Shawn Rutledge --- src/pdf/quick/qml/PdfMultiPageView.qml | 17 +++++++++-------- src/pdf/quick/qml/PdfPageView.qml | 13 +++++++------ 2 files changed, 16 insertions(+), 14 deletions(-) (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/qml/PdfMultiPageView.qml b/src/pdf/quick/qml/PdfMultiPageView.qml index 36b194812..f64238eec 100644 --- a/src/pdf/quick/qml/PdfMultiPageView.qml +++ b/src/pdf/quick/qml/PdfMultiPageView.qml @@ -47,12 +47,12 @@ ** $QT_END_LICENSE$ ** ****************************************************************************/ -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 import QtQuick.Pdf 5.15 -import QtQuick.Shapes 1.15 -import QtQuick.Window 2.15 +import QtQuick.Shapes 1.14 +import QtQuick.Window 2.14 Item { // public API @@ -159,9 +159,10 @@ Item { y: rect.y * paper.__pageScale width: rect.width * paper.__pageScale height: rect.height * paper.__pageScale - HoverHandler { cursorShape: Qt.PointingHandCursor } // 5.15 only (QTBUG-68073) - TapHandler { - onTapped: { + MouseArea { // TODO switch to TapHandler / HoverHandler in 5.15 + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { if (page >= 0) listView.currentIndex = page else diff --git a/src/pdf/quick/qml/PdfPageView.qml b/src/pdf/quick/qml/PdfPageView.qml index 041054e59..cf287ecf7 100644 --- a/src/pdf/quick/qml/PdfPageView.qml +++ b/src/pdf/quick/qml/PdfPageView.qml @@ -33,10 +33,10 @@ ** $QT_END_LICENSE$ ** ****************************************************************************/ -import QtQuick 2.15 -import QtQuick.Controls 2.15 +import QtQuick 2.14 +import QtQuick.Controls 2.14 import QtQuick.Pdf 5.15 -import QtQuick.Shapes 1.15 +import QtQuick.Shapes 1.14 Rectangle { id: paper @@ -198,9 +198,10 @@ Rectangle { y: rect.y * paper.__pageScale width: rect.width * paper.__pageScale height: rect.height * paper.__pageScale - HoverHandler { cursorShape: Qt.PointingHandCursor } // 5.15 onward (QTBUG-68073) - TapHandler { - onTapped: { + MouseArea { // TODO switch to TapHandler / HoverHandler in 5.15 + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { if (page >= 0) navigationStack.currentPage = page else -- cgit v1.2.3 From 25a371caa376c513f22d5c01e425a18629657fdc Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Mon, 3 Feb 2020 18:11:16 +0100 Subject: Add zoom and rotation to PdfMultiPageView Currently, scaleToWidth() and scaleToPage() choose the scale of the first page to fit the given viewport size, and as long as all pages are the same size, it works. On the other hand, the PinchHandler only affects the scale of the page on which the pinch gesture occurs. Calling resetScale(), scaleToWidth() or scaleToPage() undoes the effect of any previous pinch gesture or any other kind of scaling change. Task-number: QTBUG-77513 Change-Id: Ia3227ca9c4af263eb8505dbd6336657984c66ab0 Reviewed-by: Shawn Rutledge --- src/pdf/quick/qml/PdfMultiPageView.qml | 95 +++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 13 deletions(-) (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/qml/PdfMultiPageView.qml b/src/pdf/quick/qml/PdfMultiPageView.qml index f64238eec..28436b90d 100644 --- a/src/pdf/quick/qml/PdfMultiPageView.qml +++ b/src/pdf/quick/qml/PdfMultiPageView.qml @@ -59,6 +59,7 @@ Item { // TODO 5.15: required property property var document: undefined property real renderScale: 1 + property real pageRotation: 0 property string searchString property string selectedText property alias currentPage: listView.currentIndex @@ -72,6 +73,32 @@ Item { function forward() { navigationStack.forward() } signal currentPageReallyChanged(page: int) + function resetScale() { + root.renderScale = 1 + } + + function scaleToWidth(width, height) { + root.renderScale = width / (listView.rot90 ? listView.firstPagePointSize.height : listView.firstPagePointSize.width) + } + + function scaleToPage(width, height) { + var windowAspect = width / height + var pageAspect = listView.firstPagePointSize.width / listView.firstPagePointSize.height + if (listView.rot90) { + if (windowAspect > pageAspect) { + root.renderScale = height / listView.firstPagePointSize.width + } else { + root.renderScale = width / listView.firstPagePointSize.height + } + } else { + if (windowAspect > pageAspect) { + root.renderScale = height / listView.firstPagePointSize.height + } else { + root.renderScale = width / listView.firstPagePointSize.width + } + } + } + id: root ListView { id: listView @@ -80,24 +107,38 @@ Item { spacing: 6 highlightRangeMode: ListView.ApplyRange highlightMoveVelocity: 2000 // TODO increase velocity when setting currentIndex somehow, too + property real rotationModulus: Math.abs(root.pageRotation % 180) + property bool rot90: rotationModulus > 45 && rotationModulus < 135 + property size firstPagePointSize: document.pagePointSize(0) onCurrentIndexChanged: { navigationStack.currentPage = currentIndex root.currentPageReallyChanged(currentIndex) } delegate: Rectangle { id: paper - width: image.width - height: image.height + implicitWidth: image.width + implicitHeight: image.height + rotation: root.pageRotation property alias selection: selection - property real __pageScale: image.paintedWidth / document.pagePointSize(index).width + property size pagePointSize: document.pagePointSize(index) + property real pageScale: image.paintedWidth / pagePointSize.width Image { id: image source: document.source currentFrame: index asynchronous: true fillMode: Image.PreserveAspectFit - width: document.pagePointSize(currentFrame).width - height: document.pagePointSize(currentFrame).height + width: pagePointSize.width * root.renderScale + height: pagePointSize.height * root.renderScale + property real renderScale: root.renderScale + property real oldRenderScale: 1 + onRenderScaleChanged: { + image.sourceSize.width = pagePointSize.width * renderScale + image.sourceSize.height = 0 + paper.scale = 1 + paper.x = 0 + paper.y = 0 + } } Shape { anchors.fill: parent @@ -107,7 +148,7 @@ Item { strokeWidth: 1 strokeColor: "blue" fillColor: "cyan" - scale: Qt.size(paper.__pageScale, paper.__pageScale) + scale: Qt.size(paper.pageScale, paper.pageScale) PathMultiline { id: searchResultBoundaries paths: searchModel.matchGeometry @@ -115,7 +156,7 @@ Item { } ShapePath { fillColor: "orange" - scale: Qt.size(paper.__pageScale, paper.__pageScale) + scale: Qt.size(paper.pageScale, paper.pageScale) PathMultiline { id: selectionBoundaries paths: selection.geometry @@ -132,11 +173,39 @@ Item { id: selection document: root.document page: image.currentFrame - fromPoint: Qt.point(textSelectionDrag.centroid.pressPosition.x / paper.__pageScale, textSelectionDrag.centroid.pressPosition.y / paper.__pageScale) - toPoint: Qt.point(textSelectionDrag.centroid.position.x / paper.__pageScale, textSelectionDrag.centroid.position.y / paper.__pageScale) + fromPoint: Qt.point(textSelectionDrag.centroid.pressPosition.x / paper.pageScale, textSelectionDrag.centroid.pressPosition.y / paper.pageScale) + toPoint: Qt.point(textSelectionDrag.centroid.position.x / paper.pageScale, textSelectionDrag.centroid.position.y / paper.pageScale) hold: !textSelectionDrag.active && !tapHandler.pressed onTextChanged: root.selectedText = text } + function reRenderIfNecessary() { + var newSourceWidth = image.sourceSize.width * paper.scale + var ratio = newSourceWidth / image.sourceSize.width + if (ratio > 1.1 || ratio < 0.9) { + image.sourceSize.height = 0 + image.sourceSize.width = newSourceWidth + paper.scale = 1 + } + } + PinchHandler { + id: pinch + minimumScale: 0.1 + maximumScale: 10 + minimumRotation: 0 + maximumRotation: 0 + onActiveChanged: + if (active) { + paper.z = 10 + } else { + paper.x = 0 + paper.y = 0 + paper.z = 0 + image.width = undefined + image.height = undefined + paper.reRenderIfNecessary() + } + grabPermissions: PointerHandler.CanTakeOverFromAnything + } DragHandler { id: textSelectionDrag acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus @@ -155,10 +224,10 @@ Item { delegate: Rectangle { color: "transparent" border.color: "lightgrey" - x: rect.x * paper.__pageScale - y: rect.y * paper.__pageScale - width: rect.width * paper.__pageScale - height: rect.height * paper.__pageScale + x: rect.x * paper.pageScale + y: rect.y * paper.pageScale + width: rect.width * paper.pageScale + height: rect.height * paper.pageScale MouseArea { // TODO switch to TapHandler / HoverHandler in 5.15 anchors.fill: parent cursorShape: Qt.PointingHandCursor -- cgit v1.2.3 From 09a6eac4a63b32548ecc1ff5b16a5d8fc3ba1c04 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Wed, 5 Feb 2020 15:53:57 +0100 Subject: Add QPdfDestination; NavigationStack stores page, location and zoom Push/back/forward behavior seems more correct now, but still no autotest yet. QPdfDestination might be useful to represent locations of search results, for link destinations and maybe named destinations too. Fixes: QTBUG-77512 Change-Id: I113b2c535a2cd302106e6546104c64e12985d387 Reviewed-by: Shawn Rutledge --- src/pdf/quick/qml/PdfMultiPageView.qml | 4 +- src/pdf/quick/qml/PdfPageView.qml | 3 +- src/pdf/quick/qquickpdfnavigationstack.cpp | 188 ++++++++++++++++++++++------- src/pdf/quick/qquickpdfnavigationstack_p.h | 21 ++-- 4 files changed, 164 insertions(+), 52 deletions(-) (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/qml/PdfMultiPageView.qml b/src/pdf/quick/qml/PdfMultiPageView.qml index 28436b90d..be41153b6 100644 --- a/src/pdf/quick/qml/PdfMultiPageView.qml +++ b/src/pdf/quick/qml/PdfMultiPageView.qml @@ -111,7 +111,7 @@ Item { property bool rot90: rotationModulus > 45 && rotationModulus < 135 property size firstPagePointSize: document.pagePointSize(0) onCurrentIndexChanged: { - navigationStack.currentPage = currentIndex + navigationStack.push(currentIndex, Qt.point(0, 0), root.renderScale) root.currentPageReallyChanged(currentIndex) } delegate: Rectangle { @@ -244,7 +244,7 @@ Item { } PdfNavigationStack { id: navigationStack - onCurrentPageJumped: listView.currentIndex = page + onJumped: listView.currentIndex = page onCurrentPageChanged: root.currentPageReallyChanged(navigationStack.currentPage) } } diff --git a/src/pdf/quick/qml/PdfPageView.qml b/src/pdf/quick/qml/PdfPageView.qml index cf287ecf7..d03e9dc9d 100644 --- a/src/pdf/quick/qml/PdfPageView.qml +++ b/src/pdf/quick/qml/PdfPageView.qml @@ -57,6 +57,7 @@ Rectangle { property alias forwardEnabled: navigationStack.forwardAvailable function back() { navigationStack.back() } function forward() { navigationStack.forward() } + function goToPage(page) { navigationStack.push(page, Qt.point(0, 0), renderScale) } signal currentPageReallyChanged(page: int) property real __pageScale: image.paintedWidth / document.pagePointSize(navigationStack.currentPage).width @@ -203,7 +204,7 @@ Rectangle { cursorShape: Qt.PointingHandCursor onClicked: { if (page >= 0) - navigationStack.currentPage = page + navigationStack.push(page, Qt.point(0, 0), paper.renderScale) else Qt.openUrlExternally(url) } diff --git a/src/pdf/quick/qquickpdfnavigationstack.cpp b/src/pdf/quick/qquickpdfnavigationstack.cpp index c19fae735..57acdc4bc 100644 --- a/src/pdf/quick/qquickpdfnavigationstack.cpp +++ b/src/pdf/quick/qquickpdfnavigationstack.cpp @@ -46,10 +46,10 @@ Q_LOGGING_CATEGORY(qLcNav, "qt.pdf.navigationstack") \instantiates QQuickPdfNavigationStack \inqmlmodule QtQuick.Pdf \ingroup pdf - \brief History of the pages visited within a PDF Document. + \brief History of the destinations visited within a PDF Document. \since 5.15 - PdfNavigationStack remembers which pages the user has visited in a PDF + PdfNavigationStack remembers which destinations the user has visited in a PDF document, and provides the ability to traverse backward and forward. */ @@ -61,102 +61,206 @@ QQuickPdfNavigationStack::QQuickPdfNavigationStack(QObject *parent) /*! \qmlmethod void PdfNavigationStack::forward() - Goes back to the page that was being viewed before back() was called, and - then emits the \l currentPageJumped() signal. + Goes back to the page, location and zoom level that was being viewed before + back() was called, and then emits the \l jumped() signal. - If \l currentPage was set by assignment or binding since the last time - \l back() was called, the forward() function does nothing, because there is - a branch in the timeline which causes the "future" to be lost. + If a new destination was pushed since the last time \l back() was called, + the forward() function does nothing, because there is a branch in the + timeline which causes the "future" to be lost. */ void QQuickPdfNavigationStack::forward() { - if (m_nextHistoryIndex >= m_pageHistory.count()) + if (m_currentHistoryIndex >= m_pageHistory.count() - 1) return; bool backAvailableWas = backAvailable(); bool forwardAvailableWas = forwardAvailable(); + QPointF currentLocationWas = currentLocation(); + qreal currentZoomWas = currentZoom(); + ++m_currentHistoryIndex; m_changing = true; - setCurrentPage(m_pageHistory.at(++m_nextHistoryIndex)); - m_changing = false; - emit currentPageJumped(m_currentPage); - if (backAvailableWas != backAvailable()) + emit jumped(currentPage(), currentLocation(), currentZoom()); + emit currentPageChanged(); + if (currentLocationWas != currentLocation()) + emit currentLocationChanged(); + if (currentZoomWas != currentZoom()) + emit currentZoomChanged(); + if (!backAvailableWas) emit backAvailableChanged(); if (forwardAvailableWas != forwardAvailable()) emit forwardAvailableChanged(); + m_changing = false; } /*! \qmlmethod void PdfNavigationStack::back() - Pops the stack and causes the \l currentPage property to change to the - most-recently-viewed page, and then emits the \l currentPageJumped() - signal. + Pops the stack, updates the \l currentPage, \l currentLocation and + \l currentZoom properties to the most-recently-viewed destination, and then + emits the \l jumped() signal. */ void QQuickPdfNavigationStack::back() { - if (m_nextHistoryIndex <= 0) + if (m_currentHistoryIndex <= 0) return; bool backAvailableWas = backAvailable(); bool forwardAvailableWas = forwardAvailable(); + QPointF currentLocationWas = currentLocation(); + qreal currentZoomWas = currentZoom(); + --m_currentHistoryIndex; m_changing = true; - // TODO don't do that when going back after going forward - m_pageHistory.append(m_currentPage); - setCurrentPage(m_pageHistory.at(--m_nextHistoryIndex)); - m_changing = false; - emit currentPageJumped(m_currentPage); + emit jumped(currentPage(), currentLocation(), currentZoom()); + emit currentPageChanged(); + if (currentLocationWas != currentLocation()) + emit currentLocationChanged(); + if (currentZoomWas != currentZoom()) + emit currentZoomChanged(); if (backAvailableWas != backAvailable()) emit backAvailableChanged(); - if (forwardAvailableWas != forwardAvailable()) + if (!forwardAvailableWas) emit forwardAvailableChanged(); + m_changing = false; } /*! \qmlproperty int PdfNavigationStack::currentPage This property holds the current page that is being viewed. + If there is no current page, it holds \c -1. +*/ +int QQuickPdfNavigationStack::currentPage() const +{ + if (m_currentHistoryIndex < 0 || m_currentHistoryIndex >= m_pageHistory.count()) + return -1; + return m_pageHistory.at(m_currentHistoryIndex)->page; +} + +/*! + \qmlproperty point PdfNavigationStack::currentLocation + + This property holds the current location on the page that is being viewed. +*/ +QPointF QQuickPdfNavigationStack::currentLocation() const +{ + if (m_currentHistoryIndex < 0 || m_currentHistoryIndex >= m_pageHistory.count()) + return QPointF(); + return m_pageHistory.at(m_currentHistoryIndex)->location; +} - It should be set when the viewer's current page changes. Every time this - property is set, it pushes the current page number onto the stack, such - that the history of pages that have been viewed will grow. +/*! + \qmlproperty real PdfNavigationStack::currentZoom + + This property holds the magnification scale on the page that is being viewed. */ -void QQuickPdfNavigationStack::setCurrentPage(int currentPage) +qreal QQuickPdfNavigationStack::currentZoom() const { - if (m_currentPage == currentPage) + if (m_currentHistoryIndex < 0 || m_currentHistoryIndex >= m_pageHistory.count()) + return 1; + return m_pageHistory.at(m_currentHistoryIndex)->zoom; +} + +/*! + \qmlmethod void PdfNavigationStack::push(int page, point location, qreal zoom) + + Adds the given destination, consisting of \a page, \a location and \a zoom, + to the history of visited locations. + + If forwardAvailable is \c true, calling this function represents a branch + in the timeline which causes the "future" to be lost, and therefore + forwardAvailable will change to \c false. +*/ +void QQuickPdfNavigationStack::push(int page, QPointF location, qreal zoom) +{ + if (page == currentPage() && location == currentLocation() && zoom == currentZoom()) return; + if (qFuzzyIsNull(zoom)) + zoom = currentZoom(); bool backAvailableWas = backAvailable(); bool forwardAvailableWas = forwardAvailable(); if (!m_changing) { - if (m_nextHistoryIndex >= 0 && m_nextHistoryIndex < m_pageHistory.count()) - m_pageHistory.remove(m_nextHistoryIndex, m_pageHistory.count() - m_nextHistoryIndex); - m_pageHistory.append(m_currentPage); - m_nextHistoryIndex = m_pageHistory.count(); + if (m_currentHistoryIndex >= 0 && forwardAvailableWas) + m_pageHistory.remove(m_currentHistoryIndex + 1, m_pageHistory.count() - m_currentHistoryIndex - 1); + m_pageHistory.append(QExplicitlySharedDataPointer(new QPdfDestinationPrivate(page, location, zoom))); + m_currentHistoryIndex = m_pageHistory.count() - 1; } - m_currentPage = currentPage; emit currentPageChanged(); - if (backAvailableWas != backAvailable()) + emit currentLocationChanged(); + emit currentZoomChanged(); + if (m_changing) + return; + if (!backAvailableWas) emit backAvailableChanged(); - if (forwardAvailableWas != forwardAvailable()) + if (forwardAvailableWas) emit forwardAvailableChanged(); - qCDebug(qLcNav) << "current" << m_currentPage << "history" << m_pageHistory; + qCDebug(qLcNav) << "push: index" << m_currentHistoryIndex << "page" << page + << "@" << location << "zoom" << zoom << "-> history" << + [this]() { + QStringList ret; + for (auto d : m_pageHistory) + ret << QString::number(d->page); + return ret.join(','); + }(); +} + +/*! + \qmlmethod void PdfNavigationStack::update(int page, point location, qreal zoom) + + Modifies the current destination, consisting of \a page, \a location and \a zoom. + + This can be called periodically while the user is manually moving around + the document, so that after back() is called, forward() will jump back to + the most-recently-viewed destination rather than the destination that was + last specified by push(). + + The \c currentPageChanged, \c currentLocationChanged and \c currentZoomChanged + signals will be emitted if the respective properties are actually changed. + The \l jumped signal is not emitted, because this operation + represents smooth movement rather than a navigational jump. +*/ +void QQuickPdfNavigationStack::update(int page, QPointF location, qreal zoom) +{ + if (m_currentHistoryIndex < 0 || m_currentHistoryIndex >= m_pageHistory.count()) + return; + int currentPageWas = currentPage(); + QPointF currentLocationWas = currentLocation(); + qreal currentZoomWas = currentZoom(); + if (page == currentPageWas && location == currentLocationWas && zoom == currentZoomWas) + return; + m_pageHistory[m_currentHistoryIndex]->page = page; + m_pageHistory[m_currentHistoryIndex]->location = location; + m_pageHistory[m_currentHistoryIndex]->zoom = zoom; + if (currentPageWas != page) + emit currentPageChanged(); + if (currentLocationWas != location) + emit currentLocationChanged(); + if (currentZoomWas != zoom) + emit currentZoomChanged(); + qCDebug(qLcNav) << "update: index" << m_currentHistoryIndex << "page" << page + << "@" << location << "zoom" << zoom << "-> history" << + [this]() { + QStringList ret; + for (auto d : m_pageHistory) + ret << QString::number(d->page); + return ret.join(','); + }(); } bool QQuickPdfNavigationStack::backAvailable() const { - return m_nextHistoryIndex > 0; + return m_currentHistoryIndex > 0; } bool QQuickPdfNavigationStack::forwardAvailable() const { - return m_nextHistoryIndex < m_pageHistory.count(); + return m_currentHistoryIndex < m_pageHistory.count() - 1; } /*! - \qmlsignal PdfNavigationStack::currentPageJumped(int page) + \qmlsignal PdfNavigationStack::jumped(int page, point location, qreal zoom) This signal is emitted when either forward() or back() is called, to - distinguish navigational jumps from cases when the \l currentPage property - is set by means of a binding or assignment. Contrast with the - \c currentPageChanged signal, which is emitted in all cases, and does not - include the \c page argument. + distinguish navigational jumps from cases when push() is called. + Contrast with the \c currentPageChanged signal, which is emitted in all + cases, and does not include the \c page, \c location and \c zoom arguments. */ QT_END_NAMESPACE diff --git a/src/pdf/quick/qquickpdfnavigationstack_p.h b/src/pdf/quick/qquickpdfnavigationstack_p.h index 54713fabb..8d7102fb1 100644 --- a/src/pdf/quick/qquickpdfnavigationstack_p.h +++ b/src/pdf/quick/qquickpdfnavigationstack_p.h @@ -49,6 +49,7 @@ // #include "qquickpdfdocument_p.h" +#include "../api/qpdfdestination_p.h" #include @@ -57,32 +58,38 @@ QT_BEGIN_NAMESPACE class QQuickPdfNavigationStack : public QObject { Q_OBJECT - Q_PROPERTY(int currentPage READ currentPage WRITE setCurrentPage NOTIFY currentPageChanged) + Q_PROPERTY(int currentPage READ currentPage NOTIFY currentPageChanged) + Q_PROPERTY(QPointF currentLocation READ currentLocation NOTIFY currentLocationChanged) + Q_PROPERTY(qreal currentZoom READ currentZoom NOTIFY currentZoomChanged) Q_PROPERTY(bool backAvailable READ backAvailable NOTIFY backAvailableChanged) Q_PROPERTY(bool forwardAvailable READ forwardAvailable NOTIFY forwardAvailableChanged) public: explicit QQuickPdfNavigationStack(QObject *parent = nullptr); + Q_INVOKABLE void push(int page, QPointF location, qreal zoom); + Q_INVOKABLE void update(int page, QPointF location, qreal zoom); Q_INVOKABLE void forward(); Q_INVOKABLE void back(); - int currentPage() const { return m_currentPage; } - void setCurrentPage(int currentPage); + int currentPage() const; + QPointF currentLocation() const; + qreal currentZoom() const; bool backAvailable() const; bool forwardAvailable() const; Q_SIGNALS: void currentPageChanged(); - void currentPageJumped(int page); + void currentLocationChanged(); + void currentZoomChanged(); void backAvailableChanged(); void forwardAvailableChanged(); + void jumped(int page, QPointF location, qreal zoom); private: - QVector m_pageHistory; - int m_nextHistoryIndex = 0; - int m_currentPage = 0; + QVector> m_pageHistory; + int m_currentHistoryIndex = 0; bool m_changing = false; Q_DISABLE_COPY(QQuickPdfNavigationStack) -- cgit v1.2.3 From 92cd38cd4a24e344492c2dd8a7a63e9dacf10553 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Wed, 5 Feb 2020 13:05:16 +0100 Subject: PDF multipage view: add vertical scrollbar So far it does not update the spinbox with the current page to which the user has scrolled, because ListView.currentIndex doesn't change. Change-Id: I5dfa644401f77628c71ad1db7d64c5f0ac1e0c65 Reviewed-by: Shawn Rutledge --- src/pdf/quick/qml/PdfMultiPageView.qml | 1 + 1 file changed, 1 insertion(+) (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/qml/PdfMultiPageView.qml b/src/pdf/quick/qml/PdfMultiPageView.qml index be41153b6..095081c5d 100644 --- a/src/pdf/quick/qml/PdfMultiPageView.qml +++ b/src/pdf/quick/qml/PdfMultiPageView.qml @@ -241,6 +241,7 @@ Item { } } } + ScrollBar.vertical: ScrollBar { } } PdfNavigationStack { id: navigationStack -- cgit v1.2.3 From 7afe95f14d7d048a73baa12b3bd5f6a9bcea2ccb Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Wed, 5 Feb 2020 10:56:48 +0100 Subject: PDF multipage view: track specific link and navigation destinations Unfortunately it's getting harder to do things declaratively, because we have to avoid circular bindings, and because of needing to use imperative APIs. The current-page spinbox provides onValueModified() to detect when the user modifies it, distinct from the simple fact that the value changed. We shouldn't make bindings to set ListView.currentIndex anyway, because that results in slow animation (and loading pages in all delegates along the way) rather than quick jumping to the correct page. Instead we need to use ListView.positionViewAtIndex(), another imperative API, to get quick jumps without having to calculate and set contentY in some other way. Now we move toward the NavigationStack providing storage for the current destination at all times. Changes there will trigger programmatically moving the ListView. When the user scrolls manually, that generates a "destination" in the navigation stack, such that the back button can jump back to the previous location, and then the forward button can return to the destination where manual scrolling ended up. Fixes: QTBUG-77510 Change-Id: I47544210d2e0f9aa790f3d2594839678374e463d Reviewed-by: Shawn Rutledge --- src/pdf/quick/qml/PdfMultiPageView.qml | 50 +++++++++++++++++++++++------- src/pdf/quick/qquickpdfnavigationstack.cpp | 1 + 2 files changed, 39 insertions(+), 12 deletions(-) (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/qml/PdfMultiPageView.qml b/src/pdf/quick/qml/PdfMultiPageView.qml index 095081c5d..acef9fbea 100644 --- a/src/pdf/quick/qml/PdfMultiPageView.qml +++ b/src/pdf/quick/qml/PdfMultiPageView.qml @@ -62,16 +62,19 @@ Item { property real pageRotation: 0 property string searchString property string selectedText - property alias currentPage: listView.currentIndex + property alias currentPage: navigationStack.currentPage function copySelectionToClipboard() { if (listView.currentItem !== null) listView.currentItem.selection.copyToClipboard() } property alias backEnabled: navigationStack.backAvailable property alias forwardEnabled: navigationStack.forwardAvailable - function back() { navigationStack.back() } - function forward() { navigationStack.forward() } - signal currentPageReallyChanged(page: int) + function back() { + navigationStack.back() + } + function forward() { + navigationStack.forward() + } function resetScale() { root.renderScale = 1 @@ -99,6 +102,16 @@ Item { } } + function goToPage(page) { + goToLocation(page, Qt.point(0, 0), 0) + } + + function goToLocation(page, location, zoom) { + if (zoom > 0) + root.renderScale = zoom + navigationStack.push(page, location, zoom) + } + id: root ListView { id: listView @@ -109,11 +122,7 @@ Item { highlightMoveVelocity: 2000 // TODO increase velocity when setting currentIndex somehow, too property real rotationModulus: Math.abs(root.pageRotation % 180) property bool rot90: rotationModulus > 45 && rotationModulus < 135 - property size firstPagePointSize: document.pagePointSize(0) - onCurrentIndexChanged: { - navigationStack.push(currentIndex, Qt.point(0, 0), root.renderScale) - root.currentPageReallyChanged(currentIndex) - } + property size firstPagePointSize: document === undefined ? Qt.size(0, 0) : document.pagePointSize(0) delegate: Rectangle { id: paper implicitWidth: image.width @@ -233,7 +242,7 @@ Item { cursorShape: Qt.PointingHandCursor onClicked: { if (page >= 0) - listView.currentIndex = page + root.goToLocation(page, location, zoom) else Qt.openUrlExternally(url) } @@ -241,11 +250,28 @@ Item { } } } - ScrollBar.vertical: ScrollBar { } + ScrollBar.vertical: ScrollBar { + property bool moved: false + onPositionChanged: moved = true + onActiveChanged: { + var currentPage = listView.indexAt(0, listView.contentY) + var currentItem = listView.itemAtIndex(currentPage) + var currentLocation = Qt.point(0, listView.contentY - currentItem.y) + if (active) { + moved = false + navigationStack.push(currentPage, currentLocation, root.renderScale); + } else if (moved) { + navigationStack.update(currentPage, currentLocation, root.renderScale); + } + } + } } PdfNavigationStack { id: navigationStack onJumped: listView.currentIndex = page - onCurrentPageChanged: root.currentPageReallyChanged(navigationStack.currentPage) + onCurrentPageChanged: listView.positionViewAtIndex(currentPage, ListView.Beginning) + onCurrentLocationChanged: listView.contentY += currentLocation.y // currentPageChanged() MUST occur first! + onCurrentZoomChanged: root.renderScale = currentZoom + // TODO deal with horizontal location (need another Flickable probably) } } diff --git a/src/pdf/quick/qquickpdfnavigationstack.cpp b/src/pdf/quick/qquickpdfnavigationstack.cpp index 57acdc4bc..51f65f032 100644 --- a/src/pdf/quick/qquickpdfnavigationstack.cpp +++ b/src/pdf/quick/qquickpdfnavigationstack.cpp @@ -56,6 +56,7 @@ Q_LOGGING_CATEGORY(qLcNav, "qt.pdf.navigationstack") QQuickPdfNavigationStack::QQuickPdfNavigationStack(QObject *parent) : QObject(parent) { + push(0, QPointF(), 1); } /*! -- cgit v1.2.3 From e5a33355798d3277c631b0024f389cdca2f2c683 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Fri, 7 Feb 2020 14:54:31 +0100 Subject: PDF multipage viewer: iterate search results This version still has separate PdfSearchModel instances on each page, but now there are buttons to iterate and highlight the search results in order. When you come to the last result on one page, hitting the "Find Next" button will jump to the next page, and keep jumping forward from there until another result is found. Unfortunately this jumping takes time if it skips over a lot of pages because of empty search results. That seems to be another reason to make PdfSearchModel into a whole-document search model and use one instance. Also reorganize PdfMultiPageView.qml's public API into sections according to functionality rather than by type. Change-Id: I677a764fcbf231b2656aff8abe7240a27582a696 Reviewed-by: Shawn Rutledge --- src/pdf/quick/qml/PdfMultiPageView.qml | 100 ++++++++++++++++++++++----------- 1 file changed, 68 insertions(+), 32 deletions(-) (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/qml/PdfMultiPageView.qml b/src/pdf/quick/qml/PdfMultiPageView.qml index acef9fbea..bc5134267 100644 --- a/src/pdf/quick/qml/PdfMultiPageView.qml +++ b/src/pdf/quick/qml/PdfMultiPageView.qml @@ -58,32 +58,33 @@ Item { // public API // TODO 5.15: required property property var document: undefined - property real renderScale: 1 - property real pageRotation: 0 - property string searchString + property string selectedText - property alias currentPage: navigationStack.currentPage function copySelectionToClipboard() { if (listView.currentItem !== null) listView.currentItem.selection.copyToClipboard() } + + // page navigation + property alias currentPage: navigationStack.currentPage property alias backEnabled: navigationStack.backAvailable property alias forwardEnabled: navigationStack.forwardAvailable - function back() { - navigationStack.back() - } - function forward() { - navigationStack.forward() - } - - function resetScale() { - root.renderScale = 1 + function back() { navigationStack.back() } + function forward() { navigationStack.forward() } + function goToPage(page) { goToLocation(page, Qt.point(0, 0), 0) } + function goToLocation(page, location, zoom) { + if (zoom > 0) + root.renderScale = zoom + navigationStack.push(page, location, zoom) } + // page scaling + property real renderScale: 1 + property real pageRotation: 0 + function resetScale() { root.renderScale = 1 } function scaleToWidth(width, height) { root.renderScale = width / (listView.rot90 ? listView.firstPagePointSize.height : listView.firstPagePointSize.width) } - function scaleToPage(width, height) { var windowAspect = width / height var pageAspect = listView.firstPagePointSize.width / listView.firstPagePointSize.height @@ -102,14 +103,39 @@ Item { } } - function goToPage(page) { - goToLocation(page, Qt.point(0, 0), 0) + // text search + property alias searchString: searchModel.searchString + property bool searchBackEnabled: searchModel.currentResult > 0 + property bool searchForwardEnabled: searchModel.currentResult < searchModel.matchGeometry.length - 1 + function searchBack() { + if (searchModel.currentResult > 0) { + --searchModel.currentResult + } else { + searchModel.deferRendering = true // save time while we are searching + while (searchModel.currentResult <= 0) { + if (navigationStack.currentPage > 0) + goToPage(navigationStack.currentPage - 1) + else + goToPage(document.pageCount - 1) + searchModel.currentResult = searchModel.matchGeometry.length - 1 + } + searchModel.deferRendering = false + } } - - function goToLocation(page, location, zoom) { - if (zoom > 0) - root.renderScale = zoom - navigationStack.push(page, location, zoom) + function searchForward() { + if (searchModel.currentResult < searchModel.matchGeometry.length - 1) { + ++searchModel.currentResult + } else { + searchModel.deferRendering = true // save time while we are searching + while (searchModel.currentResult >= searchModel.matchGeometry.length - 1) { + searchModel.currentResult = 0 + if (navigationStack.currentPage < document.pageCount - 1) + goToPage(navigationStack.currentPage + 1) + else + goToPage(0) + } + searchModel.deferRendering = false + } } id: root @@ -133,7 +159,7 @@ Item { property real pageScale: image.paintedWidth / pagePointSize.width Image { id: image - source: document.source + source: searchModel.deferRendering ? "" : document.source currentFrame: index asynchronous: true fillMode: Image.PreserveAspectFit @@ -152,17 +178,25 @@ Item { Shape { anchors.fill: parent opacity: 0.25 - visible: image.status === Image.Ready + visible: image.status === Image.Ready && searchModel.page == index ShapePath { strokeWidth: 1 - strokeColor: "blue" - fillColor: "cyan" + strokeColor: "steelblue" + fillColor: "lightsteelblue" scale: Qt.size(paper.pageScale, paper.pageScale) PathMultiline { - id: searchResultBoundaries paths: searchModel.matchGeometry } } + ShapePath { + strokeWidth: 1 + strokeColor: "blue" + fillColor: "cyan" + scale: Qt.size(paper.pageScale, paper.pageScale) + PathPolyline { + path: searchModel.matchGeometry[searchModel.currentResult] + } + } ShapePath { fillColor: "orange" scale: Qt.size(paper.pageScale, paper.pageScale) @@ -172,12 +206,6 @@ Item { } } } - PdfSearchModel { - id: searchModel - document: root.document - page: image.currentFrame - searchString: root.searchString - } PdfSelection { id: selection document: root.document @@ -274,4 +302,12 @@ Item { onCurrentZoomChanged: root.renderScale = currentZoom // TODO deal with horizontal location (need another Flickable probably) } + PdfSearchModel { + id: searchModel + document: root.document === undefined ? null : root.document + page: navigationStack.currentPage + searchString: root.searchString + property int currentResult: 0 + property bool deferRendering: false + } } -- cgit v1.2.3 From 0b6a4d94945a975390b2574e6aff2568ebb7f061 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Mon, 10 Feb 2020 10:49:33 +0100 Subject: PdfSearchModel: be QALM and find search results on all pages It's a QAbstractListModel, so now PdfMultiPageView has a ListView in a left-side Drawer showing all results found so far. - In PdfMultiPageView, multiple pages exist at once, so it makes sense to use the same searchmodel for all. - It's faster and saves memory. - Search results on each page can be cached. - It's possible to show search results in a ListView or QListView. Change-Id: I66fba6975954a09a4d23262be87ff8cc25ee7478 Reviewed-by: Shawn Rutledge --- src/pdf/quick/qml/PdfMultiPageView.qml | 76 +++++------- src/pdf/quick/qml/PdfPageView.qml | 39 +++++-- src/pdf/quick/qquickpdfsearchmodel.cpp | 204 +++++++++++++++++++++++++-------- src/pdf/quick/qquickpdfsearchmodel_p.h | 36 +++--- 4 files changed, 234 insertions(+), 121 deletions(-) (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/qml/PdfMultiPageView.qml b/src/pdf/quick/qml/PdfMultiPageView.qml index bc5134267..b64f44576 100644 --- a/src/pdf/quick/qml/PdfMultiPageView.qml +++ b/src/pdf/quick/qml/PdfMultiPageView.qml @@ -104,39 +104,10 @@ Item { } // text search + property alias searchModel: searchModel property alias searchString: searchModel.searchString - property bool searchBackEnabled: searchModel.currentResult > 0 - property bool searchForwardEnabled: searchModel.currentResult < searchModel.matchGeometry.length - 1 - function searchBack() { - if (searchModel.currentResult > 0) { - --searchModel.currentResult - } else { - searchModel.deferRendering = true // save time while we are searching - while (searchModel.currentResult <= 0) { - if (navigationStack.currentPage > 0) - goToPage(navigationStack.currentPage - 1) - else - goToPage(document.pageCount - 1) - searchModel.currentResult = searchModel.matchGeometry.length - 1 - } - searchModel.deferRendering = false - } - } - function searchForward() { - if (searchModel.currentResult < searchModel.matchGeometry.length - 1) { - ++searchModel.currentResult - } else { - searchModel.deferRendering = true // save time while we are searching - while (searchModel.currentResult >= searchModel.matchGeometry.length - 1) { - searchModel.currentResult = 0 - if (navigationStack.currentPage < document.pageCount - 1) - goToPage(navigationStack.currentPage + 1) - else - goToPage(0) - } - searchModel.deferRendering = false - } - } + function searchBack() { --searchModel.currentResult } + function searchForward() { ++searchModel.currentResult } id: root ListView { @@ -159,7 +130,7 @@ Item { property real pageScale: image.paintedWidth / pagePointSize.width Image { id: image - source: searchModel.deferRendering ? "" : document.source + source: document.source currentFrame: index asynchronous: true fillMode: Image.PreserveAspectFit @@ -178,31 +149,36 @@ Item { Shape { anchors.fill: parent opacity: 0.25 - visible: image.status === Image.Ready && searchModel.page == index + visible: image.status === Image.Ready ShapePath { strokeWidth: 1 - strokeColor: "steelblue" - fillColor: "lightsteelblue" + strokeColor: "cyan" + fillColor: "steelblue" scale: Qt.size(paper.pageScale, paper.pageScale) PathMultiline { - paths: searchModel.matchGeometry + paths: searchModel.boundingPolygonsOnPage(index) } } ShapePath { - strokeWidth: 1 - strokeColor: "blue" - fillColor: "cyan" + fillColor: "orange" scale: Qt.size(paper.pageScale, paper.pageScale) - PathPolyline { - path: searchModel.matchGeometry[searchModel.currentResult] + PathMultiline { + id: selectionBoundaries + paths: selection.geometry } } + } + Shape { + anchors.fill: parent + opacity: 0.5 + visible: image.status === Image.Ready && searchModel.currentPage === index ShapePath { - fillColor: "orange" + strokeWidth: 1 + strokeColor: "blue" + fillColor: "cyan" scale: Qt.size(paper.pageScale, paper.pageScale) PathMultiline { - id: selectionBoundaries - paths: selection.geometry + paths: searchModel.currentResultBoundingPolygons } } } @@ -297,7 +273,10 @@ Item { PdfNavigationStack { id: navigationStack onJumped: listView.currentIndex = page - onCurrentPageChanged: listView.positionViewAtIndex(currentPage, ListView.Beginning) + onCurrentPageChanged: { + listView.positionViewAtIndex(currentPage, ListView.Beginning) + searchModel.currentPage = currentPage + } onCurrentLocationChanged: listView.contentY += currentLocation.y // currentPageChanged() MUST occur first! onCurrentZoomChanged: root.renderScale = currentZoom // TODO deal with horizontal location (need another Flickable probably) @@ -305,9 +284,6 @@ Item { PdfSearchModel { id: searchModel document: root.document === undefined ? null : root.document - page: navigationStack.currentPage - searchString: root.searchString - property int currentResult: 0 - property bool deferRendering: false + onCurrentPageChanged: root.goToPage(currentPage) } } diff --git a/src/pdf/quick/qml/PdfPageView.qml b/src/pdf/quick/qml/PdfPageView.qml index d03e9dc9d..f4d7da0af 100644 --- a/src/pdf/quick/qml/PdfPageView.qml +++ b/src/pdf/quick/qml/PdfPageView.qml @@ -50,15 +50,18 @@ Rectangle { property alias sourceSize: image.sourceSize property alias currentPage: navigationStack.currentPage property alias pageCount: image.frameCount - property alias searchString: searchModel.searchString property alias selectedText: selection.text property alias status: image.status property alias backEnabled: navigationStack.backAvailable property alias forwardEnabled: navigationStack.forwardAvailable function back() { navigationStack.back() } function forward() { navigationStack.forward() } - function goToPage(page) { navigationStack.push(page, Qt.point(0, 0), renderScale) } - signal currentPageReallyChanged(page: int) + function goToPage(page) { goToLocation(page, Qt.point(0, 0), 0) } + function goToLocation(page, location, zoom) { + if (zoom > 0) + paper.renderScale = zoom + navigationStack.push(page, location, zoom) + } property real __pageScale: image.paintedWidth / document.pagePointSize(navigationStack.currentPage).width @@ -107,6 +110,12 @@ Rectangle { paper.scale = 1 } + // text search + property alias searchModel: searchModel + property alias searchString: searchModel.searchString + function searchBack() { --searchModel.currentResult } + function searchForward() { ++searchModel.currentResult } + PdfSelection { id: selection document: paper.document @@ -121,13 +130,16 @@ Rectangle { PdfSearchModel { id: searchModel - document: paper.document - page: navigationStack.currentPage + document: paper.document === undefined ? null : paper.document + currentPage: navigationStack.currentPage + onCurrentPageChanged: paper.goToPage(currentPage) } PdfNavigationStack { id: navigationStack - onCurrentPageChanged: paper.currentPageReallyChanged(navigationStack.currentPage) + // TODO onCurrentLocationChanged: position currentLocation.x and .y in middle // currentPageChanged() MUST occur first! + onCurrentZoomChanged: paper.renderScale = currentZoom + // TODO deal with horizontal location (need WheelHandler or Flickable probably) } Image { @@ -168,19 +180,26 @@ Rectangle { visible: image.status === Image.Ready ShapePath { strokeWidth: 1 - strokeColor: "blue" + strokeColor: "cyan" + fillColor: "steelblue" + scale: Qt.size(paper.__pageScale, paper.__pageScale) + PathMultiline { + paths: searchModel.currentPageBoundingPolygons + } + } + ShapePath { + strokeWidth: 1 + strokeColor: "orange" fillColor: "cyan" scale: Qt.size(paper.__pageScale, paper.__pageScale) PathMultiline { - id: searchResultBoundaries - paths: searchModel.matchGeometry + paths: searchModel.currentResultBoundingPolygons } } ShapePath { fillColor: "orange" scale: Qt.size(paper.__pageScale, paper.__pageScale) PathMultiline { - id: selectionBoundaries paths: selection.geometry } } diff --git a/src/pdf/quick/qquickpdfsearchmodel.cpp b/src/pdf/quick/qquickpdfsearchmodel.cpp index 8b0e88673..ec998ef0c 100644 --- a/src/pdf/quick/qquickpdfsearchmodel.cpp +++ b/src/pdf/quick/qquickpdfsearchmodel.cpp @@ -35,13 +35,12 @@ ****************************************************************************/ #include "qquickpdfsearchmodel_p.h" -#include -#include -#include -#include +#include QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(qLcS, "qt.pdf.search") + /*! \qmltype PdfSearchModel \instantiates QQuickPdfSearchModel @@ -57,6 +56,8 @@ QT_BEGIN_NAMESPACE QQuickPdfSearchModel::QQuickPdfSearchModel(QObject *parent) : QPdfSearchModel(parent) { + connect(this, &QPdfSearchModel::searchStringChanged, + this, &QQuickPdfSearchModel::onResultsChanged); } QQuickPdfDocument *QQuickPdfSearchModel::document() const @@ -68,16 +69,19 @@ void QQuickPdfSearchModel::setDocument(QQuickPdfDocument *document) { if (document == m_quickDocument) return; + m_quickDocument = document; QPdfSearchModel::setDocument(&document->m_doc); } /*! - \qmlproperty list> PdfSearchModel::matchGeometry + \qmlproperty list> PdfSearchModel::currentResultBoundingPolygons A set of paths in a form that can be bound to the \c paths property of a \l {QtQuick::PathMultiline}{PathMultiline} instance to render a batch of - rectangles around all the locations where search results are found: + rectangles around the regions comprising the search result \l currentResult + on \l currentPage. This is normally used to highlight one search result + at a time, in a UI that allows stepping through the results: \qml PdfDocument { @@ -86,12 +90,13 @@ void QQuickPdfSearchModel::setDocument(QQuickPdfDocument *document) PdfSearchModel { id: searchModel document: doc - page: doc.currentPage + currentPage: view.currentPage + currentResult: ... } Shape { ShapePath { PathMultiline { - paths: searchModel.matchGeometry + paths: searchModel.currentResultBoundingPolygons } } } @@ -99,67 +104,174 @@ void QQuickPdfSearchModel::setDocument(QQuickPdfDocument *document) \sa PathMultiline */ -QVector QQuickPdfSearchModel::matchGeometry() const +QVector QQuickPdfSearchModel::currentResultBoundingPolygons() const { - return m_matchGeometry; + QVector ret; + const auto &results = const_cast(this)->resultsOnPage(m_currentPage); + if (m_currentResult < 0 || m_currentResult >= results.count()) + return ret; + const auto result = results[m_currentResult]; + for (auto rect : result.rectangles()) + ret << QPolygonF(rect); + return ret; } -/*! - \qmlproperty string PdfSearchModel::searchString - - The string to search for. -*/ -QString QQuickPdfSearchModel::searchString() const +void QQuickPdfSearchModel::onResultsChanged() { - return m_searchString; + emit currentPageBoundingPolygonsChanged(); + emit currentResultBoundingPolygonsChanged(); } -void QQuickPdfSearchModel::setSearchString(QString searchString) -{ - if (m_searchString == searchString) - return; +/*! + \qmlproperty list> PdfSearchModel::currentPageBoundingPolygons + + A set of paths in a form that can be bound to the \c paths property of a + \l {QtQuick::PathMultiline}{PathMultiline} instance to render a batch of + rectangles around all the regions where search results are found on + \l currentPage: + + \qml + PdfDocument { + id: doc + } + PdfSearchModel { + id: searchModel + document: doc + } + Shape { + ShapePath { + PathMultiline { + paths: searchModel.matchGeometry(view.currentPage) + } + } + } + \endqml - m_searchString = searchString; - emit searchStringChanged(); - updateResults(); + \sa PathMultiline +*/ +QVector QQuickPdfSearchModel::currentPageBoundingPolygons() const +{ + return const_cast(this)->boundingPolygonsOnPage(m_currentPage); } /*! - \qmlproperty int PdfSearchModel::page + \qmlfunction list> PdfSearchModel::boundingPolygonsOnPage(int page) - The page number on which to search. + Returns a set of paths in a form that can be bound to the \c paths property of a + \l {QtQuick::PathMultiline}{PathMultiline} instance to render a batch of + rectangles around all the locations where search results are found: - \sa QtQuick::Image::currentFrame + \qml + PdfDocument { + id: doc + } + PdfSearchModel { + id: searchModel + document: doc + } + Shape { + ShapePath { + PathMultiline { + paths: searchModel.matchGeometry(view.currentPage) + } + } + } + \endqml + + \sa PathMultiline */ -int QQuickPdfSearchModel::page() const +QVector QQuickPdfSearchModel::boundingPolygonsOnPage(int page) { - return m_page; + if (!document() || searchString().isEmpty() || page < 0 || page > document()->pageCount()) + return {}; + + updatePage(page); + + QVector ret; + auto m = QPdfSearchModel::resultsOnPage(page); + for (auto result : m) { + for (auto rect : result.rectangles()) + ret << QPolygonF(rect); + } + + return ret; } -void QQuickPdfSearchModel::setPage(int page) +/*! + \qmlproperty int PdfSearchModel::currentPage + + The page on which \l currentMatchGeometry should provide filtered search results. +*/ +void QQuickPdfSearchModel::setCurrentPage(int currentPage) { - if (m_page == page) + if (m_currentPage == currentPage) return; - m_page = page; - emit pageChanged(); - updateResults(); + if (currentPage < 0) + currentPage = document()->pageCount() - 1; + else if (currentPage >= document()->pageCount()) + currentPage = 0; + + m_currentPage = currentPage; + if (!m_suspendSignals) { + emit currentPageChanged(); + onResultsChanged(); + } } -void QQuickPdfSearchModel::updateResults() +/*! + \qmlproperty int PdfSearchModel::currentResult + + The result index on \l currentPage for which \l currentResultBoundingPolygons + should provide the regions to highlight. +*/ +void QQuickPdfSearchModel::setCurrentResult(int currentResult) { - if (!document() || (m_searchString.isEmpty() && !m_matchGeometry.isEmpty()) || m_page < 0 || m_page > document()->pageCount()) { - m_matchGeometry.clear(); - emit matchGeometryChanged(); - } - QVector m = QPdfSearchModel::matches(m_page, m_searchString); - QVector matches; - for (QRectF r : m) - matches << QPolygonF(r); - if (matches != m_matchGeometry) { - m_matchGeometry = matches; - emit matchGeometryChanged(); + if (m_currentResult == currentResult) + return; + + int currentResultWas = currentResult; + int currentPageWas = m_currentPage; + if (currentResult < 0) { + setCurrentPage(m_currentPage - 1); + while (resultsOnPage(m_currentPage).count() == 0 && m_currentPage != currentPageWas) { + m_suspendSignals = true; + setCurrentPage(m_currentPage - 1); + } + if (m_suspendSignals) { + emit currentPageChanged(); + m_suspendSignals = false; + } + const auto results = resultsOnPage(m_currentPage); + currentResult = results.count() - 1; + } else { + const auto results = resultsOnPage(m_currentPage); + if (currentResult >= results.count()) { + setCurrentPage(m_currentPage + 1); + while (resultsOnPage(m_currentPage).count() == 0 && m_currentPage != currentPageWas) { + m_suspendSignals = true; + setCurrentPage(m_currentPage + 1); + } + if (m_suspendSignals) { + emit currentPageChanged(); + m_suspendSignals = false; + } + currentResult = 0; + } } + qCDebug(qLcS) << "currentResult was" << m_currentResult + << "requested" << currentResultWas << "on page" << currentPageWas + << "->" << currentResult << "on page" << m_currentPage; + + m_currentResult = currentResult; + emit currentResultChanged(); + emit currentResultBoundingPolygonsChanged(); } +/*! + \qmlproperty string PdfSearchModel::searchString + + The string to search for. +*/ + QT_END_NAMESPACE diff --git a/src/pdf/quick/qquickpdfsearchmodel_p.h b/src/pdf/quick/qquickpdfsearchmodel_p.h index 82a6289d0..3e05f80e3 100644 --- a/src/pdf/quick/qquickpdfsearchmodel_p.h +++ b/src/pdf/quick/qquickpdfsearchmodel_p.h @@ -51,7 +51,7 @@ #include "qquickpdfdocument_p.h" #include "../api/qpdfsearchmodel.h" -#include +#include #include QT_BEGIN_NAMESPACE @@ -60,9 +60,10 @@ class QQuickPdfSearchModel : public QPdfSearchModel { Q_OBJECT Q_PROPERTY(QQuickPdfDocument *document READ document WRITE setDocument NOTIFY documentChanged) - Q_PROPERTY(int page READ page WRITE setPage NOTIFY pageChanged) - Q_PROPERTY(QString searchString READ searchString WRITE setSearchString NOTIFY searchStringChanged) - Q_PROPERTY(QVector matchGeometry READ matchGeometry NOTIFY matchGeometryChanged) + Q_PROPERTY(int currentPage READ currentPage WRITE setCurrentPage NOTIFY currentPageChanged) + Q_PROPERTY(int currentResult READ currentResult WRITE setCurrentResult NOTIFY currentResultChanged) + Q_PROPERTY(QVector currentPageBoundingPolygons READ currentPageBoundingPolygons NOTIFY currentPageBoundingPolygonsChanged) + Q_PROPERTY(QVector currentResultBoundingPolygons READ currentResultBoundingPolygons NOTIFY currentResultBoundingPolygonsChanged) public: explicit QQuickPdfSearchModel(QObject *parent = nullptr); @@ -70,28 +71,33 @@ public: QQuickPdfDocument *document() const; void setDocument(QQuickPdfDocument * document); - int page() const; - void setPage(int page); + Q_INVOKABLE QVector boundingPolygonsOnPage(int page); - QString searchString() const; - void setSearchString(QString searchString); + int currentPage() const { return m_currentPage; } + void setCurrentPage(int currentPage); - QVector matchGeometry() const; + int currentResult() const { return m_currentResult; } + void setCurrentResult(int currentResult); + + QVector currentPageBoundingPolygons() const; + QVector currentResultBoundingPolygons() const; signals: void documentChanged(); - void pageChanged(); - void searchStringChanged(); - void matchGeometryChanged(); + void currentPageChanged(); + void currentResultChanged(); + void currentPageBoundingPolygonsChanged(); + void currentResultBoundingPolygonsChanged(); private: void updateResults(); + void onResultsChanged(); private: QQuickPdfDocument *m_quickDocument = nullptr; - QString m_searchString; - QVector m_matchGeometry; - int m_page; + int m_currentPage = 0; + int m_currentResult = 0; + bool m_suspendSignals = false; Q_DISABLE_COPY(QQuickPdfSearchModel) }; -- cgit v1.2.3 From 24cd9f79bf7cf21e275b73ded63ee46bcc706db3 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Mon, 17 Feb 2020 16:42:56 +0100 Subject: Rearrange PdfPageView.qml Now it looks more similar to PdfMultiPageView.qml: public properties and functions are grouped by functionality, implementation details are "below the fold", and properties and functions that we don't want to expose as API are nested inside inner items. Also fixed ColumnLayout attached properties. Change-Id: Iafe6f983a34ca6bac9b0d4f3a26aba5a426e0232 Reviewed-by: Shawn Rutledge --- src/pdf/quick/qml/PdfPageView.qml | 113 +++++++++++++++++++------------------- 1 file changed, 57 insertions(+), 56 deletions(-) (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/qml/PdfPageView.qml b/src/pdf/quick/qml/PdfPageView.qml index f4d7da0af..ec3cd78e0 100644 --- a/src/pdf/quick/qml/PdfPageView.qml +++ b/src/pdf/quick/qml/PdfPageView.qml @@ -39,19 +39,18 @@ import QtQuick.Pdf 5.15 import QtQuick.Shapes 1.14 Rectangle { - id: paper - width: image.width - height: image.height - // public API // TODO 5.15: required property - property var document: null - property real renderScale: 1 - property alias sourceSize: image.sourceSize - property alias currentPage: navigationStack.currentPage - property alias pageCount: image.frameCount - property alias selectedText: selection.text + property var document: undefined property alias status: image.status + + property alias selectedText: selection.text + function copySelectionToClipboard() { + selection.copyToClipboard() + } + + // page navigation + property alias currentPage: navigationStack.currentPage property alias backEnabled: navigationStack.backAvailable property alias forwardEnabled: navigationStack.forwardAvailable function back() { navigationStack.back() } @@ -59,34 +58,33 @@ Rectangle { function goToPage(page) { goToLocation(page, Qt.point(0, 0), 0) } function goToLocation(page, location, zoom) { if (zoom > 0) - paper.renderScale = zoom + root.renderScale = zoom navigationStack.push(page, location, zoom) } - property real __pageScale: image.paintedWidth / document.pagePointSize(navigationStack.currentPage).width - + // page scaling + property real renderScale: 1 + property alias sourceSize: image.sourceSize function resetScale() { image.sourceSize.width = 0 image.sourceSize.height = 0 - paper.x = 0 - paper.y = 0 - paper.scale = 1 + root.x = 0 + root.y = 0 + root.scale = 1 } - function scaleToWidth(width, height) { - var halfRotation = Math.abs(paper.rotation % 180) + var halfRotation = Math.abs(root.rotation % 180) image.sourceSize = Qt.size((halfRotation > 45 && halfRotation < 135) ? height : width, 0) - paper.x = 0 - paper.y = 0 + root.x = 0 + root.y = 0 image.centerInSize = Qt.size(width, height) image.centerOnLoad = true image.vCenterOnLoad = (halfRotation > 45 && halfRotation < 135) - paper.scale = 1 + root.scale = 1 } - function scaleToPage(width, height) { var windowAspect = width / height - var halfRotation = Math.abs(paper.rotation % 180) + var halfRotation = Math.abs(root.rotation % 180) var pagePointSize = document.pagePointSize(navigationStack.currentPage) if (halfRotation > 45 && halfRotation < 135) { // rotated 90 or 270º @@ -107,7 +105,7 @@ Rectangle { image.centerInSize = Qt.size(width, height) image.centerOnLoad = true image.vCenterOnLoad = true - paper.scale = 1 + root.scale = 1 } // text search @@ -116,29 +114,31 @@ Rectangle { function searchBack() { --searchModel.currentResult } function searchForward() { ++searchModel.currentResult } + // implementation + id: root + width: image.width + height: image.height + PdfSelection { id: selection - document: paper.document + document: root.document page: navigationStack.currentPage - fromPoint: Qt.point(textSelectionDrag.centroid.pressPosition.x / paper.__pageScale, textSelectionDrag.centroid.pressPosition.y / paper.__pageScale) - toPoint: Qt.point(textSelectionDrag.centroid.position.x / paper.__pageScale, textSelectionDrag.centroid.position.y / paper.__pageScale) + fromPoint: Qt.point(textSelectionDrag.centroid.pressPosition.x / image.pageScale, textSelectionDrag.centroid.pressPosition.y / image.pageScale) + toPoint: Qt.point(textSelectionDrag.centroid.position.x / image.pageScale, textSelectionDrag.centroid.position.y / image.pageScale) hold: !textSelectionDrag.active && !tapHandler.pressed } - function copySelectionToClipboard() { - selection.copyToClipboard() - } PdfSearchModel { id: searchModel - document: paper.document === undefined ? null : paper.document - currentPage: navigationStack.currentPage - onCurrentPageChanged: paper.goToPage(currentPage) + document: root.document === undefined ? null : root.document + onCurrentPageChanged: root.goToPage(currentPage) } PdfNavigationStack { id: navigationStack + onCurrentPageChanged: searchModel.currentPage = currentPage // TODO onCurrentLocationChanged: position currentLocation.x and .y in middle // currentPageChanged() MUST occur first! - onCurrentZoomChanged: paper.renderScale = currentZoom + onCurrentZoomChanged: root.renderScale = currentZoom // TODO deal with horizontal location (need WheelHandler or Flickable probably) } @@ -151,27 +151,28 @@ Rectangle { property bool centerOnLoad: false property bool vCenterOnLoad: false property size centerInSize + property real pageScale: image.paintedWidth / document.pagePointSize(navigationStack.currentPage).width + function reRenderIfNecessary() { + var newSourceWidth = image.sourceSize.width * root.scale + var ratio = newSourceWidth / image.sourceSize.width + if (ratio > 1.1 || ratio < 0.9) { + image.sourceSize.width = newSourceWidth + image.sourceSize.height = 0 + root.scale = 1 + } + } onStatusChanged: if (status == Image.Ready && centerOnLoad) { - paper.x = (centerInSize.width - image.implicitWidth) / 2 - paper.y = vCenterOnLoad ? (centerInSize.height - image.implicitHeight) / 2 : 0 + root.x = (centerInSize.width - image.implicitWidth) / 2 + root.y = vCenterOnLoad ? (centerInSize.height - image.implicitHeight) / 2 : 0 centerOnLoad = false vCenterOnLoad = false } } - function reRenderIfNecessary() { - var newSourceWidth = image.sourceSize.width * paper.scale - var ratio = newSourceWidth / image.sourceSize.width - if (ratio > 1.1 || ratio < 0.9) { - image.sourceSize.width = newSourceWidth - image.sourceSize.height = 0 - paper.scale = 1 - } - } onRenderScaleChanged: { image.sourceSize.width = document.pagePointSize(navigationStack.currentPage).width * renderScale image.sourceSize.height = 0 - paper.scale = 1 + root.scale = 1 } Shape { @@ -182,7 +183,7 @@ Rectangle { strokeWidth: 1 strokeColor: "cyan" fillColor: "steelblue" - scale: Qt.size(paper.__pageScale, paper.__pageScale) + scale: Qt.size(image.pageScale, image.pageScale) PathMultiline { paths: searchModel.currentPageBoundingPolygons } @@ -191,14 +192,14 @@ Rectangle { strokeWidth: 1 strokeColor: "orange" fillColor: "cyan" - scale: Qt.size(paper.__pageScale, paper.__pageScale) + scale: Qt.size(image.pageScale, image.pageScale) PathMultiline { paths: searchModel.currentResultBoundingPolygons } } ShapePath { fillColor: "orange" - scale: Qt.size(paper.__pageScale, paper.__pageScale) + scale: Qt.size(image.pageScale, image.pageScale) PathMultiline { paths: selection.geometry } @@ -208,22 +209,22 @@ Rectangle { Repeater { model: PdfLinkModel { id: linkModel - document: paper.document + document: root.document page: navigationStack.currentPage } delegate: Rectangle { color: "transparent" border.color: "lightgrey" - x: rect.x * paper.__pageScale - y: rect.y * paper.__pageScale - width: rect.width * paper.__pageScale - height: rect.height * paper.__pageScale + x: rect.x * image.pageScale + y: rect.y * image.pageScale + width: rect.width * image.pageScale + height: rect.height * image.pageScale MouseArea { // TODO switch to TapHandler / HoverHandler in 5.15 anchors.fill: parent cursorShape: Qt.PointingHandCursor onClicked: { if (page >= 0) - navigationStack.push(page, Qt.point(0, 0), paper.renderScale) + navigationStack.push(page, Qt.point(0, 0), root.renderScale) else Qt.openUrlExternally(url) } @@ -237,7 +238,7 @@ Rectangle { maximumScale: 10 minimumRotation: 0 maximumRotation: 0 - onActiveChanged: if (!active) paper.reRenderIfNecessary() + onActiveChanged: if (!active) image.reRenderIfNecessary() grabPermissions: PinchHandler.TakeOverForbidden // don't allow takeover if pinch has started } DragHandler { -- cgit v1.2.3 From d294872b34667199455ca169d68be392942c3b00 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Mon, 17 Feb 2020 22:28:38 +0100 Subject: Enable mouse wheel scrolling in single-page PdfPageView Change-Id: I20512187dcc872b2e0429968e9ad2a9899aee6c2 Reviewed-by: Shawn Rutledge --- src/pdf/quick/qml/PdfPageView.qml | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/qml/PdfPageView.qml b/src/pdf/quick/qml/PdfPageView.qml index ec3cd78e0..dfd00a1a8 100644 --- a/src/pdf/quick/qml/PdfPageView.qml +++ b/src/pdf/quick/qml/PdfPageView.qml @@ -37,6 +37,7 @@ import QtQuick 2.14 import QtQuick.Controls 2.14 import QtQuick.Pdf 5.15 import QtQuick.Shapes 1.14 +import Qt.labs.animation 1.0 Rectangle { // public API @@ -260,4 +261,13 @@ Rectangle { id: tapHandler acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus } + // prevent it from being scrolled out of view + BoundaryRule on x { + minimum: 100 - root.width + maximum: root.parent.width - 100 + } + BoundaryRule on y { + minimum: 100 - root.height + maximum: root.parent.height - 100 + } } -- cgit v1.2.3 From 34f52195b99a03dfdacc6b1eccb236d553b93ac0 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Wed, 19 Feb 2020 18:02:40 +0100 Subject: Guard against crash in QQuickPdfSearchModel::setDocument() If the document is null, ignore it. Change-Id: I3cebd049fb5d16d0064dddf00183f231019ef03c Reviewed-by: Shawn Rutledge --- src/pdf/quick/qquickpdfsearchmodel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/qquickpdfsearchmodel.cpp b/src/pdf/quick/qquickpdfsearchmodel.cpp index ec998ef0c..a4b457841 100644 --- a/src/pdf/quick/qquickpdfsearchmodel.cpp +++ b/src/pdf/quick/qquickpdfsearchmodel.cpp @@ -67,7 +67,7 @@ QQuickPdfDocument *QQuickPdfSearchModel::document() const void QQuickPdfSearchModel::setDocument(QQuickPdfDocument *document) { - if (document == m_quickDocument) + if (document == m_quickDocument || !document) return; m_quickDocument = document; -- cgit v1.2.3 From 57af89d1fcbc81e9d17a02be3f54ca239afe6697 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Wed, 19 Feb 2020 15:46:02 +0100 Subject: Fix PdfLinkModel's location y coordinate; add PdfMultiPageView tooltip As usual, coordinates are in the first quadrant, and we need to convert to 4th quadrant to get a y value that can be used to adjust contentY of a ListView or TableView. The tooltip when hovering over links provides a way to verify that the link really jumps where it's intended to. Change-Id: I9107639f15496a987c0fa7c3c2e2583c3839cc6b Reviewed-by: Shawn Rutledge --- src/pdf/quick/qml/PdfMultiPageView.qml | 10 ++++++++++ 1 file changed, 10 insertions(+) (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/qml/PdfMultiPageView.qml b/src/pdf/quick/qml/PdfMultiPageView.qml index b64f44576..b4bc61c64 100644 --- a/src/pdf/quick/qml/PdfMultiPageView.qml +++ b/src/pdf/quick/qml/PdfMultiPageView.qml @@ -242,8 +242,10 @@ Item { width: rect.width * paper.pageScale height: rect.height * paper.pageScale MouseArea { // TODO switch to TapHandler / HoverHandler in 5.15 + id: linkMA anchors.fill: parent cursorShape: Qt.PointingHandCursor + hoverEnabled: true onClicked: { if (page >= 0) root.goToLocation(page, location, zoom) @@ -251,6 +253,14 @@ Item { Qt.openUrlExternally(url) } } + ToolTip { + visible: linkMA.containsMouse + delay: 1000 + text: page >= 0 ? + ("page " + (page + 1) + + " location " + location.x.toFixed(1) + ", " + location.y.toFixed(1) + + " zoom " + zoom) : url + } } } } -- cgit v1.2.3 From 1acd9ad2bfa1c54f19fa8a71fb41e8a90233f76b Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Wed, 19 Feb 2020 17:47:01 +0100 Subject: QtPdf examples: use test.pdf from resources if no file given On iOS, the native FileDialog doesn't work, sharing doesn't work, and packaging files along with the application requires manual effort; so a PDF file in resources seems to be the easiest alternative to make the examples demo-able. QPdfDocument wants a file path, because it uses QFile; but we like to use URLs in Qt Quick. So it's a bit of an impedance mismatch, there are several choices about when and where to do the conversion, and it's hard to say which way is more correct. This way happens to work for now. Also do the rest of the things necessary to get this working on iOS. Change-Id: Icb8614d5eed2510f101aefba534ef80cf890518f Reviewed-by: Shawn Rutledge --- src/pdf/quick/qquickpdfdocument.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/qquickpdfdocument.cpp b/src/pdf/quick/qquickpdfdocument.cpp index 6eb9d3ae4..1cfd9a9af 100644 --- a/src/pdf/quick/qquickpdfdocument.cpp +++ b/src/pdf/quick/qquickpdfdocument.cpp @@ -91,7 +91,10 @@ void QQuickPdfDocument::setSource(QUrl source) m_source = source; emit sourceChanged(); - m_doc.load(source.path()); + if (source.scheme() == QLatin1String("qrc")) + m_doc.load(QLatin1Char(':') + source.path()); + else + m_doc.load(source.path()); } /*! -- cgit v1.2.3 From f467edc97e66727be7fa3747913e4e01672d4b71 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Tue, 18 Feb 2020 21:42:35 +0100 Subject: PdfMultiPageView: use TableView; horz. scroll; control page position TableView is missing some features compared to ListView; so finding out where we currently are (which row) and programmatic positioning on a specific y coordinate of a specific row require some workarounds for now, including helpers in PdfDocument. TableView also assumes (and sporadically enforces) that all cells in a column have the same width. So we need a placeholder Item for each page. This also helps with rotation: the placeholder is now as wide as the window or the image, whichever is wider, and the "paper" is centered within; thus there's always room to rotate it. There's still some problem with setting contentY in goToPage() after the page has been zoomed to a size larger than the window: the values look correct, but it scrolls too far. But on the plus side, horizontal scrolling works. So now we attempt to control the horizontal position too: NavigationStack tracks it, and can go back to a previous position; and links can in theory jump to specific positions and zoom levels, scrolling horizontally such that a specific x coordinate is visible. Includes minor UI tweaks to make it look better on iOS. Change-Id: I643d8ef48ef815aeb49cae77dcb84c3682563d56 Reviewed-by: Shawn Rutledge --- src/pdf/quick/qml/PdfMultiPageView.qml | 349 ++++++++++++++++------------- src/pdf/quick/qquickpdfdocument.cpp | 66 ++++++ src/pdf/quick/qquickpdfdocument_p.h | 7 + src/pdf/quick/qquickpdfnavigationstack.cpp | 23 +- 4 files changed, 280 insertions(+), 165 deletions(-) (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/qml/PdfMultiPageView.qml b/src/pdf/quick/qml/PdfMultiPageView.qml index b4bc61c64..e8eccaf3b 100644 --- a/src/pdf/quick/qml/PdfMultiPageView.qml +++ b/src/pdf/quick/qml/PdfMultiPageView.qml @@ -58,11 +58,15 @@ Item { // public API // TODO 5.15: required property property var document: undefined + property bool debug: false property string selectedText function copySelectionToClipboard() { - if (listView.currentItem !== null) - listView.currentItem.selection.copyToClipboard() + var currentItem = tableView.itemAtPos(0, tableView.contentY + root.height / 2) + if (debug) + console.log("currentItem", currentItem, "sel", currentItem.selection.text) + if (currentItem !== null) + currentItem.selection.copyToClipboard() } // page navigation @@ -71,7 +75,11 @@ Item { property alias forwardEnabled: navigationStack.forwardAvailable function back() { navigationStack.back() } function forward() { navigationStack.forward() } - function goToPage(page) { goToLocation(page, Qt.point(0, 0), 0) } + function goToPage(page) { + if (page === navigationStack.currentPage) + return + goToLocation(page, Qt.point(0, 0), 0) + } function goToLocation(page, location, zoom) { if (zoom > 0) root.renderScale = zoom @@ -83,22 +91,22 @@ Item { property real pageRotation: 0 function resetScale() { root.renderScale = 1 } function scaleToWidth(width, height) { - root.renderScale = width / (listView.rot90 ? listView.firstPagePointSize.height : listView.firstPagePointSize.width) + root.renderScale = width / (tableView.rot90 ? tableView.firstPagePointSize.height : tableView.firstPagePointSize.width) } function scaleToPage(width, height) { var windowAspect = width / height - var pageAspect = listView.firstPagePointSize.width / listView.firstPagePointSize.height - if (listView.rot90) { + var pageAspect = tableView.firstPagePointSize.width / tableView.firstPagePointSize.height + if (tableView.rot90) { if (windowAspect > pageAspect) { - root.renderScale = height / listView.firstPagePointSize.width + root.renderScale = height / tableView.firstPagePointSize.width } else { - root.renderScale = width / listView.firstPagePointSize.height + root.renderScale = width / tableView.firstPagePointSize.height } } else { if (windowAspect > pageAspect) { - root.renderScale = height / listView.firstPagePointSize.height + root.renderScale = height / tableView.firstPagePointSize.height } else { - root.renderScale = width / listView.firstPagePointSize.width + root.renderScale = width / tableView.firstPagePointSize.width } } } @@ -110,75 +118,170 @@ Item { function searchForward() { ++searchModel.currentResult } id: root - ListView { - id: listView + TableView { + id: tableView anchors.fill: parent model: root.document === undefined ? 0 : root.document.pageCount - spacing: 6 - highlightRangeMode: ListView.ApplyRange - highlightMoveVelocity: 2000 // TODO increase velocity when setting currentIndex somehow, too + rowSpacing: 6 property real rotationModulus: Math.abs(root.pageRotation % 180) property bool rot90: rotationModulus > 45 && rotationModulus < 135 + onRot90Changed: forceLayout() property size firstPagePointSize: document === undefined ? Qt.size(0, 0) : document.pagePointSize(0) + contentWidth: document === undefined ? 0 : document.maxPageWidth * root.renderScale + // workaround for missing function (see https://codereview.qt-project.org/c/qt/qtdeclarative/+/248464) + function itemAtPos(x, y, includeSpacing) { + // we don't care about x (assume col 0), and assume includeSpacing is true + var ret = null + for (var i = 0; i < contentItem.children.length; ++i) { + var child = contentItem.children[i]; + if (root.debug) + console.log(child, "@y", child.y) + if (child.y < y && (!ret || child.y > ret.y)) + ret = child + } + if (root.debug) + console.log("given y", y, "found", ret, "@", ret.y) + return ret // the delegate with the largest y that is less than the given y + } + rowHeightProvider: function(row) { return (rot90 ? document.pagePointSize(row).width : document.pagePointSize(row).height) * root.renderScale } delegate: Rectangle { - id: paper - implicitWidth: image.width - implicitHeight: image.height - rotation: root.pageRotation + id: pageHolder + color: root.debug ? "beige" : "transparent" + Text { + visible: root.debug + anchors { right: parent.right; verticalCenter: parent.verticalCenter } + rotation: -90; text: pageHolder.width.toFixed(1) + "x" + pageHolder.height.toFixed(1) + "\n" + + image.width.toFixed(1) + "x" + image.height.toFixed(1) + } + implicitWidth: Math.max(root.width, (tableView.rot90 ? document.maxPageHeight : document.maxPageWidth) * root.renderScale) + implicitHeight: tableView.rot90 ? image.width : image.height + onImplicitWidthChanged: tableView.forceLayout() + objectName: "page " + index + property int delegateIndex: row // expose the context property for JS outside of the delegate property alias selection: selection - property size pagePointSize: document.pagePointSize(index) - property real pageScale: image.paintedWidth / pagePointSize.width - Image { - id: image - source: document.source - currentFrame: index - asynchronous: true - fillMode: Image.PreserveAspectFit - width: pagePointSize.width * root.renderScale - height: pagePointSize.height * root.renderScale - property real renderScale: root.renderScale - property real oldRenderScale: 1 - onRenderScaleChanged: { - image.sourceSize.width = pagePointSize.width * renderScale - image.sourceSize.height = 0 - paper.scale = 1 - paper.x = 0 - paper.y = 0 + Rectangle { + id: paper + width: image.width + height: image.height + rotation: root.pageRotation + anchors.centerIn: parent + property size pagePointSize: document.pagePointSize(index) + property real pageScale: image.paintedWidth / pagePointSize.width + Image { + id: image + source: document.source + currentFrame: index + asynchronous: true + fillMode: Image.PreserveAspectFit + width: paper.pagePointSize.width * root.renderScale + height: paper.pagePointSize.height * root.renderScale + property real renderScale: root.renderScale + property real oldRenderScale: 1 + onRenderScaleChanged: { + image.sourceSize.width = paper.pagePointSize.width * renderScale + image.sourceSize.height = 0 + paper.scale = 1 + } } - } - Shape { - anchors.fill: parent - opacity: 0.25 - visible: image.status === Image.Ready - ShapePath { - strokeWidth: 1 - strokeColor: "cyan" - fillColor: "steelblue" - scale: Qt.size(paper.pageScale, paper.pageScale) - PathMultiline { - paths: searchModel.boundingPolygonsOnPage(index) + Shape { + anchors.fill: parent + opacity: 0.25 + visible: image.status === Image.Ready + ShapePath { + strokeWidth: 1 + strokeColor: "cyan" + fillColor: "steelblue" + scale: Qt.size(paper.pageScale, paper.pageScale) + PathMultiline { + paths: searchModel.boundingPolygonsOnPage(index) + } + } + ShapePath { + fillColor: "orange" + scale: Qt.size(paper.pageScale, paper.pageScale) + PathMultiline { + id: selectionBoundaries + paths: selection.geometry + } } } - ShapePath { - fillColor: "orange" - scale: Qt.size(paper.pageScale, paper.pageScale) - PathMultiline { - id: selectionBoundaries - paths: selection.geometry + Shape { + anchors.fill: parent + opacity: 0.5 + visible: image.status === Image.Ready && searchModel.currentPage === index + ShapePath { + strokeWidth: 1 + strokeColor: "blue" + fillColor: "cyan" + scale: Qt.size(paper.pageScale, paper.pageScale) + PathMultiline { + paths: searchModel.currentResultBoundingPolygons + } } } - } - Shape { - anchors.fill: parent - opacity: 0.5 - visible: image.status === Image.Ready && searchModel.currentPage === index - ShapePath { - strokeWidth: 1 - strokeColor: "blue" - fillColor: "cyan" - scale: Qt.size(paper.pageScale, paper.pageScale) - PathMultiline { - paths: searchModel.currentResultBoundingPolygons + PinchHandler { + id: pinch + minimumScale: 0.1 + maximumScale: root.renderScale < 4 ? 2 : 1 + minimumRotation: 0 + maximumRotation: 0 + enabled: image.sourceSize.width < 5000 + onActiveChanged: + if (active) { + paper.z = 10 + } else { + paper.z = 0 + var newSourceWidth = image.sourceSize.width * paper.scale + var ratio = newSourceWidth / image.sourceSize.width + if (ratio > 1.1 || ratio < 0.9) { + paper.scale = 1 + root.renderScale *= ratio + } + } + grabPermissions: PointerHandler.CanTakeOverFromAnything + } + DragHandler { + id: textSelectionDrag + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + target: null + } + TapHandler { + id: tapHandler + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + } + Repeater { + model: PdfLinkModel { + id: linkModel + document: root.document + page: image.currentFrame + } + delegate: Rectangle { + color: "transparent" + border.color: "lightgrey" + x: rect.x * paper.pageScale + y: rect.y * paper.pageScale + width: rect.width * paper.pageScale + height: rect.height * paper.pageScale + MouseArea { // TODO switch to TapHandler / HoverHandler in 5.15 + id: linkMA + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onClicked: { + if (page >= 0) + root.goToLocation(page, location, zoom) + else + Qt.openUrlExternally(url) + } + } + ToolTip { + visible: linkMA.containsMouse + delay: 1000 + text: page >= 0 ? + ("page " + (page + 1) + + " location " + location.x.toFixed(1) + ", " + location.y.toFixed(1) + + " zoom " + zoom) : url + } } } } @@ -186,110 +289,50 @@ Item { id: selection document: root.document page: image.currentFrame - fromPoint: Qt.point(textSelectionDrag.centroid.pressPosition.x / paper.pageScale, textSelectionDrag.centroid.pressPosition.y / paper.pageScale) - toPoint: Qt.point(textSelectionDrag.centroid.position.x / paper.pageScale, textSelectionDrag.centroid.position.y / paper.pageScale) + fromPoint: Qt.point(textSelectionDrag.centroid.pressPosition.x / paper.pageScale, + textSelectionDrag.centroid.pressPosition.y / paper.pageScale) + toPoint: Qt.point(textSelectionDrag.centroid.position.x / paper.pageScale, + textSelectionDrag.centroid.position.y / paper.pageScale) hold: !textSelectionDrag.active && !tapHandler.pressed onTextChanged: root.selectedText = text } - function reRenderIfNecessary() { - var newSourceWidth = image.sourceSize.width * paper.scale - var ratio = newSourceWidth / image.sourceSize.width - if (ratio > 1.1 || ratio < 0.9) { - image.sourceSize.height = 0 - image.sourceSize.width = newSourceWidth - paper.scale = 1 - } - } - PinchHandler { - id: pinch - minimumScale: 0.1 - maximumScale: 10 - minimumRotation: 0 - maximumRotation: 0 - onActiveChanged: - if (active) { - paper.z = 10 - } else { - paper.x = 0 - paper.y = 0 - paper.z = 0 - image.width = undefined - image.height = undefined - paper.reRenderIfNecessary() - } - grabPermissions: PointerHandler.CanTakeOverFromAnything - } - DragHandler { - id: textSelectionDrag - acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus - target: null - } - TapHandler { - id: tapHandler - acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus - } - Repeater { - model: PdfLinkModel { - id: linkModel - document: root.document - page: image.currentFrame - } - delegate: Rectangle { - color: "transparent" - border.color: "lightgrey" - x: rect.x * paper.pageScale - y: rect.y * paper.pageScale - width: rect.width * paper.pageScale - height: rect.height * paper.pageScale - MouseArea { // TODO switch to TapHandler / HoverHandler in 5.15 - id: linkMA - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - hoverEnabled: true - onClicked: { - if (page >= 0) - root.goToLocation(page, location, zoom) - else - Qt.openUrlExternally(url) - } - } - ToolTip { - visible: linkMA.containsMouse - delay: 1000 - text: page >= 0 ? - ("page " + (page + 1) + - " location " + location.x.toFixed(1) + ", " + location.y.toFixed(1) + - " zoom " + zoom) : url - } - } - } } ScrollBar.vertical: ScrollBar { property bool moved: false onPositionChanged: moved = true onActiveChanged: { - var currentPage = listView.indexAt(0, listView.contentY) - var currentItem = listView.itemAtIndex(currentPage) - var currentLocation = Qt.point(0, listView.contentY - currentItem.y) + var currentItem = tableView.itemAtPos(0, tableView.contentY + root.height / 2) + var currentPage = currentItem.delegateIndex + var currentLocation = Qt.point((tableView.contentX - currentItem.x + root.width / 2) / root.renderScale, + (tableView.contentY - currentItem.y + root.height / 2) / root.renderScale) if (active) { moved = false - navigationStack.push(currentPage, currentLocation, root.renderScale); + navigationStack.push(currentPage, currentLocation, root.renderScale) } else if (moved) { - navigationStack.update(currentPage, currentLocation, root.renderScale); + navigationStack.update(currentPage, currentLocation, root.renderScale) } } } + ScrollBar.horizontal: ScrollBar { } + } + onRenderScaleChanged: { + tableView.forceLayout() + var currentItem = tableView.itemAtPos(tableView.contentX + root.width / 2, tableView.contentY + root.height / 2) + if (currentItem !== undefined) + navigationStack.update(currentItem.delegateIndex, Qt.point(currentItem.x / renderScale, currentItem.y / renderScale), renderScale) } PdfNavigationStack { id: navigationStack - onJumped: listView.currentIndex = page - onCurrentPageChanged: { - listView.positionViewAtIndex(currentPage, ListView.Beginning) - searchModel.currentPage = currentPage + onJumped: { + root.renderScale = zoom + tableView.contentX = Math.max(0, location.x - root.width / 2) * root.renderScale + tableView.contentY = tableView.originY + root.document.heightSumBeforePage(page, tableView.rowSpacing / root.renderScale) * root.renderScale + if (root.debug) { + console.log("going to page", page, + "@y", root.document.heightSumBeforePage(page, tableView.rowSpacing / root.renderScale) * root.renderScale, + "ended up @", tableView.contentY, "originY is", tableView.originY) + } } - onCurrentLocationChanged: listView.contentY += currentLocation.y // currentPageChanged() MUST occur first! - onCurrentZoomChanged: root.renderScale = currentZoom - // TODO deal with horizontal location (need another Flickable probably) } PdfSearchModel { id: searchModel diff --git a/src/pdf/quick/qquickpdfdocument.cpp b/src/pdf/quick/qquickpdfdocument.cpp index 1cfd9a9af..3d5f0fa10 100644 --- a/src/pdf/quick/qquickpdfdocument.cpp +++ b/src/pdf/quick/qquickpdfdocument.cpp @@ -90,6 +90,7 @@ void QQuickPdfDocument::setSource(QUrl source) return; m_source = source; + m_maxPageWidthHeight = QSizeF(); emit sourceChanged(); if (source.scheme() == QLatin1String("qrc")) m_doc.load(QLatin1Char(':') + source.path()); @@ -172,6 +173,71 @@ QSizeF QQuickPdfDocument::pagePointSize(int page) const return m_doc.pageSize(page); } +qreal QQuickPdfDocument::maxPageWidth() const +{ + const_cast(this)->updateMaxPageSize(); + return m_maxPageWidthHeight.width(); +} + +qreal QQuickPdfDocument::maxPageHeight() const +{ + const_cast(this)->updateMaxPageSize(); + return m_maxPageWidthHeight.height(); +} + +/*! + \internal + \qmlmethod size PdfDocument::heightSumBeforePage(int page) + + Returns the sum of the heights, in points, of all sets of \a facingPages + pages from 0 to the given \a page, exclusive. + + That is, if the pages were laid out end-to-end in adjacent sets of + \a facingPages, what would be the distance in points from the top of the + first page to the top of the given page. +*/ +// Workaround for lack of something analogous to ListView.positionViewAtIndex() in TableView +qreal QQuickPdfDocument::heightSumBeforePage(int page, qreal spacing, int facingPages) const +{ + qreal ret = 0; + for (int i = 0; i < page; i+= facingPages) { + if (i + facingPages > page) + break; + qreal facingPagesHeight = 0; + for (int j = i; j < i + facingPages; ++j) + facingPagesHeight = qMax(facingPagesHeight, pagePointSize(j).height()); + ret += facingPagesHeight + spacing; + } + return ret; +} + +void QQuickPdfDocument::updateMaxPageSize() +{ + if (m_maxPageWidthHeight.isValid()) + return; + qreal w = 0; + qreal h = 0; + const int count = pageCount(); + for (int i = 0; i < count; ++i) { + auto size = pagePointSize(i); + w = qMax(w, size.width()); + h = qMax(w, size.height()); + } + m_maxPageWidthHeight = QSizeF(w, h); +} + +/*! + \qmlproperty real PdfDocument::maxPageWidth + + This property holds the width of the widest page in the document, in points. +*/ + +/*! + \qmlproperty real PdfDocument::maxPageHeight + + This property holds the height of the tallest page in the document, in points. +*/ + /*! \qmlproperty string PdfDocument::title diff --git a/src/pdf/quick/qquickpdfdocument_p.h b/src/pdf/quick/qquickpdfdocument_p.h index 9817b5eef..cefa4f756 100644 --- a/src/pdf/quick/qquickpdfdocument_p.h +++ b/src/pdf/quick/qquickpdfdocument_p.h @@ -62,6 +62,8 @@ class QQuickPdfDocument : public QObject, public QQmlParserStatus Q_OBJECT Q_PROPERTY(QUrl source READ source WRITE setSource NOTIFY sourceChanged) Q_PROPERTY(int pageCount READ pageCount NOTIFY pageCountChanged FINAL) + Q_PROPERTY(qreal maxPageWidth READ maxPageWidth NOTIFY metaDataChanged) + Q_PROPERTY(qreal maxPageHeight READ maxPageHeight NOTIFY metaDataChanged) Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged FINAL) Q_PROPERTY(QPdfDocument::Status status READ status NOTIFY statusChanged FINAL) Q_PROPERTY(QString error READ error NOTIFY statusChanged FINAL) @@ -102,6 +104,9 @@ public: QDateTime modificationDate() { return m_doc.metaData(QPdfDocument::ModificationDate).toDateTime(); } Q_INVOKABLE QSizeF pagePointSize(int page) const; + qreal maxPageWidth() const; + qreal maxPageHeight() const; + Q_INVOKABLE qreal heightSumBeforePage(int page, qreal spacing = 0, int facingPages = 1) const; Q_SIGNALS: void sourceChanged(); @@ -113,10 +118,12 @@ Q_SIGNALS: private: QPdfDocument &document() { return m_doc; } + void updateMaxPageSize(); private: QUrl m_source; QPdfDocument m_doc; + QSizeF m_maxPageWidthHeight; friend class QQuickPdfLinkModel; friend class QQuickPdfSearchModel; diff --git a/src/pdf/quick/qquickpdfnavigationstack.cpp b/src/pdf/quick/qquickpdfnavigationstack.cpp index 51f65f032..7ba317557 100644 --- a/src/pdf/quick/qquickpdfnavigationstack.cpp +++ b/src/pdf/quick/qquickpdfnavigationstack.cpp @@ -80,11 +80,11 @@ void QQuickPdfNavigationStack::forward() ++m_currentHistoryIndex; m_changing = true; emit jumped(currentPage(), currentLocation(), currentZoom()); + if (currentZoomWas != currentZoom()) + emit currentZoomChanged(); emit currentPageChanged(); if (currentLocationWas != currentLocation()) emit currentLocationChanged(); - if (currentZoomWas != currentZoom()) - emit currentZoomChanged(); if (!backAvailableWas) emit backAvailableChanged(); if (forwardAvailableWas != forwardAvailable()) @@ -110,11 +110,11 @@ void QQuickPdfNavigationStack::back() --m_currentHistoryIndex; m_changing = true; emit jumped(currentPage(), currentLocation(), currentZoom()); + if (currentZoomWas != currentZoom()) + emit currentZoomChanged(); emit currentPageChanged(); if (currentLocationWas != currentLocation()) emit currentLocationChanged(); - if (currentZoomWas != currentZoom()) - emit currentZoomChanged(); if (backAvailableWas != backAvailable()) emit backAvailableChanged(); if (!forwardAvailableWas) @@ -183,15 +183,16 @@ void QQuickPdfNavigationStack::push(int page, QPointF location, qreal zoom) m_pageHistory.append(QExplicitlySharedDataPointer(new QPdfDestinationPrivate(page, location, zoom))); m_currentHistoryIndex = m_pageHistory.count() - 1; } + emit currentZoomChanged(); emit currentPageChanged(); emit currentLocationChanged(); - emit currentZoomChanged(); if (m_changing) return; if (!backAvailableWas) emit backAvailableChanged(); if (forwardAvailableWas) emit forwardAvailableChanged(); + emit jumped(page, location, zoom); qCDebug(qLcNav) << "push: index" << m_currentHistoryIndex << "page" << page << "@" << location << "zoom" << zoom << "-> history" << [this]() { @@ -212,7 +213,7 @@ void QQuickPdfNavigationStack::push(int page, QPointF location, qreal zoom) the most-recently-viewed destination rather than the destination that was last specified by push(). - The \c currentPageChanged, \c currentLocationChanged and \c currentZoomChanged + The \c currentZoomChanged, \c currentPageChanged and \c currentLocationChanged signals will be emitted if the respective properties are actually changed. The \l jumped signal is not emitted, because this operation represents smooth movement rather than a navigational jump. @@ -229,12 +230,12 @@ void QQuickPdfNavigationStack::update(int page, QPointF location, qreal zoom) m_pageHistory[m_currentHistoryIndex]->page = page; m_pageHistory[m_currentHistoryIndex]->location = location; m_pageHistory[m_currentHistoryIndex]->zoom = zoom; + if (currentZoomWas != zoom) + emit currentZoomChanged(); if (currentPageWas != page) emit currentPageChanged(); if (currentLocationWas != location) emit currentLocationChanged(); - if (currentZoomWas != zoom) - emit currentZoomChanged(); qCDebug(qLcNav) << "update: index" << m_currentHistoryIndex << "page" << page << "@" << location << "zoom" << zoom << "-> history" << [this]() { @@ -258,10 +259,8 @@ bool QQuickPdfNavigationStack::forwardAvailable() const /*! \qmlsignal PdfNavigationStack::jumped(int page, point location, qreal zoom) - This signal is emitted when either forward() or back() is called, to - distinguish navigational jumps from cases when push() is called. - Contrast with the \c currentPageChanged signal, which is emitted in all - cases, and does not include the \c page, \c location and \c zoom arguments. + This signal is emitted when forward(), back() or push() is called, but not + when update() is called. */ QT_END_NAMESPACE -- cgit v1.2.3 From ff13e6532975b5372280c02061cb1b7227cf6699 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Thu, 20 Feb 2020 14:09:27 +0100 Subject: Add PdfScrollablePageView, use it in the pdfviewer example PdfPageView might be useful in some cases, but we need to get feature parity with PdfMultiPageView as much as possible, including scrollbars. Including them in the view is convenient, but also less flexible. Change-Id: Ibbe6a090a5f5b1d340124986fe49672d682ddedb Reviewed-by: Shawn Rutledge --- src/pdf/quick/plugin.cpp | 1 + src/pdf/quick/qml/PdfScrollablePageView.qml | 249 ++++++++++++++++++++++++++++ src/pdf/quick/qquickpdflinkmodel.cpp | 2 +- src/pdf/quick/quick.pro | 1 + src/pdf/quick/resources.qrc | 1 + 5 files changed, 253 insertions(+), 1 deletion(-) create mode 100644 src/pdf/quick/qml/PdfScrollablePageView.qml (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/plugin.cpp b/src/pdf/quick/plugin.cpp index a831a09b6..bb68a817e 100644 --- a/src/pdf/quick/plugin.cpp +++ b/src/pdf/quick/plugin.cpp @@ -90,6 +90,7 @@ public: qmlRegisterType(QUrl("qrc:/qt-project.org/qtpdf/qml/PdfPageView.qml"), uri, 5, 15, "PdfPageView"); qmlRegisterType(QUrl("qrc:/qt-project.org/qtpdf/qml/PdfMultiPageView.qml"), uri, 5, 15, "PdfMultiPageView"); + qmlRegisterType(QUrl("qrc:/qt-project.org/qtpdf/qml/PdfScrollablePageView.qml"), uri, 5, 15, "PdfScrollablePageView"); } }; diff --git a/src/pdf/quick/qml/PdfScrollablePageView.qml b/src/pdf/quick/qml/PdfScrollablePageView.qml new file mode 100644 index 000000000..59bec04a2 --- /dev/null +++ b/src/pdf/quick/qml/PdfScrollablePageView.qml @@ -0,0 +1,249 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtPDF module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ +import QtQuick 2.14 +import QtQuick.Controls 2.14 +import QtQuick.Pdf 5.15 +import QtQuick.Shapes 1.14 +import Qt.labs.animation 1.0 + +Flickable { + // public API + // TODO 5.15: required property + property var document: undefined + property bool debug: false + property alias status: image.status + + property alias selectedText: selection.text + function copySelectionToClipboard() { + selection.copyToClipboard() + } + + // page navigation + property alias currentPage: navigationStack.currentPage + property alias backEnabled: navigationStack.backAvailable + property alias forwardEnabled: navigationStack.forwardAvailable + function back() { navigationStack.back() } + function forward() { navigationStack.forward() } + function goToPage(page) { goToLocation(page, Qt.point(0, 0), 0) } + function goToLocation(page, location, zoom) { + if (zoom > 0) + root.renderScale = zoom + navigationStack.push(page, location, zoom) + } + + // page scaling + property real renderScale: 1 + property real pageRotation: 0 + property alias sourceSize: image.sourceSize + function resetScale() { + paper.scale = 1 + root.renderScale = 1 + } + function scaleToWidth(width, height) { + var pagePointSize = document.pagePointSize(navigationStack.currentPage) + root.renderScale = root.width / (paper.rot90 ? pagePointSize.height : pagePointSize.width) + if (debug) + console.log("scaling", pagePointSize, "to fit", root.width, "rotated?", paper.rot90, "scale", root.renderScale) + root.contentX = 0 + root.contentY = 0 + } + function scaleToPage(width, height) { + + var pagePointSize = document.pagePointSize(navigationStack.currentPage) + root.renderScale = Math.min( + root.width / (paper.rot90 ? pagePointSize.height : pagePointSize.width), + root.height / (paper.rot90 ? pagePointSize.width : pagePointSize.height) ) + root.contentX = 0 + root.contentY = 0 + } + + // text search + property alias searchModel: searchModel + property alias searchString: searchModel.searchString + function searchBack() { --searchModel.currentResult } + function searchForward() { ++searchModel.currentResult } + + // implementation + id: root + contentWidth: paper.width + contentHeight: paper.height + ScrollBar.vertical: ScrollBar { } + ScrollBar.horizontal: ScrollBar { } + + onRenderScaleChanged: { + image.sourceSize.width = document.pagePointSize(navigationStack.currentPage).width * renderScale + image.sourceSize.height = 0 + paper.scale = 1 + } + + PdfSelection { + id: selection + document: root.document + page: navigationStack.currentPage + fromPoint: Qt.point(textSelectionDrag.centroid.pressPosition.x / image.pageScale, + textSelectionDrag.centroid.pressPosition.y / image.pageScale) + toPoint: Qt.point(textSelectionDrag.centroid.position.x / image.pageScale, + textSelectionDrag.centroid.position.y / image.pageScale) + hold: !textSelectionDrag.active && !tapHandler.pressed + } + + PdfSearchModel { + id: searchModel + document: root.document === undefined ? null : root.document + onCurrentPageChanged: root.goToPage(currentPage) + } + + PdfNavigationStack { + id: navigationStack + onCurrentPageChanged: searchModel.currentPage = currentPage + // TODO onCurrentLocationChanged: position currentLocation.x and .y in middle // currentPageChanged() MUST occur first! + onCurrentZoomChanged: root.renderScale = currentZoom + // TODO deal with horizontal location (need WheelHandler or Flickable probably) + } + + Rectangle { + id: paper + width: rot90 ? image.height : image.width + height: rot90 ? image.width : image.height + property real rotationModulus: Math.abs(root.pageRotation % 180) + property bool rot90: rotationModulus > 45 && rotationModulus < 135 + + Image { + id: image + currentFrame: navigationStack.currentPage + source: document.status === PdfDocument.Ready ? document.source : "" + asynchronous: true + fillMode: Image.PreserveAspectFit + rotation: root.pageRotation + anchors.centerIn: parent + property real pageScale: image.paintedWidth / document.pagePointSize(navigationStack.currentPage).width + } + + Shape { + anchors.fill: parent + opacity: 0.25 + visible: image.status === Image.Ready + ShapePath { + strokeWidth: 1 + strokeColor: "cyan" + fillColor: "steelblue" + scale: Qt.size(image.pageScale, image.pageScale) + PathMultiline { + paths: searchModel.currentPageBoundingPolygons + } + } + ShapePath { + strokeWidth: 1 + strokeColor: "orange" + fillColor: "cyan" + scale: Qt.size(image.pageScale, image.pageScale) + PathMultiline { + paths: searchModel.currentResultBoundingPolygons + } + } + ShapePath { + fillColor: "orange" + scale: Qt.size(image.pageScale, image.pageScale) + PathMultiline { + paths: selection.geometry + } + } + } + + Repeater { + model: PdfLinkModel { + id: linkModel + document: root.document + page: navigationStack.currentPage + } + delegate: Rectangle { + color: "transparent" + border.color: "lightgrey" + x: rect.x * image.pageScale + y: rect.y * image.pageScale + width: rect.width * image.pageScale + height: rect.height * image.pageScale + MouseArea { // TODO switch to TapHandler / HoverHandler in 5.15 + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + if (page >= 0) + navigationStack.push(page, Qt.point(0, 0), root.renderScale) + else + Qt.openUrlExternally(url) + } + } + } + } + + PinchHandler { + id: pinch + minimumScale: 0.1 + maximumScale: root.renderScale < 4 ? 2 : 1 + minimumRotation: 0 + maximumRotation: 0 + enabled: image.sourceSize.width < 5000 + onActiveChanged: + if (!active) { + var newSourceWidth = image.sourceSize.width * paper.scale + var ratio = newSourceWidth / image.sourceSize.width + if (ratio > 1.1 || ratio < 0.9) { + paper.scale = 1 + root.renderScale *= ratio + } + // TODO adjust contentX/Y to position the page so the same region is visible + paper.x = 0 + paper.y = 0 + } + grabPermissions: PointerHandler.CanTakeOverFromAnything + } + DragHandler { + id: pageMovingMiddleMouseDrag + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + acceptedButtons: Qt.MiddleButton + snapMode: DragHandler.NoSnap + } + DragHandler { + id: textSelectionDrag + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + target: null + } + TapHandler { + id: tapHandler + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + } + } +} diff --git a/src/pdf/quick/qquickpdflinkmodel.cpp b/src/pdf/quick/qquickpdflinkmodel.cpp index a3f552d17..f2ff3fd22 100644 --- a/src/pdf/quick/qquickpdflinkmodel.cpp +++ b/src/pdf/quick/qquickpdflinkmodel.cpp @@ -96,7 +96,7 @@ QT_BEGIN_NAMESPACE \endqml \note General-purpose PDF viewing capabilities are provided by - \l PdfPageView and \l PdfMultiPageView. PdfLinkModel is only needed + \l PdfScrollablePageView and \l PdfMultiPageView. PdfLinkModel is only needed when building PDF view components from scratch. */ diff --git a/src/pdf/quick/quick.pro b/src/pdf/quick/quick.pro index a0a39d414..b62b80346 100644 --- a/src/pdf/quick/quick.pro +++ b/src/pdf/quick/quick.pro @@ -8,6 +8,7 @@ IMPORT_VERSION = 1.0 PDF_QML_FILES = \ qml/PdfMultiPageView.qml \ qml/PdfPageView.qml \ + qml/PdfScrollablePageView.qml \ QML_FILES += $$PDF_QML_FILES qmldir diff --git a/src/pdf/quick/resources.qrc b/src/pdf/quick/resources.qrc index 282610d4c..20cac4827 100644 --- a/src/pdf/quick/resources.qrc +++ b/src/pdf/quick/resources.qrc @@ -2,5 +2,6 @@ qml/PdfMultiPageView.qml qml/PdfPageView.qml + qml/PdfScrollablePageView.qml -- cgit v1.2.3 From c0aa9d794378846e4cc0b6fe94f2765bc31cefdd Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Thu, 20 Feb 2020 14:50:56 +0100 Subject: PdfScrollablePageView: improve positional navigation Similar to f467edc97e66727be7fa3747913e4e01672d4b71: NavigationStack is kept up-to-date when the scroll position changes, and can go back to a previous position; and links can in theory jump to specific positions and zoom levels, scrolling such that a specific x coordinate is visible. Change-Id: I2add617914d89b0dc5389e7c3d12d11580a1f82f Reviewed-by: Shawn Rutledge --- src/pdf/quick/qml/PdfScrollablePageView.qml | 39 +++++++++++++++++++++++------ 1 file changed, 32 insertions(+), 7 deletions(-) (limited to 'src/pdf/quick') diff --git a/src/pdf/quick/qml/PdfScrollablePageView.qml b/src/pdf/quick/qml/PdfScrollablePageView.qml index 59bec04a2..55aa44bbf 100644 --- a/src/pdf/quick/qml/PdfScrollablePageView.qml +++ b/src/pdf/quick/qml/PdfScrollablePageView.qml @@ -57,7 +57,11 @@ Flickable { property alias forwardEnabled: navigationStack.forwardAvailable function back() { navigationStack.back() } function forward() { navigationStack.forward() } - function goToPage(page) { goToLocation(page, Qt.point(0, 0), 0) } + function goToPage(page) { + if (page === navigationStack.currentPage) + return + goToLocation(page, Qt.point(0, 0), 0) + } function goToLocation(page, location, zoom) { if (zoom > 0) root.renderScale = zoom @@ -81,7 +85,6 @@ Flickable { root.contentY = 0 } function scaleToPage(width, height) { - var pagePointSize = document.pagePointSize(navigationStack.currentPage) root.renderScale = Math.min( root.width / (paper.rot90 ? pagePointSize.height : pagePointSize.width), @@ -100,13 +103,30 @@ Flickable { id: root contentWidth: paper.width contentHeight: paper.height - ScrollBar.vertical: ScrollBar { } - ScrollBar.horizontal: ScrollBar { } + ScrollBar.vertical: ScrollBar { + onActiveChanged: + if (!active ) { + var currentLocation = Qt.point((root.contentX + root.width / 2) / root.renderScale, + (root.contentY + root.height / 2) / root.renderScale) + navigationStack.update(navigationStack.currentPage, currentLocation, root.renderScale) + } + } + ScrollBar.horizontal: ScrollBar { + onActiveChanged: + if (!active ) { + var currentLocation = Qt.point((root.contentX + root.width / 2) / root.renderScale, + (root.contentY + root.height / 2) / root.renderScale) + navigationStack.update(navigationStack.currentPage, currentLocation, root.renderScale) + } + } onRenderScaleChanged: { image.sourceSize.width = document.pagePointSize(navigationStack.currentPage).width * renderScale image.sourceSize.height = 0 paper.scale = 1 + var currentLocation = Qt.point((root.contentX + root.width / 2) / root.renderScale, + (root.contentY + root.height / 2) / root.renderScale) + navigationStack.update(navigationStack.currentPage, currentLocation, root.renderScale) } PdfSelection { @@ -128,10 +148,15 @@ Flickable { PdfNavigationStack { id: navigationStack + onJumped: { + root.renderScale = zoom + root.contentX = Math.max(0, location.x * root.renderScale - root.width / 2) + root.contentY = Math.max(0, location.y * root.renderScale - root.height / 2) + if (root.debug) + console.log("going to zoom", zoom, "loc", location, + "on page", page, "ended up @", root.contentX + ", " + root.contentY) + } onCurrentPageChanged: searchModel.currentPage = currentPage - // TODO onCurrentLocationChanged: position currentLocation.x and .y in middle // currentPageChanged() MUST occur first! - onCurrentZoomChanged: root.renderScale = currentZoom - // TODO deal with horizontal location (need WheelHandler or Flickable probably) } Rectangle { -- cgit v1.2.3