From 9231d3444555945297857ee4aae05919083ea479 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Thu, 9 Jan 2020 18:11:05 +0100 Subject: QPdfDocument: check first page in load(), current page in render() When a viewer application is opened with a book-sized PDF, there's no need to check hundreds of pages before we can render the first page. Using it in QPdfIOHandler is even worse because this check needs to be repeated each time we advance from one page to the next. But we do need to ensure that the page we want to render is available. Amends 945840bd067d9ca3179a667f48b451cc2087931b Change-Id: Ib6576b1b91c63c2b57893d14b05632eff8cc4a15 Reviewed-by: Michal Klocek --- src/pdf/api/qpdfdocument_p.h | 1 + src/pdf/qpdfdocument.cpp | 49 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/pdf/api/qpdfdocument_p.h b/src/pdf/api/qpdfdocument_p.h index 928754210..15d8b8259 100644 --- a/src/pdf/api/qpdfdocument_p.h +++ b/src/pdf/api/qpdfdocument_p.h @@ -98,6 +98,7 @@ public: void _q_copyFromSequentialSourceDevice(); void tryLoadDocument(); void checkComplete(); + bool checkPageComplete(int page); void setStatus(QPdfDocument::Status status); static FPDF_BOOL fpdf_IsDataAvail(struct _FX_FILEAVAIL* pThis, size_t offset, size_t size); diff --git a/src/pdf/qpdfdocument.cpp b/src/pdf/qpdfdocument.cpp index 690953691..4c4f8f24e 100644 --- a/src/pdf/qpdfdocument.cpp +++ b/src/pdf/qpdfdocument.cpp @@ -186,7 +186,35 @@ void QPdfDocumentPrivate::load(QIODevice *newDevice, bool transferDeviceOwnershi } else { device = newDevice; initiateAsyncLoadWithTotalSizeKnown(device->size()); - checkComplete(); + if (!avail) { + setStatus(QPdfDocument::Error); + return; + } + + if (!doc) + tryLoadDocument(); + + if (!doc) { + updateLastError(); + setStatus(QPdfDocument::Error); + return; + } + + QPdfMutexLocker lock; + const int newPageCount = FPDF_GetPageCount(doc); + lock.unlock(); + if (newPageCount != pageCount) { + pageCount = newPageCount; + emit q->pageCountChanged(pageCount); + } + + // If it's a local file, and the first page is available, probably the whole document is available. + if (checkPageComplete(0)) { + setStatus(QPdfDocument::Ready); + } else { + updateLastError(); + setStatus(QPdfDocument::Error); + } } } @@ -311,6 +339,23 @@ void QPdfDocumentPrivate::checkComplete() } } +bool QPdfDocumentPrivate::checkPageComplete(int page) +{ + if (loadComplete) + return true; + + QPdfMutexLocker lock; + int result = PDF_DATA_NOTAVAIL; + while (result == PDF_DATA_NOTAVAIL) + result = FPDFAvail_IsPageAvail(avail, page, this); + lock.unlock(); + + if (result == PDF_DATA_ERROR) + updateLastError(); + + return (result != PDF_DATA_ERROR); +} + void QPdfDocumentPrivate::setStatus(QPdfDocument::Status documentStatus) { if (status == documentStatus) @@ -581,7 +626,7 @@ QSizeF QPdfDocument::pageSize(int page) const */ QImage QPdfDocument::render(int page, QSize imageSize, QPdfDocumentRenderOptions renderOptions) { - if (!d->doc) + if (!d->doc || !d->checkPageComplete(page)) return QImage(); const QPdfMutexLocker lock; -- cgit v1.2.3 From 56ba4a2326b6c36c365ac9de2e1f596493c2324b Mon Sep 17 00:00:00 2001 From: Leena Miettinen Date: Fri, 24 Jan 2020 16:54:01 +0100 Subject: Doc: Add docs for the Qt PDF Viewer example Task-number: QTBUG-81560 Change-Id: I159747a097a6a6a28c2355e8ff6b6c59a4f3a29a Reviewed-by: Shawn Rutledge --- .../pdfwidgets/pdfviewer/doc/src/pdfviewer.qdoc | 108 +++++++++++++++++++++ src/pdf/doc/qtpdf.qdocconf | 3 +- src/pdf/doc/src/qtpdf-examples.qdoc | 37 +++++++ src/pdf/doc/src/qtpdf-index.qdoc | 6 ++ 4 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 examples/pdfwidgets/pdfviewer/doc/src/pdfviewer.qdoc create mode 100644 src/pdf/doc/src/qtpdf-examples.qdoc diff --git a/examples/pdfwidgets/pdfviewer/doc/src/pdfviewer.qdoc b/examples/pdfwidgets/pdfviewer/doc/src/pdfviewer.qdoc new file mode 100644 index 000000000..9c7b3deed --- /dev/null +++ b/examples/pdfwidgets/pdfviewer/doc/src/pdfviewer.qdoc @@ -0,0 +1,108 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** 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. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: https://www.gnu.org/licenses/fdl-1.3.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \example pdfviewer + \ingroup qtpdf-examples + + \title PDF Viewer Example + \brief Renders PDF documents. + + \image pdfviewer.png + + \e {PDF Viewer} demonstrates how to use the \l QPdfDocument class to render + PDF documents and the \l QPdfPageNavigation class to navigate them. + + Qt Creator and the integrated Qt Designer were used to create the example + UI and to connect it to the code. This affects the code, which might be + somewhat different to what you would typically write by hand. + For more information about using Qt Designer, see \l{Qt Designer Manual} + and \l{Qt Creator: Creating a Qt Widget Based Application}. + + \include examples-run.qdocinc + + \section1 Creating the Main Window + + The MainWindow class inherits the QMainWindow class: + + \quotefromfile pdfviewer/mainwindow.h + \skipto public QMainWindow + \printuntil ~MainWindow() + + The class declares public and private slots that match the actions of the + selectors: + + \printuntil on_actionContinuous_triggered() + + The actual layout of the main window is specified in a \c{.ui} file. The + widgets and actions are available at runtime in the \c ui member variable. + + \printuntil Ui:: + + The \c m_zoomSelector variable holds the zoom selector and the + \c m_pageSelector holds the page selector. The \c m_document + variable is an instance of the QPdfDocument class that contains + the PDF document. + + \printuntil } + + The actual setup of the different objects is done in the MainWindow + constructor: + + \quotefromfile pdfviewer/mainwindow.cpp + \skipto MainWindow:: + \printuntil { + + The constructor first calls \c setupUi() to construct the zoom and page + selectors according to the UI file. We set the maximum width of the + selectors. + + \printuntil addWidget(m_pageSelector) + + We use the \l QPdfPageNavigation class to handle the navigation through a + PDF document: + + \printuntil setPageNavigation + + We connect the \c zoomModeChanged and \c zoomFactor changed signals of the + PDF view to the functions that reset the zoom selector: + + \printuntil reset() + + We then load the PDF document to the viewer: + + \dots + \skipto pdfView + \printuntil ; + + Finally, we connect the \c zoomFactorChanged signal to the function that + sets the value of the zoom selector: + + \printuntil } + + \section1 Files and Attributions +*/ diff --git a/src/pdf/doc/qtpdf.qdocconf b/src/pdf/doc/qtpdf.qdocconf index b55b25327..67b969419 100644 --- a/src/pdf/doc/qtpdf.qdocconf +++ b/src/pdf/doc/qtpdf.qdocconf @@ -37,7 +37,8 @@ depends += qtcore \ qtwidgets \ qtgui \ qtdoc \ - qmake + qmake \ + qtdesigner headerdirs += ../api \ ../quick diff --git a/src/pdf/doc/src/qtpdf-examples.qdoc b/src/pdf/doc/src/qtpdf-examples.qdoc new file mode 100644 index 000000000..9daa0e7f8 --- /dev/null +++ b/src/pdf/doc/src/qtpdf-examples.qdoc @@ -0,0 +1,37 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the documentation of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:FDL$ +** 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. +** +** GNU Free Documentation License Usage +** Alternatively, this file may be used under the terms of the GNU Free +** Documentation License version 1.3 as published by the Free Software +** Foundation and appearing in the file included in the packaging of +** this file. Please review the following information to ensure +** the GNU Free Documentation License version 1.3 requirements +** will be met: https://www.gnu.org/licenses/fdl-1.3.html. +** $QT_END_LICENSE$ +** +****************************************************************************/ + +/*! + \group qtpdf-examples + \ingroup all-examples + + \title Qt PDF Examples + \brief Using the classes and types in the Qt PDF module. + + The following examples illustrate how to use the C++ classes and QML types + in the \l{Qt PDF} module to render PDF documents. +*/ diff --git a/src/pdf/doc/src/qtpdf-index.qdoc b/src/pdf/doc/src/qtpdf-index.qdoc index 6893272ac..d50d1482c 100644 --- a/src/pdf/doc/src/qtpdf-index.qdoc +++ b/src/pdf/doc/src/qtpdf-index.qdoc @@ -56,6 +56,12 @@ \li \l{Qt PDF Overview} \endlist + \section1 Examples + + \list + \li \l{Qt PDF Examples} + \endlist + \section1 API Reference \list -- cgit v1.2.3 From c3a5b107698e1194d18459ab696651cf5ac1e700 Mon Sep 17 00:00:00 2001 From: Leena Miettinen Date: Mon, 27 Jan 2020 13:02:47 +0100 Subject: Doc: Add dependency to the Qt Quick module to fix QDoc warnings Change-Id: Ia56a07bc2d53bd4a2166566a9c95126a5d2f9154 Reviewed-by: Paul Wicking --- src/pdf/doc/qtpdf.qdocconf | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pdf/doc/qtpdf.qdocconf b/src/pdf/doc/qtpdf.qdocconf index 67b969419..7f7a0e80b 100644 --- a/src/pdf/doc/qtpdf.qdocconf +++ b/src/pdf/doc/qtpdf.qdocconf @@ -38,7 +38,8 @@ depends += qtcore \ qtgui \ qtdoc \ qmake \ - qtdesigner + qtdesigner \ + qtquick headerdirs += ../api \ ../quick -- cgit v1.2.3 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(-) 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 d1749c375ea924390d19225b40ef2631b524849b Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Wed, 22 Jan 2020 00:08:15 +0100 Subject: Use SpinBox for page navigation to get jump-to-page feature Change-Id: Ic4c7d1a7458995415452e899b3dc369c9fe574f4 Reviewed-by: Shawn Rutledge --- examples/pdf/pdfviewer/viewer.qml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/examples/pdf/pdfviewer/viewer.qml b/examples/pdf/pdfviewer/viewer.qml index adc2a4b5b..8f2603659 100644 --- a/examples/pdf/pdfviewer/viewer.qml +++ b/examples/pdf/pdfviewer/viewer.qml @@ -113,20 +113,19 @@ ApplicationWindow { onTriggered: pageView.rotation += 90 } } - ToolButton { - action: Action { - shortcut: StandardKey.MoveToPreviousPage - icon.source: "resources/go-previous-view-page.svg" - enabled: pageView.currentPage > 0 - onTriggered: pageView.currentPage-- + SpinBox { + id: currentPageSB + from: 1 + to: document.pageCount + value: 1 + editable: true + Shortcut { + sequence: StandardKey.MoveToPreviousPage + onActivated: currentPageSB.value-- } - } - ToolButton { - action: Action { - shortcut: StandardKey.MoveToNextPage - icon.source: "resources/go-next-view-page.svg" - enabled: pageView.currentPage < pageView.pageCount - 1 - onTriggered: pageView.currentPage++ + Shortcut { + sequence: StandardKey.MoveToNextPage + onActivated: currentPageSB.value++ } } TextField { @@ -184,6 +183,7 @@ ApplicationWindow { PdfPageView { id: pageView + currentPage: currentPageSB.value - 1 document: PdfDocument { id: document onStatusChanged: if (status === PdfDocument.Error) errorDialog.open() -- cgit v1.2.3 From 4f5f0705bc161ff95899fdb2c5fcdb4581bf15bb Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Mon, 23 Dec 2019 07:34:16 +0100 Subject: QPdfDocument and QPdfIOHandler: add scale and clip features Like some other image plugins, the PDF plugin now supports ImageOption::ScaledClipRect and ScaledSize. Change-Id: Ie7278752e49c885cc4580f30af1ec5e6310f8334 Reviewed-by: Michal Klocek --- src/pdf/api/qpdfdocumentrenderoptions.h | 12 ++++++++- src/pdf/qpdfdocument.cpp | 34 ++++++++++++++++++++++++-- src/plugins/imageformats/pdf/qpdfiohandler.cpp | 10 ++++++-- 3 files changed, 51 insertions(+), 5 deletions(-) diff --git a/src/pdf/api/qpdfdocumentrenderoptions.h b/src/pdf/api/qpdfdocumentrenderoptions.h index 99a5db2e3..873be0085 100644 --- a/src/pdf/api/qpdfdocumentrenderoptions.h +++ b/src/pdf/api/qpdfdocumentrenderoptions.h @@ -39,6 +39,7 @@ #include #include +#include QT_BEGIN_NAMESPACE @@ -53,6 +54,12 @@ public: Q_DECL_CONSTEXPR QPdf::RenderFlags renderFlags() const Q_DECL_NOTHROW { return static_cast(bits.renderFlags); } Q_DECL_RELAXED_CONSTEXPR void setRenderFlags(QPdf::RenderFlags _renderFlags) Q_DECL_NOTHROW { bits.renderFlags = _renderFlags; } + Q_DECL_CONSTEXPR QRect scaledClipRect() const Q_DECL_NOTHROW { return m_clipRect; } + Q_DECL_RELAXED_CONSTEXPR void setScaledClipRect(const QRect &r) Q_DECL_NOTHROW { m_clipRect = r; } + + Q_DECL_CONSTEXPR QSize scaledSize() const Q_DECL_NOTHROW { return m_scaledSize; } + Q_DECL_RELAXED_CONSTEXPR void setScaledSize(const QSize &s) Q_DECL_NOTHROW { m_scaledSize = s; } + private: friend Q_DECL_CONSTEXPR inline bool operator==(QPdfDocumentRenderOptions lhs, QPdfDocumentRenderOptions rhs) Q_DECL_NOTHROW; @@ -68,13 +75,16 @@ private: Bits bits; quint64 data; }; + + QRect m_clipRect; + QSize m_scaledSize; }; Q_DECLARE_TYPEINFO(QPdfDocumentRenderOptions, Q_PRIMITIVE_TYPE); Q_DECL_CONSTEXPR inline bool operator==(QPdfDocumentRenderOptions lhs, QPdfDocumentRenderOptions rhs) Q_DECL_NOTHROW { - return lhs.data == rhs.data; + return lhs.data == rhs.data && lhs.m_clipRect == rhs.m_clipRect && lhs.m_scaledSize == rhs.m_scaledSize; } Q_DECL_CONSTEXPR inline bool operator!=(QPdfDocumentRenderOptions lhs, QPdfDocumentRenderOptions rhs) Q_DECL_NOTHROW diff --git a/src/pdf/qpdfdocument.cpp b/src/pdf/qpdfdocument.cpp index 4c4f8f24e..728bf1245 100644 --- a/src/pdf/qpdfdocument.cpp +++ b/src/pdf/qpdfdocument.cpp @@ -46,6 +46,7 @@ #include #include #include +#include QT_BEGIN_NAMESPACE @@ -675,12 +676,41 @@ QImage QPdfDocument::render(int page, QSize imageSize, QPdfDocumentRenderOptions if (renderFlags & QPdf::RenderPathAliased) flags |= FPDF_RENDER_NO_SMOOTHPATH; - FPDF_RenderPageBitmap(bitmap, pdfPage, 0, 0, result.width(), result.height(), rotation, flags); + if (renderOptions.scaledClipRect().isValid()) { + const QRect &clipRect = renderOptions.scaledClipRect(); + + // TODO take rotation into account, like cpdf_page.cpp lines 145-178 + float x0 = clipRect.left(); + float y0 = clipRect.top(); + float x1 = clipRect.left(); + float y1 = clipRect.bottom(); + float x2 = clipRect.right(); + float y2 = clipRect.top(); + QSizeF origSize = pageSize(page); + QVector2D pageScale(1, 1); + if (!renderOptions.scaledSize().isNull()) { + pageScale = QVector2D(renderOptions.scaledSize().width() / float(origSize.width()), + renderOptions.scaledSize().height() / float(origSize.height())); + } + FS_MATRIX matrix {(x2 - x0) / result.width() * pageScale.x(), + (y2 - y0) / result.width() * pageScale.x(), + (x1 - x0) / result.height() * pageScale.y(), + (y1 - y0) / result.height() * pageScale.y(), -x0, -y0}; + + FS_RECTF clipRectF { 0, 0, float(imageSize.width()), float(imageSize.height()) }; + + FPDF_RenderPageBitmapWithMatrix(bitmap, pdfPage, &matrix, &clipRectF, flags); + qCDebug(qLcDoc) << "matrix" << matrix.a << matrix.b << matrix.c << matrix.d << matrix.e << matrix.f; + qCDebug(qLcDoc) << "page" << page << "region" << renderOptions.scaledClipRect() + << "size" << imageSize << "took" << timer.elapsed() << "ms"; + } else { + FPDF_RenderPageBitmap(bitmap, pdfPage, 0, 0, result.width(), result.height(), rotation, flags); + qCDebug(qLcDoc) << "page" << page << "size" << imageSize << "took" << timer.elapsed() << "ms"; + } FPDFBitmap_Destroy(bitmap); FPDF_ClosePage(pdfPage); - qCDebug(qLcDoc) << "page" << page << imageSize << "took" << timer.elapsed() << "ms"; return result; } diff --git a/src/plugins/imageformats/pdf/qpdfiohandler.cpp b/src/plugins/imageformats/pdf/qpdfiohandler.cpp index 9df85cf08..739e8b34c 100644 --- a/src/plugins/imageformats/pdf/qpdfiohandler.cpp +++ b/src/plugins/imageformats/pdf/qpdfiohandler.cpp @@ -98,7 +98,8 @@ bool QPdfIOHandler::read(QImage *image) if (m_page < 0) m_page = 0; const bool xform = (m_clipRect.isValid() || m_scaledSize.isValid() || m_scaledClipRect.isValid()); - QSize finalSize = m_doc.pageSize(m_page).toSize(); + QSize pageSize = m_doc.pageSize(m_page).toSize(); + QSize finalSize = pageSize; QRectF bounds; if (xform && !finalSize.isEmpty()) { bounds = QRectF(QPointF(0,0), QSizeF(finalSize)); @@ -112,6 +113,7 @@ bool QPdfIOHandler::read(QImage *image) sc = QSizeF(qreal(m_scaledSize.width()) / finalSize.width(), qreal(m_scaledSize.height()) / finalSize.height()); finalSize = m_scaledSize; + pageSize = m_scaledSize; } if (m_scaledClipRect.isValid()) { tr2 = -m_scaledClipRect.topLeft(); @@ -133,9 +135,13 @@ bool QPdfIOHandler::read(QImage *image) } } if (!finalSize.isEmpty()) { + QPdfDocumentRenderOptions options; + if (m_scaledClipRect.isValid()) + options.setScaledClipRect(m_scaledClipRect); + options.setScaledSize(pageSize); image->fill(m_backColor.rgba()); QPainter p(image); - QImage pageImage = m_doc.render(m_page, finalSize); + QImage pageImage = m_doc.render(m_page, finalSize, options); p.drawImage(0, 0, pageImage); p.end(); } -- 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 --- examples/pdf/pdfviewer/resources/edit-copy.svg | 15 ++ examples/pdf/pdfviewer/viewer.qml | 8 + examples/pdf/pdfviewer/viewer.qrc | 1 + src/pdf/api/qpdfdocument.h | 3 + src/pdf/api/qpdfselection.h | 83 ++++++++ src/pdf/api/qpdfselection_p.h | 59 ++++++ src/pdf/pdfcore.pro | 3 + src/pdf/qpdfdocument.cpp | 47 +++++ src/pdf/qpdfselection.cpp | 138 +++++++++++++ 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 + tests/manual/quick/pdf/withdoc.qml | 22 ++ 16 files changed, 804 insertions(+) create mode 100644 examples/pdf/pdfviewer/resources/edit-copy.svg create mode 100644 src/pdf/api/qpdfselection.h create mode 100644 src/pdf/api/qpdfselection_p.h create mode 100644 src/pdf/qpdfselection.cpp create mode 100644 src/pdf/quick/qquickpdfselection.cpp create mode 100644 src/pdf/quick/qquickpdfselection_p.h diff --git a/examples/pdf/pdfviewer/resources/edit-copy.svg b/examples/pdf/pdfviewer/resources/edit-copy.svg new file mode 100644 index 000000000..9dd16877d --- /dev/null +++ b/examples/pdf/pdfviewer/resources/edit-copy.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/examples/pdf/pdfviewer/viewer.qml b/examples/pdf/pdfviewer/viewer.qml index 8f2603659..99d9b8ed0 100644 --- a/examples/pdf/pdfviewer/viewer.qml +++ b/examples/pdf/pdfviewer/viewer.qml @@ -128,6 +128,14 @@ ApplicationWindow { onActivated: currentPageSB.value++ } } + ToolButton { + action: Action { + shortcut: StandardKey.Copy + icon.source: "resources/edit-copy.svg" + enabled: pageView.selectedText !== "" + onTriggered: pageView.copySelectionToClipboard() + } + } TextField { id: searchField placeholderText: "search" diff --git a/examples/pdf/pdfviewer/viewer.qrc b/examples/pdf/pdfviewer/viewer.qrc index 78f9c8d30..c376803c2 100644 --- a/examples/pdf/pdfviewer/viewer.qrc +++ b/examples/pdf/pdfviewer/viewer.qrc @@ -2,6 +2,7 @@ viewer.qml resources/edit-clear.svg + resources/edit-copy.svg resources/go-next-view-page.svg resources/go-previous-view-page.svg resources/rotate-left.svg diff --git a/src/pdf/api/qpdfdocument.h b/src/pdf/api/qpdfdocument.h index 1252fa6d2..46d197f1d 100644 --- a/src/pdf/api/qpdfdocument.h +++ b/src/pdf/api/qpdfdocument.h @@ -41,6 +41,7 @@ #include #include #include +#include "qpdfselection.h" QT_BEGIN_NAMESPACE @@ -111,6 +112,8 @@ public: QImage render(int page, QSize imageSize, QPdfDocumentRenderOptions options = QPdfDocumentRenderOptions()); + Q_INVOKABLE QPdfSelection getSelection(int page, QPointF start, QPointF end); + Q_SIGNALS: void passwordChanged(); void passwordRequired(); diff --git a/src/pdf/api/qpdfselection.h b/src/pdf/api/qpdfselection.h new file mode 100644 index 000000000..900b203cf --- /dev/null +++ b/src/pdf/api/qpdfselection.h @@ -0,0 +1,83 @@ +/**************************************************************************** +** +** 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 QPDFSELECTION_H +#define QPDFSELECTION_H + +#include +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QPdfSelectionPrivate; + +class Q_PDF_EXPORT QPdfSelection +{ + Q_GADGET + Q_PROPERTY(bool valid READ isValid) + Q_PROPERTY(QVector bounds READ bounds) + Q_PROPERTY(QString text READ text) + +public: + QPdfSelection(const QPdfSelection &other); + ~QPdfSelection(); + QPdfSelection &operator=(const QPdfSelection &other); + inline QPdfSelection &operator=(QPdfSelection &&other) noexcept { swap(other); return *this; } + void swap(QPdfSelection &other) noexcept { d.swap(other.d); } + bool isValid() const; + QVector bounds() const; + QString text() const; +#if QT_CONFIG(clipboard) + void copyToClipboard(QClipboard::Mode mode = QClipboard::Clipboard) const; +#endif + +private: + QPdfSelection(); + QPdfSelection(const QString &text, QVector bounds); + QPdfSelection(QPdfSelectionPrivate *d); + friend class QPdfDocument; + friend class QQuickPdfSelection; + +private: + QExplicitlySharedDataPointer d; +}; + +QT_END_NAMESPACE + +#endif // QPDFSELECTION_H diff --git a/src/pdf/api/qpdfselection_p.h b/src/pdf/api/qpdfselection_p.h new file mode 100644 index 000000000..37145f7f9 --- /dev/null +++ b/src/pdf/api/qpdfselection_p.h @@ -0,0 +1,59 @@ +/**************************************************************************** +** +** 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 QPDFSELECTION_P_H +#define QPDFSELECTION_P_H + +#include +#include + +QT_BEGIN_NAMESPACE + +class QPdfSelectionPrivate : public QSharedData +{ +public: + QPdfSelectionPrivate() = default; + QPdfSelectionPrivate(const QString &text, QVector bounds) + : text(text), + bounds(bounds) { } + + QString text; + QVector bounds; +}; + +QT_END_NAMESPACE + +#endif // QPDFSELECTION_P_H diff --git a/src/pdf/pdfcore.pro b/src/pdf/pdfcore.pro index 1f9f21e0a..f50c3aa06 100644 --- a/src/pdf/pdfcore.pro +++ b/src/pdf/pdfcore.pro @@ -64,6 +64,7 @@ SOURCES += \ qpdfpagenavigation.cpp \ qpdfpagerenderer.cpp \ qpdfsearchmodel.cpp \ + qpdfselection.cpp \ # all "public" headers must be in "api" for sync script and to hide auto generated headers # by Chromium in case of in-source build @@ -79,5 +80,7 @@ HEADERS += \ api/qpdfpagerenderer.h \ api/qpdfsearchmodel.h \ qpdfsearchmodel_p.h \ + api/qpdfselection.h \ + api/qpdfselection_p.h \ load(qt_module) diff --git a/src/pdf/qpdfdocument.cpp b/src/pdf/qpdfdocument.cpp index 728bf1245..37eeb8426 100644 --- a/src/pdf/qpdfdocument.cpp +++ b/src/pdf/qpdfdocument.cpp @@ -38,6 +38,7 @@ #include "qpdfdocument_p.h" #include "third_party/pdfium/public/fpdf_doc.h" +#include "third_party/pdfium/public/fpdf_text.h" #include #include @@ -53,6 +54,7 @@ QT_BEGIN_NAMESPACE // The library is not thread-safe at all, it has a lot of global variables. Q_GLOBAL_STATIC_WITH_ARGS(QMutex, pdfMutex, (QMutex::Recursive)); static int libraryRefCount; +static const double CharacterHitTolerance = 6.0; Q_LOGGING_CATEGORY(qLcDoc, "qt.pdf.document") QPdfMutexLocker::QPdfMutexLocker() @@ -714,6 +716,51 @@ QImage QPdfDocument::render(int page, QSize imageSize, QPdfDocumentRenderOptions return result; } +/*! + Returns information about the text on the given \a page that can be found + between the given \a start and \a end points, if any. +*/ +QPdfSelection QPdfDocument::getSelection(int page, QPointF start, QPointF end) +{ + const QPdfMutexLocker lock; + FPDF_PAGE pdfPage = FPDF_LoadPage(d->doc, page); + double pageHeight = FPDF_GetPageHeight(pdfPage); + FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage); + int startIndex = FPDFText_GetCharIndexAtPos(textPage, start.x(), pageHeight - start.y(), + CharacterHitTolerance, CharacterHitTolerance); + int endIndex = FPDFText_GetCharIndexAtPos(textPage, end.x(), pageHeight - end.y(), + CharacterHitTolerance, CharacterHitTolerance); + if (startIndex >= 0 && endIndex != startIndex) { + QString text; + if (startIndex > endIndex) + qSwap(startIndex, endIndex); + int count = endIndex - startIndex + 1; + QVector buf(count + 1); + // TODO is that enough space in case one unicode character is more than one in utf-16? + int len = FPDFText_GetText(textPage, startIndex, count, buf.data()); + Q_ASSERT(len - 1 <= count); // len is number of characters written, including the terminator + text = QString::fromUtf16(buf.constData(), len - 1); + QVector bounds; + int rectCount = FPDFText_CountRects(textPage, startIndex, endIndex - startIndex); + for (int i = 0; i < rectCount; ++i) { + double l, r, b, t; + FPDFText_GetRect(textPage, i, &l, &t, &r, &b); + QPolygonF poly; + poly << QPointF(l, pageHeight - t); + poly << QPointF(r, pageHeight - t); + poly << QPointF(r, pageHeight - b); + poly << QPointF(l, pageHeight - b); + poly << QPointF(l, pageHeight - t); + bounds << poly; + } + qCDebug(qLcDoc) << page << start << "->" << end << "found" << startIndex << "->" << endIndex << text; + return QPdfSelection(text, bounds); + } + + qDebug(qLcDoc) << page << start << "->" << end << "nothing found"; + return QPdfSelection(); +} + QT_END_NAMESPACE #include "moc_qpdfdocument.cpp" diff --git a/src/pdf/qpdfselection.cpp b/src/pdf/qpdfselection.cpp new file mode 100644 index 000000000..8c3d6fde0 --- /dev/null +++ b/src/pdf/qpdfselection.cpp @@ -0,0 +1,138 @@ +/**************************************************************************** +** +** 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 "qpdfselection.h" +#include "qpdfselection_p.h" +#include + +QT_BEGIN_NAMESPACE + +/*! + \class QPdfSelection + \since 5.15 + \inmodule QtPdf + + \brief The QPdfSelection class defines a range of text that has been selected + on one page in a PDF document, and its geometric boundaries. + + \sa QPdfDocument::getSelection() +*/ + +/*! + Constructs an invalid selection. + + \sa valid +*/ +QPdfSelection::QPdfSelection() + : d(new QPdfSelectionPrivate()) +{ +} + +/*! + \internal + Constructs a selection including the range of characters that make up the + \a text string, and which take up space on the page within the polygon + regions given in \a bounds. +*/ +QPdfSelection::QPdfSelection(const QString &text, QVector bounds) + : d(new QPdfSelectionPrivate(text, bounds)) +{ +} + +QPdfSelection::QPdfSelection(QPdfSelectionPrivate *d) + : d(d) +{ +} + +QPdfSelection::QPdfSelection(const QPdfSelection &other) + : d(other.d) +{ +} + +QPdfSelection::~QPdfSelection() +{ +} + +/*! + \property QPdfSelection::valid + + This property holds whether the selection is valid. +*/ +bool QPdfSelection::isValid() const +{ + return !d->bounds.isEmpty(); +} + +/*! + \property QPdfSelection::bounds + + This property holds a set of regions that the selected text occupies on the + page, represented as polygons. The coordinate system for the polygons has + the origin at the upper-left corner of the page, and the units are + \l {https://en.wikipedia.org/wiki/Point_(typography)}{points}. + + \note For now, the polygons returned from \l QPdfDocument::getSelection() + are always rectangles; but in the future it may be possible to represent + more complex regions. +*/ +QVector QPdfSelection::bounds() const +{ + return d->bounds; +} + +/*! + \property QPdfSelection::text + + This property holds the selected text. +*/ +QString QPdfSelection::text() const +{ + return d->text; +} + +#if QT_CONFIG(clipboard) +/*! + Copies \l text to the \l {QGuiApplication::clipboard()}{system clipboard}. +*/ +void QPdfSelection::copyToClipboard(QClipboard::Mode mode) const +{ + QGuiApplication::clipboard()->setText(d->text, mode); +} +#endif + +QT_END_NAMESPACE + +#include "moc_qpdfselection.cpp" 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 diff --git a/tests/manual/quick/pdf/withdoc.qml b/tests/manual/quick/pdf/withdoc.qml index 0fed5b16e..fe3297ee8 100644 --- a/tests/manual/quick/pdf/withdoc.qml +++ b/tests/manual/quick/pdf/withdoc.qml @@ -51,6 +51,7 @@ import QtQuick 2.14 import QtQuick.Controls 2.14 import Qt.labs.platform 1.1 as Platform import QtQuick.Pdf 5.15 +import QtQuick.Shapes 1.14 import QtQuick.Window 2.14 Window { @@ -72,6 +73,15 @@ Window { onAccepted: doc.source = file } + PdfSelection { + id: selection + document: doc + page: image.currentFrame + fromPoint: dragHandler.centroid.pressPosition + toPoint: dragHandler.centroid.position + hold: !dragHandler.active + } + Column { id: column anchors.fill: parent @@ -149,6 +159,18 @@ Window { onActivated: Qt.quit() } } + + Shape { + anchors.fill: parent + opacity: 0.25 + ShapePath { + fillColor: "cyan" + PathMultiline { + id: selectionBoundaries + paths: selection.geometry + } + } + } } } } -- 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/api/qpdfdocument.h | 1 + src/pdf/api/qpdflinkmodel_p.h | 106 +++++++++++++++ src/pdf/api/qpdflinkmodel_p_p.h | 91 +++++++++++++ src/pdf/pdfcore.pro | 3 + src/pdf/qpdflinkmodel.cpp | 251 +++++++++++++++++++++++++++++++++++ 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 + tests/manual/quick/pdf/withdoc.qml | 25 ++++ 12 files changed, 727 insertions(+) create mode 100644 src/pdf/api/qpdflinkmodel_p.h create mode 100644 src/pdf/api/qpdflinkmodel_p_p.h create mode 100644 src/pdf/qpdflinkmodel.cpp create mode 100644 src/pdf/quick/qquickpdflinkmodel.cpp create mode 100644 src/pdf/quick/qquickpdflinkmodel_p.h diff --git a/src/pdf/api/qpdfdocument.h b/src/pdf/api/qpdfdocument.h index 46d197f1d..9d3a4bb19 100644 --- a/src/pdf/api/qpdfdocument.h +++ b/src/pdf/api/qpdfdocument.h @@ -122,6 +122,7 @@ Q_SIGNALS: private: friend class QPdfBookmarkModelPrivate; + friend class QPdfLinkModelPrivate; friend class QPdfSearchModel; Q_PRIVATE_SLOT(d, void _q_tryLoadingWithSizeFromContentHeader()) diff --git a/src/pdf/api/qpdflinkmodel_p.h b/src/pdf/api/qpdflinkmodel_p.h new file mode 100644 index 000000000..cf9c0aad4 --- /dev/null +++ b/src/pdf/api/qpdflinkmodel_p.h @@ -0,0 +1,106 @@ +/**************************************************************************** +** +** 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 QPDFLINKMODEL_P_H +#define QPDFLINKMODEL_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 "qtpdfglobal.h" +#include "qpdfdocument.h" + +#include +#include + +QT_BEGIN_NAMESPACE + +class QPdfLinkModelPrivate; + +class Q_PDF_EXPORT QPdfLinkModel : public QAbstractListModel +{ + Q_OBJECT + Q_PROPERTY(QPdfDocument *document READ document WRITE setDocument NOTIFY documentChanged) + Q_PROPERTY(int page READ page WRITE setPage NOTIFY pageChanged) + +public: + enum class Role : int { + Rect = Qt::UserRole, + Url, + Page, + Location, + Zoom, + _Count + }; + Q_ENUM(Role) + explicit QPdfLinkModel(QObject *parent = nullptr); + ~QPdfLinkModel(); + + QPdfDocument *document() const; + + QHash roleNames() const override; + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; + + int page() const; + +public Q_SLOTS: + void setDocument(QPdfDocument *document); + void setPage(int page); + +Q_SIGNALS: + void documentChanged(); + void pageChanged(int page); + +private Q_SLOTS: + void onStatusChanged(QPdfDocument::Status status); + +private: + QHash m_roleNames; + Q_DECLARE_PRIVATE(QPdfLinkModel) +}; + +QT_END_NAMESPACE + +#endif // QPDFLINKMODEL_P_H diff --git a/src/pdf/api/qpdflinkmodel_p_p.h b/src/pdf/api/qpdflinkmodel_p_p.h new file mode 100644 index 000000000..3e44f1651 --- /dev/null +++ b/src/pdf/api/qpdflinkmodel_p_p.h @@ -0,0 +1,91 @@ +/**************************************************************************** +** +** 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 QPDFLINKMODEL_P_P_H +#define QPDFLINKMODEL_P_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 "qpdflinkmodel_p.h" +#include + +#include "third_party/pdfium/public/fpdfview.h" + +#include + +QT_BEGIN_NAMESPACE + +class QPdfLinkModelPrivate: public QAbstractItemModelPrivate +{ + Q_DECLARE_PUBLIC(QPdfLinkModel) + +public: + QPdfLinkModelPrivate(); + + void update(); + + struct Link { + // where it is on the current page + QRectF rect; + int textStart = -1; + int textCharCount = 0; + // destination inside PDF + int page = -1; // -1 means look at the url instead + QPointF location; + qreal zoom = 1; + // web destination + QUrl url; + + QString toString() const; + }; + + QPdfDocument *document = nullptr; + QVector links; + int page = 0; +}; + +QT_END_NAMESPACE + +#endif // QPDFLINKMODEL_P_P_H diff --git a/src/pdf/pdfcore.pro b/src/pdf/pdfcore.pro index f50c3aa06..ecb1d0cdb 100644 --- a/src/pdf/pdfcore.pro +++ b/src/pdf/pdfcore.pro @@ -61,6 +61,7 @@ ios: OBJECTS += $$NINJA_OBJECTS SOURCES += \ qpdfbookmarkmodel.cpp \ qpdfdocument.cpp \ + qpdflinkmodel.cpp \ qpdfpagenavigation.cpp \ qpdfpagerenderer.cpp \ qpdfsearchmodel.cpp \ @@ -75,6 +76,8 @@ HEADERS += \ api/qpdfdocument_p.h \ api/qpdfdocumentrenderoptions.h \ api/qtpdfglobal.h \ + api/qpdflinkmodel_p.h \ + api/qpdflinkmodel_p_p.h \ api/qpdfnamespace.h \ api/qpdfpagenavigation.h \ api/qpdfpagerenderer.h \ diff --git a/src/pdf/qpdflinkmodel.cpp b/src/pdf/qpdflinkmodel.cpp new file mode 100644 index 000000000..8b49fec21 --- /dev/null +++ b/src/pdf/qpdflinkmodel.cpp @@ -0,0 +1,251 @@ +/**************************************************************************** +** +** 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 "qpdflinkmodel_p.h" +#include "qpdflinkmodel_p_p.h" +#include "qpdfdocument_p.h" + +#include "third_party/pdfium/public/fpdf_doc.h" +#include "third_party/pdfium/public/fpdf_text.h" + +#include +#include + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(qLcLink, "qt.pdf.links") + +QPdfLinkModel::QPdfLinkModel(QObject *parent) + : QAbstractListModel(*(new QPdfLinkModelPrivate()), parent) +{ + QMetaEnum rolesMetaEnum = metaObject()->enumerator(metaObject()->indexOfEnumerator("Role")); + for (int r = Qt::UserRole; r < int(Role::_Count); ++r) + m_roleNames.insert(r, QByteArray(rolesMetaEnum.valueToKey(r)).toLower()); +} + +QPdfLinkModel::~QPdfLinkModel() {} + +QHash QPdfLinkModel::roleNames() const +{ + return m_roleNames; +} + +int QPdfLinkModel::rowCount(const QModelIndex &parent) const +{ + Q_D(const QPdfLinkModel); + Q_UNUSED(parent) + return d->links.count(); +} + +QVariant QPdfLinkModel::data(const QModelIndex &index, int role) const +{ + Q_D(const QPdfLinkModel); + const QPdfLinkModelPrivate::Link &link = d->links.at(index.row()); + switch (Role(role)) { + case Role::Rect: + return link.rect; + case Role::Url: + return link.url; + case Role::Page: + return link.page; + case Role::Location: + return link.location; + case Role::Zoom: + return link.zoom; + case Role::_Count: + break; + } + if (role == Qt::DisplayRole) + return link.toString(); + return QVariant(); +} + +QPdfDocument *QPdfLinkModel::document() const +{ + Q_D(const QPdfLinkModel); + return d->document; +} + +void QPdfLinkModel::setDocument(QPdfDocument *document) +{ + Q_D(QPdfLinkModel); + if (d->document == document) + return; + disconnect(d->document, &QPdfDocument::statusChanged, this, &QPdfLinkModel::onStatusChanged); + connect(document, &QPdfDocument::statusChanged, this, &QPdfLinkModel::onStatusChanged); + d->document = document; + emit documentChanged(); + if (page()) + setPage(0); + else + d->update(); +} + +int QPdfLinkModel::page() const +{ + Q_D(const QPdfLinkModel); + return d->page; +} + +void QPdfLinkModel::setPage(int page) +{ + Q_D(QPdfLinkModel); + if (d->page == page) + return; + + d->page = page; + emit pageChanged(page); + d->update(); +} + +QPdfLinkModelPrivate::QPdfLinkModelPrivate() : QAbstractItemModelPrivate() +{ +} + +void QPdfLinkModelPrivate::update() +{ + Q_Q(QPdfLinkModel); + if (!document || !document->d->doc) + return; + auto doc = document->d->doc; + const QPdfMutexLocker lock; + FPDF_PAGE pdfPage = FPDF_LoadPage(doc, page); + if (!pdfPage) { + qWarning() << "failed to load page" << page; + return; + } + double pageHeight = FPDF_GetPageHeight(pdfPage); + q->beginResetModel(); + links.clear(); + + // Iterate the ordinary links + int linkStart = 0; + bool ok = true; + while (ok) { + FPDF_LINK linkAnnot; + ok = FPDFLink_Enumerate(pdfPage, &linkStart, &linkAnnot); + if (!ok) + break; + FS_RECTF rect; + ok = FPDFLink_GetAnnotRect(linkAnnot, &rect); + if (!ok) + break; + Link linkData; + linkData.rect = QRectF(rect.left, pageHeight - rect.top, + rect.right - rect.left, rect.top - rect.bottom); + FPDF_DEST dest = FPDFLink_GetDest(doc, linkAnnot); + FPDF_ACTION action = FPDFLink_GetAction(linkAnnot); + if (FPDFAction_GetType(action) != PDFACTION_GOTO) { + qWarning() << "link action type" << FPDFAction_GetType(action) << "is not yet supported"; + continue; + } + linkData.page = FPDFDest_GetDestPageIndex(doc, dest); + FPDF_BOOL hasX, hasY, hasZoom; + FS_FLOAT x, y, zoom; + ok = FPDFDest_GetLocationInPage(dest, &hasX, &hasY, &hasZoom, &x, &y, &zoom); + if (!ok) + break; + if (hasX && hasY) + linkData.location = QPointF(x, y); + if (hasZoom) + linkData.zoom = zoom; + links << linkData; + } + + // Iterate the web links + FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage); + if (textPage) { + FPDF_PAGELINK webLinks = FPDFLink_LoadWebLinks(textPage); + if (webLinks) { + int count = FPDFLink_CountWebLinks(webLinks); + for (int i = 0; i < count; ++i) { + Link linkData; + int len = FPDFLink_GetURL(webLinks, i, nullptr, 0); + if (len < 1) { + qWarning() << "URL" << i << "has length" << len; + } else { + QVector buf(len); + int got = FPDFLink_GetURL(webLinks, i, buf.data(), len); + Q_ASSERT(got == len); + linkData.url = QString::fromUtf16(buf.data(), got - 1); + } + FPDFLink_GetTextRange(webLinks, i, &linkData.textStart, &linkData.textCharCount); + len = FPDFLink_CountRects(webLinks, i); + for (int r = 0; r < len; ++r) { + double left, top, right, bottom; + bool success = FPDFLink_GetRect(webLinks, i, r, &left, &top, &right, &bottom); + if (success) { + linkData.rect = QRectF(left, pageHeight - top, right - left, top - bottom); + links << linkData; + } + } + } + FPDFLink_CloseWebLinks(webLinks); + } + FPDFText_ClosePage(textPage); + } + + // All done + FPDF_ClosePage(pdfPage); + if (Q_UNLIKELY(qLcLink().isDebugEnabled())) { + for (const Link &l : links) + qCDebug(qLcLink) << l.rect << l.toString(); + } + q->endResetModel(); +} + +void QPdfLinkModel::onStatusChanged(QPdfDocument::Status status) +{ + Q_D(QPdfLinkModel); + qCDebug(qLcLink) << "sees document statusChanged" << status; + if (status == QPdfDocument::Ready) + d->update(); +} + +QString QPdfLinkModelPrivate::Link::toString() const +{ + QString ret; + if (page >= 0) + return QLatin1String("page ") + QString::number(page) + + QLatin1String(" location ") + QString::number(location.x()) + QLatin1Char(',') + QString::number(location.y()) + + QLatin1String(" zoom ") + QString::number(zoom); + else + return url.toString(); +} + +QT_END_NAMESPACE + +#include "moc_qpdflinkmodel_p.cpp" 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 \ diff --git a/tests/manual/quick/pdf/withdoc.qml b/tests/manual/quick/pdf/withdoc.qml index fe3297ee8..2d82a6abf 100644 --- a/tests/manual/quick/pdf/withdoc.qml +++ b/tests/manual/quick/pdf/withdoc.qml @@ -171,6 +171,31 @@ Window { } } } + + Repeater { + model: PdfLinkModel { + id: linkModel + document: doc + 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 } // 5.15 onward (QTBUG-68073) + TapHandler { + onTapped: { + if (page >= 0) + image.currentFrame = page + else + Qt.openUrlExternally(url) + } + } + } + } } } } -- cgit v1.2.3 From c435f8efa2e30016c58f150a1ebe1ca560bfd2dc Mon Sep 17 00:00:00 2001 From: Leena Miettinen Date: Mon, 27 Jan 2020 13:43:04 +0100 Subject: Doc: Add information about building the module with CMake Task-number: QTBUG-81560 Change-Id: I9d75606634da4c604f34913908f97a91797d1813 Reviewed-by: Paul Wicking Reviewed-by: Leander Beernaert Reviewed-by: Shawn Rutledge --- src/pdf/doc/qtpdf.qdocconf | 3 ++- src/pdf/doc/snippets/qtpdf-build.cmake | 2 ++ src/pdf/doc/src/qtpdf-index.qdoc | 7 ++++++- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 src/pdf/doc/snippets/qtpdf-build.cmake diff --git a/src/pdf/doc/qtpdf.qdocconf b/src/pdf/doc/qtpdf.qdocconf index 7f7a0e80b..7a77105c9 100644 --- a/src/pdf/doc/qtpdf.qdocconf +++ b/src/pdf/doc/qtpdf.qdocconf @@ -39,7 +39,8 @@ depends += qtcore \ qtdoc \ qmake \ qtdesigner \ - qtquick + qtquick \ + qtcmake headerdirs += ../api \ ../quick diff --git a/src/pdf/doc/snippets/qtpdf-build.cmake b/src/pdf/doc/snippets/qtpdf-build.cmake new file mode 100644 index 000000000..d46b9c3ee --- /dev/null +++ b/src/pdf/doc/snippets/qtpdf-build.cmake @@ -0,0 +1,2 @@ +find_package(Qt5 COMPONENTS Pdf REQUIRED) +target_link_libraries(mytarget Qt5::Pdf) diff --git a/src/pdf/doc/src/qtpdf-index.qdoc b/src/pdf/doc/src/qtpdf-index.qdoc index d50d1482c..b32787eb5 100644 --- a/src/pdf/doc/src/qtpdf-index.qdoc +++ b/src/pdf/doc/src/qtpdf-index.qdoc @@ -39,7 +39,12 @@ \l QPdfPageNavigation class handles the navigation through a PDF document. - \section1 Getting Started + \include module-use.qdocinc using qt module + \quotefile qtpdf-build.cmake + + See also the \l{Build with CMake} overview. + + \section2 Building with qmake To include the definitions of the module's classes, use the following directive: -- cgit v1.2.3 From 12c151f9873f85d6f4d4fc54372a239478248e8f Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Thu, 30 Jan 2020 16:36:05 +0100 Subject: doc: add QPdfDocumentRenderOptions::scaledClipRect and scaledSize Amends 4f5f0705bc161ff95899fdb2c5fcdb4581bf15bb. Change-Id: I62563f5be367e8139dc4cdf12866c65f55a576b5 Reviewed-by: Shawn Rutledge --- src/pdf/qpdfdocumentrenderoptions.qdoc | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/pdf/qpdfdocumentrenderoptions.qdoc b/src/pdf/qpdfdocumentrenderoptions.qdoc index cafb0afc1..cc5083f9d 100644 --- a/src/pdf/qpdfdocumentrenderoptions.qdoc +++ b/src/pdf/qpdfdocumentrenderoptions.qdoc @@ -86,6 +86,40 @@ QT_BEGIN_NAMESPACE \sa renderFlags() */ +/*! + \fn QRect QPdfDocumentRenderOptions::scaledClipRect() const + + Returns the rectangular region to be clipped from the page after having + been scaled to \l scaledSize(). + + \sa setScaledClipRect() +*/ + +/*! + \fn void QPdfDocumentRenderOptions::setScaledClipRect(QRect rect) + + Sets the region \a rect to be clipped from the page after having been + scaled to \l scaledSize(). + + \sa scaledClipRect() +*/ + +/*! + \fn QRect QPdfDocumentRenderOptions::scaledSize() const + + Returns the \a size of the page to be rendered, in pixels. + + \sa setScaledSize() +*/ + +/*! + \fn void QPdfDocumentRenderOptions::setScaledSize(QSize size) + + Sets the \a size of the page to be rendered, in pixels. + + \sa scaledSize() +*/ + /*! \fn bool operator!=(QPdfDocumentRenderOptions lhs, QPdfDocumentRenderOptions rhs) \relates QPdfDocumentRenderOptions -- cgit v1.2.3 From 17ee6a55f3178324b123e301e4854c413b640738 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Mon, 3 Feb 2020 15:09:54 +0100 Subject: QPdfDocument::pageSize(): add checkPageComplete() checkPageComplete() calls FPDFAvail_IsPageAvail(), which does some initialization that is necessary before PDFium is able to provide any information about that page. The static readImage() function in qquickpixmapcache.cpp calls QImageReader::size() before rendering, to determine the size of the image that will be read, and that ends up calling QPdfDocument::pageSize(); so it's important to "go to" that page before attempting to read its size. In summary, checkPageComplete() is the "go to page" function that must be called before any other page-specific function. Amends 9231d3444555945297857ee4aae05919083ea479 Change-Id: Idf5ba9d013b62395c284d055fbfe835fe949df79 Reviewed-by: Shawn Rutledge --- src/pdf/qpdfdocument.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pdf/qpdfdocument.cpp b/src/pdf/qpdfdocument.cpp index 37eeb8426..69dc6e9ad 100644 --- a/src/pdf/qpdfdocument.cpp +++ b/src/pdf/qpdfdocument.cpp @@ -608,7 +608,7 @@ int QPdfDocument::pageCount() const QSizeF QPdfDocument::pageSize(int page) const { QSizeF result; - if (!d->doc) + if (!d->doc || !d->checkPageComplete(page)) return result; const QPdfMutexLocker lock; -- cgit v1.2.3 From b700f65011eaecefc60f6a4760547ecfb5542e34 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Mon, 3 Feb 2020 16:54:40 +0100 Subject: QPdfDocumentPrivate::load(): check the first two pages This seems to make QPdfLinkModel happy for some reason; otherwise QPdfLinkModelPrivate::update() does not succeed in getting page numbers when it calls FPDFDest_GetDestPageIndex(). Amends 9231d3444555945297857ee4aae05919083ea479 Change-Id: Iaed3301a1ab304ac9813c3b605b2f9c7465bf8e7 Reviewed-by: Shawn Rutledge --- src/pdf/qpdfdocument.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pdf/qpdfdocument.cpp b/src/pdf/qpdfdocument.cpp index 69dc6e9ad..3719938a2 100644 --- a/src/pdf/qpdfdocument.cpp +++ b/src/pdf/qpdfdocument.cpp @@ -211,8 +211,9 @@ void QPdfDocumentPrivate::load(QIODevice *newDevice, bool transferDeviceOwnershi emit q->pageCountChanged(pageCount); } - // If it's a local file, and the first page is available, probably the whole document is available. - if (checkPageComplete(0)) { + // If it's a local file, and the first couple of pages are available, + // probably the whole document is available. + if (checkPageComplete(0) && (pageCount < 2 || checkPageComplete(1))) { setStatus(QPdfDocument::Ready); } else { updateLastError(); -- 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 --- examples/pdf/pdfviewer/resources/zoom-fit-best.svg | 13 +++++ .../pdf/pdfviewer/resources/zoom-fit-width.svg | 13 +++++ examples/pdf/pdfviewer/viewer.qml | 14 +++++- examples/pdf/pdfviewer/viewer.qrc | 2 + src/pdf/quick/qml/PdfPageView.qml | 57 +++++++++++++++++++++- 5 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 examples/pdf/pdfviewer/resources/zoom-fit-best.svg create mode 100644 examples/pdf/pdfviewer/resources/zoom-fit-width.svg diff --git a/examples/pdf/pdfviewer/resources/zoom-fit-best.svg b/examples/pdf/pdfviewer/resources/zoom-fit-best.svg new file mode 100644 index 000000000..adf302621 --- /dev/null +++ b/examples/pdf/pdfviewer/resources/zoom-fit-best.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/examples/pdf/pdfviewer/resources/zoom-fit-width.svg b/examples/pdf/pdfviewer/resources/zoom-fit-width.svg new file mode 100644 index 000000000..985ee5205 --- /dev/null +++ b/examples/pdf/pdfviewer/resources/zoom-fit-width.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/examples/pdf/pdfviewer/viewer.qml b/examples/pdf/pdfviewer/viewer.qml index 99d9b8ed0..1cf0b432b 100644 --- a/examples/pdf/pdfviewer/viewer.qml +++ b/examples/pdf/pdfviewer/viewer.qml @@ -92,11 +92,23 @@ ApplicationWindow { onTriggered: pageView.renderScale /= root.scaleStep } } + ToolButton { + action: Action { + icon.source: "resources/zoom-fit-width.svg" + onTriggered: pageView.scaleToWidth(root.contentItem.width, root.contentItem.height) + } + } + ToolButton { + action: Action { + icon.source: "resources/zoom-fit-best.svg" + onTriggered: pageView.scaleToPage(root.contentItem.width, root.contentItem.height) + } + } ToolButton { action: Action { shortcut: "Ctrl+0" icon.source: "resources/zoom-original.svg" - onTriggered: pageView.renderScale = 1 + onTriggered: pageView.resetScale() } } ToolButton { diff --git a/examples/pdf/pdfviewer/viewer.qrc b/examples/pdf/pdfviewer/viewer.qrc index c376803c2..fa3561caf 100644 --- a/examples/pdf/pdfviewer/viewer.qrc +++ b/examples/pdf/pdfviewer/viewer.qrc @@ -8,6 +8,8 @@ resources/rotate-left.svg resources/rotate-right.svg resources/zoom-in.svg + resources/zoom-fit-best.svg + resources/zoom-fit-width.svg resources/zoom-original.svg resources/zoom-out.svg resources/document-open.svg 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 --- examples/pdf/pdfviewer/viewer.qml | 31 +++++- 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 + 6 files changed, 307 insertions(+), 12 deletions(-) create mode 100644 src/pdf/quick/qquickpdfnavigationstack.cpp create mode 100644 src/pdf/quick/qquickpdfnavigationstack_p.h diff --git a/examples/pdf/pdfviewer/viewer.qml b/examples/pdf/pdfviewer/viewer.qml index 1cf0b432b..b0cd8985d 100644 --- a/examples/pdf/pdfviewer/viewer.qml +++ b/examples/pdf/pdfviewer/viewer.qml @@ -57,8 +57,8 @@ import Qt.labs.platform 1.1 as Platform ApplicationWindow { id: root - width: 800 - height: 640 + width: 1280 + height: 1024 color: "lightgrey" title: document.title visible: true @@ -125,12 +125,22 @@ ApplicationWindow { onTriggered: pageView.rotation += 90 } } + ToolButton { + action: Action { + icon.source: "resources/go-previous-view-page.svg" + enabled: pageView.backEnabled + onTriggered: pageView.back() + } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "go back" + } SpinBox { id: currentPageSB from: 1 to: document.pageCount - value: 1 editable: true + onValueChanged: pageView.currentPage = value - 1 Shortcut { sequence: StandardKey.MoveToPreviousPage onActivated: currentPageSB.value-- @@ -140,6 +150,16 @@ ApplicationWindow { onActivated: currentPageSB.value++ } } + ToolButton { + action: Action { + icon.source: "resources/go-next-view-page.svg" + enabled: pageView.forwardEnabled + onTriggered: pageView.forward() + } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "go forward" + } ToolButton { action: Action { shortcut: StandardKey.Copy @@ -203,7 +223,10 @@ ApplicationWindow { PdfPageView { id: pageView - currentPage: currentPageSB.value - 1 +// currentPage: currentPageSB.value - 1 + // TODO should work but ends up being NaN in QQuickSpinBoxPrivate::setValue() (?!) +// onCurrentPageChanged: currentPageSB.value = pageView.currrentPage + 1 + onCurrentPageReallyChanged: currentPageSB.value = page + 1 document: PdfDocument { id: document onStatusChanged: if (status === PdfDocument.Error) errorDialog.open() 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 --- examples/pdf/multipage/main.cpp | 69 ++++++ examples/pdf/multipage/multipage.pro | 14 ++ examples/pdf/multipage/resources/document-open.svg | 13 + examples/pdf/multipage/resources/edit-clear.svg | 15 ++ examples/pdf/multipage/resources/edit-copy.svg | 15 ++ .../pdf/multipage/resources/go-next-view-page.svg | 13 + .../multipage/resources/go-previous-view-page.svg | 13 + examples/pdf/multipage/resources/rotate-left.svg | 6 + examples/pdf/multipage/resources/rotate-right.svg | 6 + examples/pdf/multipage/resources/zoom-fit-best.svg | 13 + .../pdf/multipage/resources/zoom-fit-width.svg | 13 + examples/pdf/multipage/resources/zoom-in.svg | 13 + examples/pdf/multipage/resources/zoom-original.svg | 13 + examples/pdf/multipage/resources/zoom-out.svg | 13 + examples/pdf/multipage/viewer.qml | 276 +++++++++++++++++++++ examples/pdf/multipage/viewer.qrc | 17 ++ 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 + 20 files changed, 705 insertions(+) create mode 100644 examples/pdf/multipage/main.cpp create mode 100644 examples/pdf/multipage/multipage.pro create mode 100644 examples/pdf/multipage/resources/document-open.svg create mode 100644 examples/pdf/multipage/resources/edit-clear.svg create mode 100644 examples/pdf/multipage/resources/edit-copy.svg create mode 100644 examples/pdf/multipage/resources/go-next-view-page.svg create mode 100644 examples/pdf/multipage/resources/go-previous-view-page.svg create mode 100644 examples/pdf/multipage/resources/rotate-left.svg create mode 100644 examples/pdf/multipage/resources/rotate-right.svg create mode 100644 examples/pdf/multipage/resources/zoom-fit-best.svg create mode 100644 examples/pdf/multipage/resources/zoom-fit-width.svg create mode 100644 examples/pdf/multipage/resources/zoom-in.svg create mode 100644 examples/pdf/multipage/resources/zoom-original.svg create mode 100644 examples/pdf/multipage/resources/zoom-out.svg create mode 100644 examples/pdf/multipage/viewer.qml create mode 100644 examples/pdf/multipage/viewer.qrc create mode 100644 src/pdf/quick/qml/PdfMultiPageView.qml diff --git a/examples/pdf/multipage/main.cpp b/examples/pdf/multipage/main.cpp new file mode 100644 index 000000000..451521f80 --- /dev/null +++ b/examples/pdf/multipage/main.cpp @@ -0,0 +1,69 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: http://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$ +** +****************************************************************************/ + +#include +#include + +int main(int argc, char* argv[]) +{ + QApplication app(argc, argv); + QCoreApplication::setApplicationName("Qt Quick Multi-page PDF Viewer Example"); + QCoreApplication::setOrganizationName("QtProject"); + QCoreApplication::setApplicationVersion(QT_VERSION_STR); + + QQmlApplicationEngine engine; + engine.load(QUrl(QStringLiteral("qrc:///pdfviewer/viewer.qml"))); + if (app.arguments().count() > 1) { + QUrl toLoad = QUrl::fromUserInput(app.arguments().at(1)); + engine.rootObjects().first()->setProperty("source", toLoad); + } + + return app.exec(); +} diff --git a/examples/pdf/multipage/multipage.pro b/examples/pdf/multipage/multipage.pro new file mode 100644 index 000000000..5fff58fe1 --- /dev/null +++ b/examples/pdf/multipage/multipage.pro @@ -0,0 +1,14 @@ +TEMPLATE = app + +QT += qml quick pdf widgets + +SOURCES += main.cpp + +RESOURCES += \ + viewer.qrc +EXAMPLE_FILES = \ + viewer.qml + +target.path = $$[QT_INSTALL_EXAMPLES]/pdf/multipage +INSTALLS += target + diff --git a/examples/pdf/multipage/resources/document-open.svg b/examples/pdf/multipage/resources/document-open.svg new file mode 100644 index 000000000..bf23123a3 --- /dev/null +++ b/examples/pdf/multipage/resources/document-open.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/examples/pdf/multipage/resources/edit-clear.svg b/examples/pdf/multipage/resources/edit-clear.svg new file mode 100644 index 000000000..1c35aaf04 --- /dev/null +++ b/examples/pdf/multipage/resources/edit-clear.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/examples/pdf/multipage/resources/edit-copy.svg b/examples/pdf/multipage/resources/edit-copy.svg new file mode 100644 index 000000000..9dd16877d --- /dev/null +++ b/examples/pdf/multipage/resources/edit-copy.svg @@ -0,0 +1,15 @@ + + + + + + diff --git a/examples/pdf/multipage/resources/go-next-view-page.svg b/examples/pdf/multipage/resources/go-next-view-page.svg new file mode 100644 index 000000000..e453ddbec --- /dev/null +++ b/examples/pdf/multipage/resources/go-next-view-page.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/examples/pdf/multipage/resources/go-previous-view-page.svg b/examples/pdf/multipage/resources/go-previous-view-page.svg new file mode 100644 index 000000000..b032309e9 --- /dev/null +++ b/examples/pdf/multipage/resources/go-previous-view-page.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/examples/pdf/multipage/resources/rotate-left.svg b/examples/pdf/multipage/resources/rotate-left.svg new file mode 100644 index 000000000..90ce53c9d --- /dev/null +++ b/examples/pdf/multipage/resources/rotate-left.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/examples/pdf/multipage/resources/rotate-right.svg b/examples/pdf/multipage/resources/rotate-right.svg new file mode 100644 index 000000000..7383d1c84 --- /dev/null +++ b/examples/pdf/multipage/resources/rotate-right.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/examples/pdf/multipage/resources/zoom-fit-best.svg b/examples/pdf/multipage/resources/zoom-fit-best.svg new file mode 100644 index 000000000..adf302621 --- /dev/null +++ b/examples/pdf/multipage/resources/zoom-fit-best.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/examples/pdf/multipage/resources/zoom-fit-width.svg b/examples/pdf/multipage/resources/zoom-fit-width.svg new file mode 100644 index 000000000..985ee5205 --- /dev/null +++ b/examples/pdf/multipage/resources/zoom-fit-width.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/examples/pdf/multipage/resources/zoom-in.svg b/examples/pdf/multipage/resources/zoom-in.svg new file mode 100644 index 000000000..efdc9f17d --- /dev/null +++ b/examples/pdf/multipage/resources/zoom-in.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/examples/pdf/multipage/resources/zoom-original.svg b/examples/pdf/multipage/resources/zoom-original.svg new file mode 100644 index 000000000..1b4080a03 --- /dev/null +++ b/examples/pdf/multipage/resources/zoom-original.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/examples/pdf/multipage/resources/zoom-out.svg b/examples/pdf/multipage/resources/zoom-out.svg new file mode 100644 index 000000000..fcde9e526 --- /dev/null +++ b/examples/pdf/multipage/resources/zoom-out.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/examples/pdf/multipage/viewer.qml b/examples/pdf/multipage/viewer.qml new file mode 100644 index 000000000..d20ad4a5b --- /dev/null +++ b/examples/pdf/multipage/viewer.qml @@ -0,0 +1,276 @@ +/**************************************************************************** +** +** 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.14 +import QtQuick.Controls 2.14 +import QtQuick.Layouts 1.14 +import QtQuick.Pdf 5.15 +import QtQuick.Shapes 1.14 +import QtQuick.Window 2.14 +import Qt.labs.platform 1.1 as Platform + +ApplicationWindow { + id: root + width: 800 + height: 1024 + color: "lightgrey" + title: document.title + visible: true + property alias source: document.source // for main.cpp + + header: ToolBar { + RowLayout { + anchors.fill: parent + anchors.rightMargin: 6 + ToolButton { + action: Action { + shortcut: StandardKey.Open + icon.source: "resources/document-open.svg" + onTriggered: fileDialog.open() + } + } + /* TODO zoom & rotation + ToolButton { + action: Action { + shortcut: StandardKey.ZoomIn + enabled: view.sourceSize.width < 10000 + icon.source: "resources/zoom-in.svg" + onTriggered: view.renderScale *= Math.sqrt(2) + } + } + ToolButton { + action: Action { + shortcut: StandardKey.ZoomOut + enabled: view.sourceSize.width > 50 + icon.source: "resources/zoom-out.svg" + onTriggered: view.renderScale /= Math.sqrt(2) + } + } + ToolButton { + action: Action { + icon.source: "resources/zoom-fit-width.svg" + onTriggered: view.scaleToWidth(root.contentItem.width, root.contentItem.height) + } + } + ToolButton { + action: Action { + icon.source: "resources/zoom-fit-best.svg" + onTriggered: view.scaleToPage(root.contentItem.width, root.contentItem.height) + } + } + ToolButton { + action: Action { + shortcut: "Ctrl+0" + icon.source: "resources/zoom-original.svg" + onTriggered: view.resetScale() + } + } + ToolButton { + action: Action { + shortcut: "Ctrl+L" + icon.source: "resources/rotate-left.svg" + onTriggered: view.rotation -= 90 + } + } + ToolButton { + action: Action { + shortcut: "Ctrl+R" + icon.source: "resources/rotate-right.svg" + onTriggered: view.rotation += 90 + } + } + */ + ToolButton { + action: Action { + icon.source: "resources/go-previous-view-page.svg" + enabled: view.backEnbled + onTriggered: view.back() + } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "go back" + } + SpinBox { + id: currentPageSB + from: 1 + to: document.pageCount + editable: true + onValueChanged: view.currentPage = value - 1 + Shortcut { + sequence: StandardKey.MoveToPreviousPage + onActivated: currentPageSB.value-- + } + Shortcut { + sequence: StandardKey.MoveToNextPage + onActivated: currentPageSB.value++ + } + } + ToolButton { + action: Action { + icon.source: "resources/go-next-view-page.svg" + enabled: view.forwardEnabled + onTriggered: view.forward() + } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "go forward" + } + ToolButton { + action: Action { + shortcut: StandardKey.Copy + icon.source: "resources/edit-copy.svg" + enabled: view.selectedText !== "" + onTriggered: view.copySelectionToClipboard() + } + } + TextField { + id: searchField + placeholderText: "search" + Layout.minimumWidth: 200 + Layout.fillWidth: true + Image { + visible: searchField.text !== "" + source: "resources/edit-clear.svg" + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + margins: 3 + rightMargin: 5 + } + TapHandler { + onTapped: searchField.clear() + } + } + } + Shortcut { + sequence: StandardKey.Find + onActivated: searchField.forceActiveFocus() + } + Shortcut { + sequence: StandardKey.Quit + onActivated: Qt.quit() + } + } + } + + Platform.FileDialog { + id: fileDialog + title: "Open a PDF file" + nameFilters: [ "PDF files (*.pdf)" ] + onAccepted: document.source = file + } + + Dialog { + id: passwordDialog + title: "Password" + standardButtons: Dialog.Ok | Dialog.Cancel + modal: true + closePolicy: Popup.CloseOnEscape + anchors.centerIn: parent + width: 300 + + TextField { + id: passwordField + placeholderText: qsTr("Please provide the password") + echoMode: TextInput.Password + width: parent.width + onAccepted: passwordDialog.accept() + } + onAccepted: document.password = passwordField.text + } + + Dialog { + id: errorDialog + title: "Error loading " + document.source + standardButtons: Dialog.Ok + modal: true + closePolicy: Popup.CloseOnEscape + anchors.centerIn: parent + width: 300 + + Label { + id: errorField + text: document.error + } + } + + PdfDocument { + id: document + onStatusChanged: { + if (status === PdfDocument.Error) errorDialog.open() + view.document = (status === PdfDocument.Ready ? document : undefined) + } + onPasswordRequired: { + passwordDialog.open() + passwordField.forceActiveFocus() + } + } + + PdfMultiPageView { + id: view + anchors.fill: parent + document: root.document + searchString: searchField.text + onCurrentPageReallyChanged: currentPageSB.value = page + 1 + } + + footer: ToolBar { + height: statusLabel.implicitHeight * 1.5 + Label { + id: statusLabel + anchors.verticalCenter: parent.verticalCenter + x: 6 + property size implicitPointSize: document.pagePointSize(view.currentPage) + text: "page " + (currentPageSB.value) + " of " + document.pageCount + + " original " + implicitPointSize.width.toFixed(1) + "x" + implicitPointSize.height.toFixed(1) + visible: document.pageCount > 0 + } + } +} diff --git a/examples/pdf/multipage/viewer.qrc b/examples/pdf/multipage/viewer.qrc new file mode 100644 index 000000000..fa3561caf --- /dev/null +++ b/examples/pdf/multipage/viewer.qrc @@ -0,0 +1,17 @@ + + + viewer.qml + resources/edit-clear.svg + resources/edit-copy.svg + resources/go-next-view-page.svg + resources/go-previous-view-page.svg + resources/rotate-left.svg + resources/rotate-right.svg + resources/zoom-in.svg + resources/zoom-fit-best.svg + resources/zoom-fit-width.svg + resources/zoom-original.svg + resources/zoom-out.svg + resources/document-open.svg + + 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(-) 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 130b058352077b88f8839871f88c226e3e1fa705 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Tue, 4 Feb 2020 16:08:42 +0100 Subject: Add multipage example to pdf.pro; work with 5.14; fix main.cpp - build the multipage example by default - don't import anything that won't work with Qt 5.14 - set application attributes before creating an instance (to fix the warning about that) Change-Id: I265f49ca75cae1908d4c23848cba8c42e5e3824b Reviewed-by: Shawn Rutledge --- examples/pdf/multipage/main.cpp | 3 ++- examples/pdf/pdf.pro | 2 +- examples/pdf/pdfviewer/main.cpp | 2 +- examples/pdf/pdfviewer/viewer.qml | 10 +++++----- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/examples/pdf/multipage/main.cpp b/examples/pdf/multipage/main.cpp index 451521f80..7b766d77e 100644 --- a/examples/pdf/multipage/main.cpp +++ b/examples/pdf/multipage/main.cpp @@ -53,10 +53,11 @@ int main(int argc, char* argv[]) { - QApplication app(argc, argv); QCoreApplication::setApplicationName("Qt Quick Multi-page PDF Viewer Example"); QCoreApplication::setOrganizationName("QtProject"); QCoreApplication::setApplicationVersion(QT_VERSION_STR); + QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QApplication app(argc, argv); QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:///pdfviewer/viewer.qml"))); diff --git a/examples/pdf/pdf.pro b/examples/pdf/pdf.pro index 45df33e46..7130f3560 100644 --- a/examples/pdf/pdf.pro +++ b/examples/pdf/pdf.pro @@ -1,3 +1,3 @@ TEMPLATE=subdirs -SUBDIRS += pdfviewer +SUBDIRS += pdfviewer multipage diff --git a/examples/pdf/pdfviewer/main.cpp b/examples/pdf/pdfviewer/main.cpp index 6b94a3de1..639b11825 100644 --- a/examples/pdf/pdfviewer/main.cpp +++ b/examples/pdf/pdfviewer/main.cpp @@ -53,11 +53,11 @@ int main(int argc, char* argv[]) { - QApplication app(argc, argv); QCoreApplication::setApplicationName("Qt Quick PDF Viewer Example"); QCoreApplication::setOrganizationName("QtProject"); QCoreApplication::setApplicationVersion(QT_VERSION_STR); QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); + QApplication app(argc, argv); QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:///pdfviewer/viewer.qml"))); diff --git a/examples/pdf/pdfviewer/viewer.qml b/examples/pdf/pdfviewer/viewer.qml index b0cd8985d..a8e581a45 100644 --- a/examples/pdf/pdfviewer/viewer.qml +++ b/examples/pdf/pdfviewer/viewer.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 import Qt.labs.platform 1.1 as Platform ApplicationWindow { -- 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 --- examples/pdf/multipage/viewer.qml | 13 +++-- src/pdf/quick/qml/PdfMultiPageView.qml | 95 +++++++++++++++++++++++++++++----- 2 files changed, 88 insertions(+), 20 deletions(-) diff --git a/examples/pdf/multipage/viewer.qml b/examples/pdf/multipage/viewer.qml index d20ad4a5b..77c06f80f 100644 --- a/examples/pdf/multipage/viewer.qml +++ b/examples/pdf/multipage/viewer.qml @@ -75,11 +75,10 @@ ApplicationWindow { onTriggered: fileDialog.open() } } - /* TODO zoom & rotation ToolButton { action: Action { shortcut: StandardKey.ZoomIn - enabled: view.sourceSize.width < 10000 + enabled: view.renderScale < 10 icon.source: "resources/zoom-in.svg" onTriggered: view.renderScale *= Math.sqrt(2) } @@ -87,7 +86,7 @@ ApplicationWindow { ToolButton { action: Action { shortcut: StandardKey.ZoomOut - enabled: view.sourceSize.width > 50 + enabled: view.renderScale > 0.1 icon.source: "resources/zoom-out.svg" onTriggered: view.renderScale /= Math.sqrt(2) } @@ -115,17 +114,16 @@ ApplicationWindow { action: Action { shortcut: "Ctrl+L" icon.source: "resources/rotate-left.svg" - onTriggered: view.rotation -= 90 + onTriggered: view.pageRotation -= 90 } } ToolButton { action: Action { shortcut: "Ctrl+R" icon.source: "resources/rotate-right.svg" - onTriggered: view.rotation += 90 + onTriggered: view.pageRotation += 90 } } - */ ToolButton { action: Action { icon.source: "resources/go-previous-view-page.svg" @@ -269,7 +267,8 @@ ApplicationWindow { x: 6 property size implicitPointSize: document.pagePointSize(view.currentPage) text: "page " + (currentPageSB.value) + " of " + document.pageCount + - " original " + implicitPointSize.width.toFixed(1) + "x" + implicitPointSize.height.toFixed(1) + " scale " + view.renderScale.toFixed(2) + + " original size " + implicitPointSize.width.toFixed(1) + "x" + implicitPointSize.height.toFixed(1) + " pt" visible: document.pageCount > 0 } } 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 --- examples/pdf/pdfviewer/viewer.qml | 3 +- src/pdf/api/qpdfdestination.h | 81 +++++++++++++ src/pdf/api/qpdfdestination_p.h | 60 +++++++++ src/pdf/pdfcore.pro | 3 + src/pdf/qpdfdestination.cpp | 135 +++++++++++++++++++++ src/pdf/qpdfdocument.cpp | 3 + 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 ++-- 10 files changed, 447 insertions(+), 54 deletions(-) create mode 100644 src/pdf/api/qpdfdestination.h create mode 100644 src/pdf/api/qpdfdestination_p.h create mode 100644 src/pdf/qpdfdestination.cpp diff --git a/examples/pdf/pdfviewer/viewer.qml b/examples/pdf/pdfviewer/viewer.qml index a8e581a45..095bc3985 100644 --- a/examples/pdf/pdfviewer/viewer.qml +++ b/examples/pdf/pdfviewer/viewer.qml @@ -140,7 +140,7 @@ ApplicationWindow { from: 1 to: document.pageCount editable: true - onValueChanged: pageView.currentPage = value - 1 + onValueChanged: pageView.goToPage(value - 1) Shortcut { sequence: StandardKey.MoveToPreviousPage onActivated: currentPageSB.value-- @@ -223,7 +223,6 @@ ApplicationWindow { PdfPageView { id: pageView -// currentPage: currentPageSB.value - 1 // TODO should work but ends up being NaN in QQuickSpinBoxPrivate::setValue() (?!) // onCurrentPageChanged: currentPageSB.value = pageView.currrentPage + 1 onCurrentPageReallyChanged: currentPageSB.value = page + 1 diff --git a/src/pdf/api/qpdfdestination.h b/src/pdf/api/qpdfdestination.h new file mode 100644 index 000000000..dc5d6314a --- /dev/null +++ b/src/pdf/api/qpdfdestination.h @@ -0,0 +1,81 @@ +/**************************************************************************** +** +** 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 QPDFDESTINATION_H +#define QPDFDESTINATION_H + +#include +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QPdfDestinationPrivate; + +class Q_PDF_EXPORT QPdfDestination +{ + Q_GADGET + Q_PROPERTY(bool valid READ isValid) + Q_PROPERTY(int page READ page) + Q_PROPERTY(QPointF location READ location) + Q_PROPERTY(qreal zoom READ zoom) + +public: + QPdfDestination(const QPdfDestination &other); + ~QPdfDestination(); + QPdfDestination &operator=(const QPdfDestination &other); + inline QPdfDestination &operator=(QPdfDestination &&other) noexcept { swap(other); return *this; } + void swap(QPdfDestination &other) noexcept { d.swap(other.d); } + bool isValid() const; + int page() const; + QPointF location() const; + qreal zoom() const; + +private: + QPdfDestination(); + QPdfDestination(int page, QPointF location, qreal zoom); + QPdfDestination(QPdfDestinationPrivate *d); + friend class QPdfDocument; + friend class QQuickPdfNavigationStack; + +private: + QExplicitlySharedDataPointer d; +}; + +QT_END_NAMESPACE + +#endif // QPDFDESTINATION_H diff --git a/src/pdf/api/qpdfdestination_p.h b/src/pdf/api/qpdfdestination_p.h new file mode 100644 index 000000000..a5aeb804f --- /dev/null +++ b/src/pdf/api/qpdfdestination_p.h @@ -0,0 +1,60 @@ +/**************************************************************************** +** +** 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 QPDFDESTINATION_P_H +#define QPDFDESTINATION_P_H + +#include + +QT_BEGIN_NAMESPACE + +class QPdfDestinationPrivate : public QSharedData +{ +public: + QPdfDestinationPrivate() = default; + QPdfDestinationPrivate(int page, QPointF location, qreal zoom) + : page(page), + location(location), + zoom(zoom) { } + + int page = -1; + QPointF location; + qreal zoom = 1; +}; + +QT_END_NAMESPACE + +#endif // QPDFDESTINATION_P_H diff --git a/src/pdf/pdfcore.pro b/src/pdf/pdfcore.pro index ecb1d0cdb..951b5699f 100644 --- a/src/pdf/pdfcore.pro +++ b/src/pdf/pdfcore.pro @@ -60,6 +60,7 @@ ios: OBJECTS += $$NINJA_OBJECTS SOURCES += \ qpdfbookmarkmodel.cpp \ + qpdfdestination.cpp \ qpdfdocument.cpp \ qpdflinkmodel.cpp \ qpdfpagenavigation.cpp \ @@ -72,6 +73,8 @@ SOURCES += \ HEADERS += \ api/qpdfbookmarkmodel.h \ + api/qpdfdestination.h \ + api/qpdfdestination_p.h \ api/qpdfdocument.h \ api/qpdfdocument_p.h \ api/qpdfdocumentrenderoptions.h \ diff --git a/src/pdf/qpdfdestination.cpp b/src/pdf/qpdfdestination.cpp new file mode 100644 index 000000000..86e429dcf --- /dev/null +++ b/src/pdf/qpdfdestination.cpp @@ -0,0 +1,135 @@ +/**************************************************************************** +** +** 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 "qpdfdestination.h" +#include "qpdfdestination_p.h" + +QT_BEGIN_NAMESPACE + +/*! + \class QPdfDestination + \since 5.15 + \inmodule QtPdf + + \brief The QPdfDestination class defines a location on a page in a PDF + document, and a suggested zoom level at which it is intended to be viewed. +*/ + +/*! + Constructs an invalid Destination. + + \sa valid +*/ +QPdfDestination::QPdfDestination() + : d(new QPdfDestinationPrivate()) +{ +} + +QPdfDestination::QPdfDestination(int page, QPointF location, qreal zoom) + : d(new QPdfDestinationPrivate(page, location, zoom)) +{ +} + +QPdfDestination::QPdfDestination(QPdfDestinationPrivate *d) + : d(d) +{ +} + +QPdfDestination::QPdfDestination(const QPdfDestination &other) + : d(other.d) +{ +} + +QPdfDestination::~QPdfDestination() +{ +} + +/*! + \property QPdfDestination::valid + + This property holds whether the destination is valid. +*/ +bool QPdfDestination::isValid() const +{ + return d->page >= 0; +} + +/*! + \property QPdfDestination::page + + This property holds the page number. +*/ +int QPdfDestination::page() const +{ + return d->page; +} + +/*! + \property QPdfDestination::location + + This property holds the location on the page, in units of points. +*/ +QPointF QPdfDestination::location() const +{ + return d->location; +} + +/*! + \property QPdfDestination::zoom + + This property holds the suggested magnification level, where 1.0 means default scale + (1 pixel = 1 point). +*/ +qreal QPdfDestination::zoom() const +{ + return d->zoom; +} + +//QDataStream& operator<<(QDataStream& stream, const QPdfDestination& dest) +//{ +// stream << *dest.d.data(); +// return stream; +//} + +QDataStream& operator<<(QDataStream& stream, const QPdfDestinationPrivate& dest) +{ + stream << QStringLiteral("QPdfDestination") << dest.page << dest.location ; // << dest.zoom(); + return stream; +} + +QT_END_NAMESPACE + +#include "moc_qpdfdestination.cpp" diff --git a/src/pdf/qpdfdocument.cpp b/src/pdf/qpdfdocument.cpp index 3719938a2..42cd2492d 100644 --- a/src/pdf/qpdfdocument.cpp +++ b/src/pdf/qpdfdocument.cpp @@ -345,6 +345,9 @@ void QPdfDocumentPrivate::checkComplete() bool QPdfDocumentPrivate::checkPageComplete(int page) { + if (page < 0 || page >= pageCount) + return false; + if (loadComplete) return true; 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(+) 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 --- examples/pdf/multipage/viewer.qml | 4 +-- src/pdf/quick/qml/PdfMultiPageView.qml | 50 +++++++++++++++++++++++------- src/pdf/quick/qquickpdfnavigationstack.cpp | 1 + 3 files changed, 41 insertions(+), 14 deletions(-) diff --git a/examples/pdf/multipage/viewer.qml b/examples/pdf/multipage/viewer.qml index 77c06f80f..bbc28cd8d 100644 --- a/examples/pdf/multipage/viewer.qml +++ b/examples/pdf/multipage/viewer.qml @@ -139,7 +139,7 @@ ApplicationWindow { from: 1 to: document.pageCount editable: true - onValueChanged: view.currentPage = value - 1 + onValueModified: view.goToPage(value - 1) Shortcut { sequence: StandardKey.MoveToPreviousPage onActivated: currentPageSB.value-- @@ -256,7 +256,7 @@ ApplicationWindow { anchors.fill: parent document: root.document searchString: searchField.text - onCurrentPageReallyChanged: currentPageSB.value = page + 1 + onCurrentPageChanged: currentPageSB.value = view.currentPage + 1 } footer: ToolBar { 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 --- .../pdf/multipage/resources/go-down-search.svg | 13 +++ examples/pdf/multipage/resources/go-up-search.svg | 8 ++ examples/pdf/multipage/viewer.qml | 81 ++++++++++++----- examples/pdf/multipage/viewer.qrc | 2 + src/pdf/quick/qml/PdfMultiPageView.qml | 100 ++++++++++++++------- 5 files changed, 148 insertions(+), 56 deletions(-) create mode 100644 examples/pdf/multipage/resources/go-down-search.svg create mode 100644 examples/pdf/multipage/resources/go-up-search.svg diff --git a/examples/pdf/multipage/resources/go-down-search.svg b/examples/pdf/multipage/resources/go-down-search.svg new file mode 100644 index 000000000..ae17ab93b --- /dev/null +++ b/examples/pdf/multipage/resources/go-down-search.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/examples/pdf/multipage/resources/go-up-search.svg b/examples/pdf/multipage/resources/go-up-search.svg new file mode 100644 index 000000000..5cc155873 --- /dev/null +++ b/examples/pdf/multipage/resources/go-up-search.svg @@ -0,0 +1,8 @@ + + + + diff --git a/examples/pdf/multipage/viewer.qml b/examples/pdf/multipage/viewer.qml index bbc28cd8d..3d9cd371a 100644 --- a/examples/pdf/multipage/viewer.qml +++ b/examples/pdf/multipage/viewer.qml @@ -167,30 +167,6 @@ ApplicationWindow { onTriggered: view.copySelectionToClipboard() } } - TextField { - id: searchField - placeholderText: "search" - Layout.minimumWidth: 200 - Layout.fillWidth: true - Image { - visible: searchField.text !== "" - source: "resources/edit-clear.svg" - anchors { - right: parent.right - top: parent.top - bottom: parent.bottom - margins: 3 - rightMargin: 5 - } - TapHandler { - onTapped: searchField.clear() - } - } - } - Shortcut { - sequence: StandardKey.Find - onActivated: searchField.forceActiveFocus() - } Shortcut { sequence: StandardKey.Quit onActivated: Qt.quit() @@ -259,6 +235,63 @@ ApplicationWindow { onCurrentPageChanged: currentPageSB.value = view.currentPage + 1 } + Drawer { + id: searchDrawer + edge: Qt.BottomEdge + x: 20 + width: searchLayout.implicitWidth + height: searchLayout.implicitHeight + dim: false + Shortcut { + sequence: StandardKey.Find + onActivated: { + searchDrawer.open() + searchField.forceActiveFocus() + } + } + RowLayout { + id: searchLayout + ToolButton { + action: Action { + icon.source: "resources/go-up-search.svg" + onTriggered: view.searchBack() + } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "find previous" + } + TextField { + id: searchField + placeholderText: "search" + Layout.minimumWidth: 200 + Layout.fillWidth: true + Image { + visible: searchField.text !== "" + source: "resources/edit-clear.svg" + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + margins: 3 + rightMargin: 5 + } + TapHandler { + onTapped: searchField.clear() + } + } + } + ToolButton { + action: Action { + icon.source: "resources/go-down-search.svg" + onTriggered: view.searchForward() + } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "find next" + } + } + } + footer: ToolBar { height: statusLabel.implicitHeight * 1.5 Label { diff --git a/examples/pdf/multipage/viewer.qrc b/examples/pdf/multipage/viewer.qrc index fa3561caf..9698a2689 100644 --- a/examples/pdf/multipage/viewer.qrc +++ b/examples/pdf/multipage/viewer.qrc @@ -3,8 +3,10 @@ viewer.qml resources/edit-clear.svg resources/edit-copy.svg + resources/go-down-search.svg resources/go-next-view-page.svg resources/go-previous-view-page.svg + resources/go-up-search.svg resources/rotate-left.svg resources/rotate-right.svg resources/zoom-in.svg 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 --- examples/pdf/multipage/viewer.qml | 96 +++++---- .../pdf/pdfviewer/resources/go-down-search.svg | 13 ++ examples/pdf/pdfviewer/resources/go-up-search.svg | 8 + examples/pdf/pdfviewer/viewer.qml | 119 ++++++++--- examples/pdf/pdfviewer/viewer.qrc | 2 + src/pdf/api/qpdfdestination.h | 4 +- src/pdf/api/qpdfdestination_p.h | 11 + src/pdf/api/qpdfdocument.h | 1 + src/pdf/api/qpdfsearchmodel.h | 30 ++- src/pdf/api/qpdfsearchmodel_p.h | 84 ++++++++ src/pdf/api/qpdfsearchresult.h | 75 +++++++ src/pdf/api/qpdfsearchresult_p.h | 70 +++++++ src/pdf/pdfcore.pro | 5 +- src/pdf/qpdfsearchmodel.cpp | 229 ++++++++++++++++++--- src/pdf/qpdfsearchmodel_p.h | 56 ----- src/pdf/qpdfsearchresult.cpp | 74 +++++++ 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 ++-- 20 files changed, 956 insertions(+), 276 deletions(-) create mode 100644 examples/pdf/pdfviewer/resources/go-down-search.svg create mode 100644 examples/pdf/pdfviewer/resources/go-up-search.svg create mode 100644 src/pdf/api/qpdfsearchmodel_p.h create mode 100644 src/pdf/api/qpdfsearchresult.h create mode 100644 src/pdf/api/qpdfsearchresult_p.h delete mode 100644 src/pdf/qpdfsearchmodel_p.h create mode 100644 src/pdf/qpdfsearchresult.cpp diff --git a/examples/pdf/multipage/viewer.qml b/examples/pdf/multipage/viewer.qml index 3d9cd371a..8f102a3c1 100644 --- a/examples/pdf/multipage/viewer.qml +++ b/examples/pdf/multipage/viewer.qml @@ -230,6 +230,7 @@ ApplicationWindow { PdfMultiPageView { id: view anchors.fill: parent + anchors.leftMargin: searchDrawer.position * searchDrawer.width document: root.document searchString: searchField.text onCurrentPageChanged: currentPageSB.value = view.currentPage + 1 @@ -237,10 +238,11 @@ ApplicationWindow { Drawer { id: searchDrawer - edge: Qt.BottomEdge - x: 20 + edge: Qt.LeftEdge + modal: false width: searchLayout.implicitWidth - height: searchLayout.implicitHeight + y: root.header.height + height: view.height dim: false Shortcut { sequence: StandardKey.Find @@ -249,45 +251,69 @@ ApplicationWindow { searchField.forceActiveFocus() } } - RowLayout { + ColumnLayout { id: searchLayout - ToolButton { - action: Action { - icon.source: "resources/go-up-search.svg" - onTriggered: view.searchBack() + anchors.fill: parent + anchors.margins: 2 + RowLayout { + ToolButton { + action: Action { + icon.source: "resources/go-up-search.svg" + shortcut: StandardKey.FindPrevious + onTriggered: view.searchBack() + } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "find previous" } - ToolTip.visible: enabled && hovered - ToolTip.delay: 2000 - ToolTip.text: "find previous" - } - TextField { - id: searchField - placeholderText: "search" - Layout.minimumWidth: 200 - Layout.fillWidth: true - Image { - visible: searchField.text !== "" - source: "resources/edit-clear.svg" - anchors { - right: parent.right - top: parent.top - bottom: parent.bottom - margins: 3 - rightMargin: 5 + TextField { + id: searchField + placeholderText: "search" + Layout.minimumWidth: 200 + Layout.fillWidth: true + Image { + visible: searchField.text !== "" + source: "resources/edit-clear.svg" + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + margins: 3 + rightMargin: 5 + } + TapHandler { + onTapped: searchField.clear() + } } - TapHandler { - onTapped: searchField.clear() + } + ToolButton { + action: Action { + icon.source: "resources/go-down-search.svg" + shortcut: StandardKey.FindNext + onTriggered: view.searchForward() } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "find next" } } - ToolButton { - action: Action { - icon.source: "resources/go-down-search.svg" - onTriggered: view.searchForward() + ListView { + id: searchResultsList + ColumnLayout.fillWidth: true + ColumnLayout.fillHeight: true + clip: true + model: view.searchModel + ScrollBar.vertical: ScrollBar { } + delegate: ItemDelegate { + width: parent ? parent.width : 0 + text: "page " + (page + 1) + ": " + context + highlighted: ListView.isCurrentItem + onClicked: { + searchResultsList.currentIndex = index + view.goToLocation(page, location, 0) + view.searchModel.currentResult = indexOnPage + } } - ToolTip.visible: enabled && hovered - ToolTip.delay: 2000 - ToolTip.text: "find next" } } } diff --git a/examples/pdf/pdfviewer/resources/go-down-search.svg b/examples/pdf/pdfviewer/resources/go-down-search.svg new file mode 100644 index 000000000..ae17ab93b --- /dev/null +++ b/examples/pdf/pdfviewer/resources/go-down-search.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/examples/pdf/pdfviewer/resources/go-up-search.svg b/examples/pdf/pdfviewer/resources/go-up-search.svg new file mode 100644 index 000000000..5cc155873 --- /dev/null +++ b/examples/pdf/pdfviewer/resources/go-up-search.svg @@ -0,0 +1,8 @@ + + + + diff --git a/examples/pdf/pdfviewer/viewer.qml b/examples/pdf/pdfviewer/viewer.qml index 095bc3985..e94642c5e 100644 --- a/examples/pdf/pdfviewer/viewer.qml +++ b/examples/pdf/pdfviewer/viewer.qml @@ -57,7 +57,7 @@ import Qt.labs.platform 1.1 as Platform ApplicationWindow { id: root - width: 1280 + width: 800 height: 1024 color: "lightgrey" title: document.title @@ -140,14 +140,15 @@ ApplicationWindow { from: 1 to: document.pageCount editable: true - onValueChanged: pageView.goToPage(value - 1) + value: pageView.currentPage + 1 + onValueModified: pageView.goToPage(value - 1) Shortcut { sequence: StandardKey.MoveToPreviousPage - onActivated: currentPageSB.value-- + onActivated: pageView.goToPage(currentPageSB.value - 2) } Shortcut { sequence: StandardKey.MoveToNextPage - onActivated: currentPageSB.value++ + onActivated: pageView.goToPage(currentPageSB.value) } } ToolButton { @@ -168,30 +169,6 @@ ApplicationWindow { onTriggered: pageView.copySelectionToClipboard() } } - TextField { - id: searchField - placeholderText: "search" - Layout.minimumWidth: 200 - Layout.fillWidth: true - Image { - visible: searchField.text !== "" - source: "resources/edit-clear.svg" - anchors { - right: parent.right - top: parent.top - bottom: parent.bottom - margins: 3 - rightMargin: 5 - } - TapHandler { - onTapped: searchField.clear() - } - } - } - Shortcut { - sequence: StandardKey.Find - onActivated: searchField.forceActiveFocus() - } Shortcut { sequence: StandardKey.Quit onActivated: Qt.quit() @@ -223,9 +200,7 @@ ApplicationWindow { PdfPageView { id: pageView - // TODO should work but ends up being NaN in QQuickSpinBoxPrivate::setValue() (?!) -// onCurrentPageChanged: currentPageSB.value = pageView.currrentPage + 1 - onCurrentPageReallyChanged: currentPageSB.value = page + 1 + x: searchDrawer.position * searchDrawer.width // TODO binding gets broken during centering document: PdfDocument { id: document onStatusChanged: if (status === PdfDocument.Error) errorDialog.open() @@ -233,6 +208,88 @@ ApplicationWindow { searchString: searchField.text } + Drawer { + id: searchDrawer + edge: Qt.LeftEdge + modal: false + width: searchLayout.implicitWidth + y: root.header.height + height: root.contentItem.height + dim: false + Shortcut { + sequence: StandardKey.Find + onActivated: { + searchDrawer.open() + searchField.forceActiveFocus() + } + } + ColumnLayout { + id: searchLayout + anchors.fill: parent + anchors.margins: 2 + RowLayout { + ToolButton { + action: Action { + icon.source: "resources/go-up-search.svg" + shortcut: StandardKey.FindPrevious + onTriggered: pageView.searchBack() + } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "find previous" + } + TextField { + id: searchField + placeholderText: "search" + Layout.minimumWidth: 200 + Layout.fillWidth: true + Image { + visible: searchField.text !== "" + source: "resources/edit-clear.svg" + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + margins: 3 + rightMargin: 5 + } + TapHandler { + onTapped: searchField.clear() + } + } + } + ToolButton { + action: Action { + icon.source: "resources/go-down-search.svg" + shortcut: StandardKey.FindNext + onTriggered: pageView.searchForward() + } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "find next" + } + } + ListView { + id: searchResultsList + ColumnLayout.fillWidth: true + ColumnLayout.fillHeight: true + clip: true + model: pageView.searchModel + ScrollBar.vertical: ScrollBar { } + delegate: ItemDelegate { + width: parent ? parent.width : 0 + text: "page " + (page + 1) + ": " + context + highlighted: ListView.isCurrentItem + onClicked: { + searchResultsList.currentIndex = index + pageView.goToLocation(page, location, 0) + pageView.searchModel.currentResult = indexOnPage + } + } + } + } + } + footer: Label { property size implicitPointSize: document.pagePointSize(pageView.currentPage) text: "page " + (pageView.currentPage + 1) + " of " + pageView.pageCount + diff --git a/examples/pdf/pdfviewer/viewer.qrc b/examples/pdf/pdfviewer/viewer.qrc index fa3561caf..9698a2689 100644 --- a/examples/pdf/pdfviewer/viewer.qrc +++ b/examples/pdf/pdfviewer/viewer.qrc @@ -3,8 +3,10 @@ viewer.qml resources/edit-clear.svg resources/edit-copy.svg + resources/go-down-search.svg resources/go-next-view-page.svg resources/go-previous-view-page.svg + resources/go-up-search.svg resources/rotate-left.svg resources/rotate-right.svg resources/zoom-in.svg diff --git a/src/pdf/api/qpdfdestination.h b/src/pdf/api/qpdfdestination.h index dc5d6314a..cad041982 100644 --- a/src/pdf/api/qpdfdestination.h +++ b/src/pdf/api/qpdfdestination.h @@ -65,14 +65,14 @@ public: QPointF location() const; qreal zoom() const; -private: +protected: QPdfDestination(); QPdfDestination(int page, QPointF location, qreal zoom); QPdfDestination(QPdfDestinationPrivate *d); friend class QPdfDocument; friend class QQuickPdfNavigationStack; -private: +protected: QExplicitlySharedDataPointer d; }; diff --git a/src/pdf/api/qpdfdestination_p.h b/src/pdf/api/qpdfdestination_p.h index a5aeb804f..3520fb795 100644 --- a/src/pdf/api/qpdfdestination_p.h +++ b/src/pdf/api/qpdfdestination_p.h @@ -37,6 +37,17 @@ #ifndef QPDFDESTINATION_P_H #define QPDFDESTINATION_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 QT_BEGIN_NAMESPACE diff --git a/src/pdf/api/qpdfdocument.h b/src/pdf/api/qpdfdocument.h index 9d3a4bb19..40df181f4 100644 --- a/src/pdf/api/qpdfdocument.h +++ b/src/pdf/api/qpdfdocument.h @@ -124,6 +124,7 @@ private: friend class QPdfBookmarkModelPrivate; friend class QPdfLinkModelPrivate; friend class QPdfSearchModel; + friend class QPdfSearchModelPrivate; Q_PRIVATE_SLOT(d, void _q_tryLoadingWithSizeFromContentHeader()) Q_PRIVATE_SLOT(d, void _q_copyFromSequentialSourceDevice()) diff --git a/src/pdf/api/qpdfsearchmodel.h b/src/pdf/api/qpdfsearchmodel.h index 02d2a20d5..c8190f192 100644 --- a/src/pdf/api/qpdfsearchmodel.h +++ b/src/pdf/api/qpdfsearchmodel.h @@ -39,34 +39,56 @@ #include "qtpdfglobal.h" #include "qpdfdocument.h" +#include "qpdfsearchresult.h" -#include +#include QT_BEGIN_NAMESPACE class QPdfSearchModelPrivate; -class Q_PDF_EXPORT QPdfSearchModel : public QObject // TODO QAIM? +class Q_PDF_EXPORT QPdfSearchModel : public QAbstractListModel { Q_OBJECT Q_PROPERTY(QPdfDocument *document READ document WRITE setDocument NOTIFY documentChanged) + Q_PROPERTY(QString searchString READ searchString WRITE setSearchString NOTIFY searchStringChanged) public: + enum class Role : int { + Page = Qt::UserRole, + IndexOnPage, + Location, + Context, + _Count + }; + Q_ENUM(Role) explicit QPdfSearchModel(QObject *parent = nullptr); ~QPdfSearchModel(); - QVector matches(int page, const QString &searchString); + QVector resultsOnPage(int page) const; + QPdfSearchResult resultAtIndex(int index) const; QPdfDocument *document() const; + QString searchString() const; + + QHash roleNames() const override; + int rowCount(const QModelIndex &parent) const override; + QVariant data(const QModelIndex &index, int role) const override; public Q_SLOTS: + void setSearchString(QString searchString); void setDocument(QPdfDocument *document); Q_SIGNALS: void documentChanged(); + void searchStringChanged(); + +protected: + void updatePage(int page); private: - QScopedPointer d; + QHash m_roleNames; + Q_DECLARE_PRIVATE(QPdfSearchModel) }; QT_END_NAMESPACE diff --git a/src/pdf/api/qpdfsearchmodel_p.h b/src/pdf/api/qpdfsearchmodel_p.h new file mode 100644 index 000000000..0855bc216 --- /dev/null +++ b/src/pdf/api/qpdfsearchmodel_p.h @@ -0,0 +1,84 @@ +/**************************************************************************** +** +** 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 QPDFSEARCHMODEL_P_H +#define QPDFSEARCHMODEL_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 "qpdfsearchmodel.h" +#include "qpdfsearchresult_p.h" +#include + +#include "third_party/pdfium/public/fpdfview.h" + +QT_BEGIN_NAMESPACE + +class QPdfSearchModelPrivate : public QAbstractItemModelPrivate +{ + Q_DECLARE_PUBLIC(QPdfSearchModel) + +public: + QPdfSearchModelPrivate(); + void clearResults(); + bool doSearch(int page); + + struct PageAndIndex { + int page; + int index; + }; + PageAndIndex pageAndIndexForResult(int resultIndex); + int rowsBeforePage(int page); + + QPdfDocument *document = nullptr; + QString searchString; + QVector pagesSearched; + QVector> searchResults; + int rowCountSoFar = 0; +}; + +QT_END_NAMESPACE + +#endif // QPDFSEARCHMODEL_P_H diff --git a/src/pdf/api/qpdfsearchresult.h b/src/pdf/api/qpdfsearchresult.h new file mode 100644 index 000000000..db7af3dd9 --- /dev/null +++ b/src/pdf/api/qpdfsearchresult.h @@ -0,0 +1,75 @@ +/**************************************************************************** +** +** 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 QPDFSEARCHRESULT_H +#define QPDFSEARCHRESULT_H + +#include "qpdfdestination.h" + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +class QPdfSearchResultPrivate; + +class Q_PDF_EXPORT QPdfSearchResult : public QPdfDestination +{ + Q_GADGET + Q_PROPERTY(QString context READ context) + Q_PROPERTY(QVector rectangles READ rectangles) + +public: + QPdfSearchResult(); + ~QPdfSearchResult() {} + + QString context() const; + QVector rectangles() const; + +private: + QPdfSearchResult(int page, QVector rects, QString context); + QPdfSearchResult(QPdfSearchResultPrivate *d); + friend class QPdfDocument; + friend class QPdfSearchModelPrivate; + friend class QQuickPdfNavigationStack; +}; + +Q_PDF_EXPORT QDebug operator<<(QDebug, const QPdfSearchResult &); + +QT_END_NAMESPACE + +#endif // QPDFSEARCHRESULT_H diff --git a/src/pdf/api/qpdfsearchresult_p.h b/src/pdf/api/qpdfsearchresult_p.h new file mode 100644 index 000000000..a0f8e4457 --- /dev/null +++ b/src/pdf/api/qpdfsearchresult_p.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** 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 QPDFSEARCHRESULT_P_H +#define QPDFSEARCHRESULT_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 "qpdfdestination_p.h" + +QT_BEGIN_NAMESPACE + +class QPdfSearchResultPrivate : public QPdfDestinationPrivate +{ +public: + QPdfSearchResultPrivate() = default; + QPdfSearchResultPrivate(int page, QVector rects, QString context) : + QPdfDestinationPrivate(page, rects.first().topLeft(), 0), + context(context), + rects(rects) {} + + QString context; + QVector rects; +}; + +QT_END_NAMESPACE + +#endif // QPDFSEARCHRESULT_P_H diff --git a/src/pdf/pdfcore.pro b/src/pdf/pdfcore.pro index 951b5699f..e723a02fd 100644 --- a/src/pdf/pdfcore.pro +++ b/src/pdf/pdfcore.pro @@ -66,6 +66,7 @@ SOURCES += \ qpdfpagenavigation.cpp \ qpdfpagerenderer.cpp \ qpdfsearchmodel.cpp \ + qpdfsearchresult.cpp \ qpdfselection.cpp \ # all "public" headers must be in "api" for sync script and to hide auto generated headers @@ -85,7 +86,9 @@ HEADERS += \ api/qpdfpagenavigation.h \ api/qpdfpagerenderer.h \ api/qpdfsearchmodel.h \ - qpdfsearchmodel_p.h \ + api/qpdfsearchmodel_p.h \ + api/qpdfsearchresult.h \ + api/qpdfsearchresult_p.h \ api/qpdfselection.h \ api/qpdfselection_p.h \ diff --git a/src/pdf/qpdfsearchmodel.cpp b/src/pdf/qpdfsearchmodel.cpp index 9010d76d3..aa19af5b1 100644 --- a/src/pdf/qpdfsearchmodel.cpp +++ b/src/pdf/qpdfsearchmodel.cpp @@ -34,80 +34,257 @@ ** ****************************************************************************/ +#include "qpdfdestination.h" +#include "qpdfdocument_p.h" #include "qpdfsearchmodel.h" #include "qpdfsearchmodel_p.h" -#include "qpdfdocument_p.h" +#include "qpdfsearchresult_p.h" #include "third_party/pdfium/public/fpdf_doc.h" #include "third_party/pdfium/public/fpdf_text.h" -#include +#include +#include +#include QT_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(qLcS, "qt.pdf.search") +static const int ContextChars = 20; +static const double CharacterHitTolerance = 6.0; + QPdfSearchModel::QPdfSearchModel(QObject *parent) - : QObject(parent), - d(new QPdfSearchModelPrivate()) + : QAbstractListModel(*(new QPdfSearchModelPrivate()), parent) { + QMetaEnum rolesMetaEnum = metaObject()->enumerator(metaObject()->indexOfEnumerator("Role")); + for (int r = Qt::UserRole; r < int(Role::_Count); ++r) { + QByteArray roleName = QByteArray(rolesMetaEnum.valueToKey(r)); + if (roleName.isEmpty()) + continue; + roleName[0] = QChar::toLower(roleName[0]); + m_roleNames.insert(r, roleName); + } } QPdfSearchModel::~QPdfSearchModel() {} -QVector QPdfSearchModel::matches(int page, const QString &searchString) +QHash QPdfSearchModel::roleNames() const +{ + return m_roleNames; +} + +int QPdfSearchModel::rowCount(const QModelIndex &parent) const +{ + Q_D(const QPdfSearchModel); + Q_UNUSED(parent) + return d->rowCountSoFar; +} + +QVariant QPdfSearchModel::data(const QModelIndex &index, int role) const { + Q_D(const QPdfSearchModel); + const auto pi = const_cast(d)->pageAndIndexForResult(index.row()); + if (pi.page < 0) + return QVariant(); + switch (Role(role)) { + case Role::Page: + return pi.page; + case Role::IndexOnPage: + return pi.index; + case Role::Location: + return d->searchResults[pi.page][pi.index].location(); + case Role::Context: + return d->searchResults[pi.page][pi.index].context(); + case Role::_Count: + break; + } + if (role == Qt::DisplayRole) + return d->searchResults[pi.page][pi.index].context(); + return QVariant(); +} + +void QPdfSearchModel::updatePage(int page) +{ + Q_D(QPdfSearchModel); + d->doSearch(page); +} + +QString QPdfSearchModel::searchString() const +{ + Q_D(const QPdfSearchModel); + return d->searchString; +} + +void QPdfSearchModel::setSearchString(QString searchString) +{ + Q_D(QPdfSearchModel); + if (d->searchString == searchString) + return; + + d->searchString = searchString; + emit searchStringChanged(); + beginResetModel(); + d->clearResults(); + endResetModel(); +} + +QVector QPdfSearchModel::resultsOnPage(int page) const +{ + Q_D(const QPdfSearchModel); + const_cast(d)->doSearch(page); + if (d->searchResults.count() <= page) + return {}; + return d->searchResults[page]; +} + +QPdfSearchResult QPdfSearchModel::resultAtIndex(int index) const +{ + Q_D(const QPdfSearchModel); + const auto pi = const_cast(d)->pageAndIndexForResult(index); + if (pi.page < 0) + return QPdfSearchResult(); + return d->searchResults[pi.page][pi.index]; +} + +QPdfDocument *QPdfSearchModel::document() const +{ + Q_D(const QPdfSearchModel); + return d->document; +} + +void QPdfSearchModel::setDocument(QPdfDocument *document) +{ + Q_D(QPdfSearchModel); + if (d->document == document) + return; + + d->document = document; + emit documentChanged(); + d->clearResults(); +} + +QPdfSearchModelPrivate::QPdfSearchModelPrivate() +{ +} + +void QPdfSearchModelPrivate::clearResults() +{ + rowCountSoFar = 0; + searchResults.clear(); + pagesSearched.clear(); + if (document) { + searchResults.resize(document->pageCount()); + pagesSearched.resize(document->pageCount()); + } else { + searchResults.resize(0); + pagesSearched.resize(0); + } +} + +bool QPdfSearchModelPrivate::doSearch(int page) +{ + if (page < 0 || page >= pagesSearched.count() || searchString.isEmpty()) + return false; + if (pagesSearched[page]) + return true; + Q_Q(QPdfSearchModel); + const QPdfMutexLocker lock; - FPDF_PAGE pdfPage = FPDF_LoadPage(d->document->d->doc, page); + QElapsedTimer timer; + timer.start(); + FPDF_PAGE pdfPage = FPDF_LoadPage(document->d->doc, page); if (!pdfPage) { qWarning() << "failed to load page" << page; - return {}; + return false; } double pageHeight = FPDF_GetPageHeight(pdfPage); FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage); if (!textPage) { qWarning() << "failed to load text of page" << page; FPDF_ClosePage(pdfPage); - return {}; + return false; } - QVector ret; - if (searchString.isEmpty()) - return ret; FPDF_SCHHANDLE sh = FPDFText_FindStart(textPage, searchString.utf16(), 0, 0); + QVector newSearchResults; while (FPDFText_FindNext(sh)) { int idx = FPDFText_GetSchResultIndex(sh); int count = FPDFText_GetSchCount(sh); int rectCount = FPDFText_CountRects(textPage, idx, count); - qCDebug(qLcS) << searchString << ": matched" << count << "chars @" << idx << "across" << rectCount << "rects"; + QVector rects; + int startIndex = -1; + int endIndex = -1; for (int r = 0; r < rectCount; ++r) { double left, top, right, bottom; FPDFText_GetRect(textPage, r, &left, &top, &right, &bottom); - ret << QRectF(left, pageHeight - top, right - left, top - bottom); - qCDebug(qLcS) << ret.last(); + rects << QRectF(left, pageHeight - top, right - left, top - bottom); + if (r == 0) { + startIndex = FPDFText_GetCharIndexAtPos(textPage, left, top, + CharacterHitTolerance, CharacterHitTolerance); + } + if (r == rectCount - 1) { + endIndex = FPDFText_GetCharIndexAtPos(textPage, right, top, + CharacterHitTolerance, CharacterHitTolerance); + } + qCDebug(qLcS) << rects.last() << "char idx" << startIndex << "->" << endIndex; + } + QString context; + if (startIndex >= 0 || endIndex >= 0) { + startIndex = qMax(0, startIndex - ContextChars); + endIndex += ContextChars; + int count = endIndex - startIndex + 1; + if (count > 0) { + QVector buf(count + 1); + int len = FPDFText_GetText(textPage, startIndex, count, buf.data()); + Q_ASSERT(len - 1 <= count); // len is number of characters written, including the terminator + context = QString::fromUtf16(buf.constData(), len - 1); + context = context.replace(QLatin1Char('\n'), QLatin1Char(' ')); + context = context.replace(searchString, + QLatin1String("") + searchString + QLatin1String("")); + } } + newSearchResults << QPdfSearchResult(page, rects, context); } FPDFText_FindClose(sh); FPDFText_ClosePage(textPage); FPDF_ClosePage(pdfPage); + qCDebug(qLcS) << searchString << "took" << timer.elapsed() << "ms to find" + << newSearchResults.count() << "results on page" << page; - return ret; -} - -QPdfDocument *QPdfSearchModel::document() const -{ - return d->document; + pagesSearched[page] = true; + searchResults[page] = newSearchResults; + if (newSearchResults.count() > 0) { + int rowsBefore = rowsBeforePage(page); + qCDebug(qLcS) << "from row" << rowsBefore << "rowCount" << rowCountSoFar << "increasing by" << newSearchResults.count(); + rowCountSoFar += newSearchResults.count(); + q->beginInsertRows(QModelIndex(), rowsBefore, rowsBefore + newSearchResults.count() - 1); + q->endInsertRows(); + } + return true; } -void QPdfSearchModel::setDocument(QPdfDocument *document) +QPdfSearchModelPrivate::PageAndIndex QPdfSearchModelPrivate::pageAndIndexForResult(int resultIndex) { - if (d->document == document) - return; - d->document = document; - emit documentChanged(); + const int pageCount = document->pageCount(); + int totalSoFar = 0; + int previousTotalSoFar = 0; + for (int page = 0; page < pageCount; ++page) { + if (!pagesSearched[page]) + doSearch(page); + totalSoFar += searchResults[page].count(); + if (totalSoFar > resultIndex) + return {page, resultIndex - previousTotalSoFar}; + previousTotalSoFar = totalSoFar; + } + return {-1, -1}; } -QPdfSearchModelPrivate::QPdfSearchModelPrivate() +int QPdfSearchModelPrivate::rowsBeforePage(int page) { + int ret = 0; + for (int i = 0; i < page; ++i) + ret += searchResults[i].count(); + return ret; } QT_END_NAMESPACE diff --git a/src/pdf/qpdfsearchmodel_p.h b/src/pdf/qpdfsearchmodel_p.h deleted file mode 100644 index 90490d8e5..000000000 --- a/src/pdf/qpdfsearchmodel_p.h +++ /dev/null @@ -1,56 +0,0 @@ -/**************************************************************************** -** -** 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 QPDFSEARCHMODEL_P_H -#define QPDFSEARCHMODEL_P_H - -#include "qpdfsearchmodel.h" - -#include "third_party/pdfium/public/fpdfview.h" - -QT_BEGIN_NAMESPACE - -class QPdfSearchModelPrivate -{ -public: - QPdfSearchModelPrivate(); - - QPdfDocument *document = nullptr; -}; - -QT_END_NAMESPACE - -#endif // QPDFSEARCHMODEL_P_H diff --git a/src/pdf/qpdfsearchresult.cpp b/src/pdf/qpdfsearchresult.cpp new file mode 100644 index 000000000..1164a1d43 --- /dev/null +++ b/src/pdf/qpdfsearchresult.cpp @@ -0,0 +1,74 @@ +/**************************************************************************** +** +** 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 "qpdfsearchresult.h" +#include "qpdfsearchresult_p.h" + +QT_BEGIN_NAMESPACE + +QPdfSearchResult::QPdfSearchResult() : + QPdfSearchResult(new QPdfSearchResultPrivate()) { } + +QPdfSearchResult::QPdfSearchResult(int page, QVector rects, QString context) : + QPdfSearchResult(new QPdfSearchResultPrivate(page, rects, context)) { } + +QPdfSearchResult::QPdfSearchResult(QPdfSearchResultPrivate *d) : + QPdfDestination(static_cast(d)) { } + +QString QPdfSearchResult::context() const +{ + return static_cast(d.data())->context; +} + +QVector QPdfSearchResult::rectangles() const +{ + return static_cast(d.data())->rects; +} + +QDebug operator<<(QDebug dbg, const QPdfSearchResult &searchResult) +{ + QDebugStateSaver saver(dbg); + dbg.nospace(); + dbg << "QPdfSearchResult(page=" << searchResult.page() + << " context=" << searchResult.context() + << " rects=" << searchResult.rectangles(); + dbg << ')'; + return dbg; +} + +QT_END_NAMESPACE + +#include "moc_qpdfsearchresult.cpp" 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 d0e96aa21daa8601254cffd584d33f38d62ff1df Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Fri, 14 Feb 2020 09:26:19 +0100 Subject: Use a timer to update PdfSearchModel in the background Change-Id: I855150578c9127b175c5907500d057b704fe5e0e Reviewed-by: Shawn Rutledge --- src/pdf/api/qpdfsearchmodel.h | 1 + src/pdf/api/qpdfsearchmodel_p.h | 2 ++ src/pdf/qpdfsearchmodel.cpp | 18 ++++++++++++++++++ 3 files changed, 21 insertions(+) diff --git a/src/pdf/api/qpdfsearchmodel.h b/src/pdf/api/qpdfsearchmodel.h index c8190f192..cc91e214a 100644 --- a/src/pdf/api/qpdfsearchmodel.h +++ b/src/pdf/api/qpdfsearchmodel.h @@ -85,6 +85,7 @@ Q_SIGNALS: protected: void updatePage(int page); + void timerEvent(QTimerEvent *event) override; private: QHash m_roleNames; diff --git a/src/pdf/api/qpdfsearchmodel_p.h b/src/pdf/api/qpdfsearchmodel_p.h index 0855bc216..2a23706b2 100644 --- a/src/pdf/api/qpdfsearchmodel_p.h +++ b/src/pdf/api/qpdfsearchmodel_p.h @@ -77,6 +77,8 @@ public: QVector pagesSearched; QVector> searchResults; int rowCountSoFar = 0; + int updateTimerId = -1; + int nextPageToUpdate = 0; }; QT_END_NAMESPACE diff --git a/src/pdf/qpdfsearchmodel.cpp b/src/pdf/qpdfsearchmodel.cpp index aa19af5b1..4129c7cb7 100644 --- a/src/pdf/qpdfsearchmodel.cpp +++ b/src/pdf/qpdfsearchmodel.cpp @@ -51,6 +51,7 @@ QT_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(qLcS, "qt.pdf.search") +static const int UpdateTimerInterval = 100; static const int ContextChars = 20; static const double CharacterHitTolerance = 6.0; @@ -164,12 +165,27 @@ void QPdfSearchModel::setDocument(QPdfDocument *document) d->clearResults(); } +void QPdfSearchModel::timerEvent(QTimerEvent *event) +{ + Q_D(QPdfSearchModel); + if (event->timerId() != d->updateTimerId) + return; + if (!d->document || d->nextPageToUpdate >= d->document->pageCount()) { + if (d->document) + qCDebug(qLcS, "done updating search results on %d pages", d->searchResults.count()); + killTimer(d->updateTimerId); + d->updateTimerId = -1; + } + d->doSearch(d->nextPageToUpdate++); +} + QPdfSearchModelPrivate::QPdfSearchModelPrivate() { } void QPdfSearchModelPrivate::clearResults() { + Q_Q(QPdfSearchModel); rowCountSoFar = 0; searchResults.clear(); pagesSearched.clear(); @@ -180,6 +196,8 @@ void QPdfSearchModelPrivate::clearResults() searchResults.resize(0); pagesSearched.resize(0); } + nextPageToUpdate = 0; + updateTimerId = q->startTimer(UpdateTimerInterval); } bool QPdfSearchModelPrivate::doSearch(int page) -- cgit v1.2.3 From 3b3970776c99b6ad7ac13cd8b743fc77a455b98c Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Mon, 17 Feb 2020 17:02:52 +0100 Subject: Fix operators and includes in QPdfDestination Amends 09a6eac4a63b32548ecc1ff5b16a5d8fc3ba1c04. Change-Id: Id321d016a758d4f58d82a32575d034df226e083e Reviewed-by: Shawn Rutledge --- src/pdf/api/qpdfdestination.h | 9 ++++++--- src/pdf/qpdfdestination.cpp | 23 ++++++++++++++--------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/pdf/api/qpdfdestination.h b/src/pdf/api/qpdfdestination.h index cad041982..325863226 100644 --- a/src/pdf/api/qpdfdestination.h +++ b/src/pdf/api/qpdfdestination.h @@ -38,9 +38,10 @@ #define QPDFDESTINATION_H #include -#include -#include -#include +#include +#include +#include +#include QT_BEGIN_NAMESPACE @@ -76,6 +77,8 @@ protected: QExplicitlySharedDataPointer d; }; +Q_PDF_EXPORT QDebug operator<<(QDebug, const QPdfDestination &); + QT_END_NAMESPACE #endif // QPDFDESTINATION_H diff --git a/src/pdf/qpdfdestination.cpp b/src/pdf/qpdfdestination.cpp index 86e429dcf..b347445e9 100644 --- a/src/pdf/qpdfdestination.cpp +++ b/src/pdf/qpdfdestination.cpp @@ -77,6 +77,12 @@ QPdfDestination::~QPdfDestination() { } +QPdfDestination &QPdfDestination::operator=(const QPdfDestination &other) +{ + d = other.d; + return *this; +} + /*! \property QPdfDestination::valid @@ -118,16 +124,15 @@ qreal QPdfDestination::zoom() const return d->zoom; } -//QDataStream& operator<<(QDataStream& stream, const QPdfDestination& dest) -//{ -// stream << *dest.d.data(); -// return stream; -//} - -QDataStream& operator<<(QDataStream& stream, const QPdfDestinationPrivate& dest) +QDebug operator<<(QDebug dbg, const QPdfDestination& dest) { - stream << QStringLiteral("QPdfDestination") << dest.page << dest.location ; // << dest.zoom(); - return stream; + QDebugStateSaver saver(dbg); + dbg.nospace(); + dbg << "QPdfDestination(page=" << dest.page() + << " location=" << dest.location() + << " zoom=" << dest.zoom(); + dbg << ')'; + return dbg; } QT_END_NAMESPACE -- 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 --- examples/pdf/multipage/viewer.qml | 4 +- examples/pdf/pdfviewer/viewer.qml | 11 ++-- src/pdf/quick/qml/PdfPageView.qml | 113 +++++++++++++++++++------------------- 3 files changed, 64 insertions(+), 64 deletions(-) diff --git a/examples/pdf/multipage/viewer.qml b/examples/pdf/multipage/viewer.qml index 8f102a3c1..3a6347f5a 100644 --- a/examples/pdf/multipage/viewer.qml +++ b/examples/pdf/multipage/viewer.qml @@ -299,8 +299,8 @@ ApplicationWindow { } ListView { id: searchResultsList - ColumnLayout.fillWidth: true - ColumnLayout.fillHeight: true + Layout.fillWidth: true + Layout.fillHeight: true clip: true model: view.searchModel ScrollBar.vertical: ScrollBar { } diff --git a/examples/pdf/pdfviewer/viewer.qml b/examples/pdf/pdfviewer/viewer.qml index e94642c5e..c63485dd3 100644 --- a/examples/pdf/pdfviewer/viewer.qml +++ b/examples/pdf/pdfviewer/viewer.qml @@ -271,8 +271,8 @@ ApplicationWindow { } ListView { id: searchResultsList - ColumnLayout.fillWidth: true - ColumnLayout.fillHeight: true + Layout.fillWidth: true + Layout.fillHeight: true clip: true model: pageView.searchModel ScrollBar.vertical: ScrollBar { } @@ -292,10 +292,9 @@ ApplicationWindow { footer: Label { property size implicitPointSize: document.pagePointSize(pageView.currentPage) - text: "page " + (pageView.currentPage + 1) + " of " + pageView.pageCount + + text: "page " + (pageView.currentPage + 1) + " of " + document.pageCount + " scale " + pageView.renderScale.toFixed(2) + - " sourceSize " + pageView.sourceSize.width.toFixed(1) + "x" + pageView.sourceSize.height.toFixed(1) + - " original " + implicitPointSize.width.toFixed(1) + "x" + implicitPointSize.height.toFixed(1) - visible: pageView.pageCount > 0 + " original " + implicitPointSize.width.toFixed(1) + "x" + implicitPointSize.height.toFixed(1) + "pts" + visible: document.status === PdfDocument.Ready } } 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 --- examples/pdf/pdfviewer/viewer.qml | 16 ++++++++++++++++ src/pdf/quick/qml/PdfPageView.qml | 10 ++++++++++ 2 files changed, 26 insertions(+) diff --git a/examples/pdf/pdfviewer/viewer.qml b/examples/pdf/pdfviewer/viewer.qml index c63485dd3..586a822a0 100644 --- a/examples/pdf/pdfviewer/viewer.qml +++ b/examples/pdf/pdfviewer/viewer.qml @@ -53,6 +53,7 @@ import QtQuick.Layouts 1.14 import QtQuick.Pdf 5.15 import QtQuick.Shapes 1.14 import QtQuick.Window 2.14 +import Qt.labs.animation 1.0 import Qt.labs.platform 1.1 as Platform ApplicationWindow { @@ -208,6 +209,21 @@ ApplicationWindow { searchString: searchField.text } + WheelHandler { + rotationScale: 15 + target: pageView + property: "x" + orientation: Qt.Horizontal + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + } + WheelHandler { + rotationScale: 15 + target: pageView + property: "y" + orientation: Qt.Vertical + acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad + } + Drawer { id: searchDrawer edge: Qt.LeftEdge 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(-) 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/qpdflinkmodel.cpp | 2 +- src/pdf/quick/qml/PdfMultiPageView.qml | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pdf/qpdflinkmodel.cpp b/src/pdf/qpdflinkmodel.cpp index 8b49fec21..96e6ddd5c 100644 --- a/src/pdf/qpdflinkmodel.cpp +++ b/src/pdf/qpdflinkmodel.cpp @@ -179,7 +179,7 @@ void QPdfLinkModelPrivate::update() if (!ok) break; if (hasX && hasY) - linkData.location = QPointF(x, y); + linkData.location = QPointF(x, pageHeight - y); if (hasZoom) linkData.zoom = zoom; links << linkData; 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 a2be3a7a79dc4fabe8675ea80a6ba550e0e4deec Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Wed, 19 Feb 2020 10:30:52 +0100 Subject: PDF multipage example: move search field to the footer Hiding the search feature in a closed Drawer might not have been sufficiently touch-friendly and discoverable, for example on iOS where the user is not going to press control-F to start searching. But the top toolbar is too full to put the search field back up there. It's familiar from Firefox to have the search field at the bottom, and we have enough space for it there. So now you can search and jump around the search results without opening the drawer; but pressing Enter in the search field opens the drawer. Hopefully swiping to open the drawer is also convenient enough on touch platforms; otherwise we could add another button for that, perhaps at the left of the footer. Change-Id: Iaec63bc22b03e29156fee817d197daae5b0cf9d5 Reviewed-by: Shawn Rutledge --- examples/pdf/multipage/viewer.qml | 145 ++++++++++++++++++-------------------- 1 file changed, 69 insertions(+), 76 deletions(-) diff --git a/examples/pdf/multipage/viewer.qml b/examples/pdf/multipage/viewer.qml index 3a6347f5a..ba54065b7 100644 --- a/examples/pdf/multipage/viewer.qml +++ b/examples/pdf/multipage/viewer.qml @@ -167,6 +167,10 @@ ApplicationWindow { onTriggered: view.copySelectionToClipboard() } } + Shortcut { + sequence: StandardKey.Find + onActivated: searchField.forceActiveFocus() + } Shortcut { sequence: StandardKey.Quit onActivated: Qt.quit() @@ -240,95 +244,84 @@ ApplicationWindow { id: searchDrawer edge: Qt.LeftEdge modal: false - width: searchLayout.implicitWidth + width: 300 y: root.header.height height: view.height dim: false - Shortcut { - sequence: StandardKey.Find - onActivated: { - searchDrawer.open() - searchField.forceActiveFocus() + ListView { + id: searchResultsList + anchors.fill: parent + anchors.margins: 2 + model: view.searchModel + ScrollBar.vertical: ScrollBar { } + delegate: ItemDelegate { + width: parent ? parent.width : 0 + text: "page " + (page + 1) + ": " + context + highlighted: ListView.isCurrentItem + onClicked: { + searchResultsList.currentIndex = index + view.goToLocation(page, location, 0) + view.searchModel.currentResult = indexOnPage + } } } - ColumnLayout { - id: searchLayout + } + + footer: ToolBar { + height: footerRow.implicitHeight + RowLayout { + id: footerRow anchors.fill: parent - anchors.margins: 2 - RowLayout { - ToolButton { - action: Action { - icon.source: "resources/go-up-search.svg" - shortcut: StandardKey.FindPrevious - onTriggered: view.searchBack() - } - ToolTip.visible: enabled && hovered - ToolTip.delay: 2000 - ToolTip.text: "find previous" + ToolButton { + action: Action { + icon.source: "resources/go-up-search.svg" + shortcut: StandardKey.FindPrevious + onTriggered: view.searchBack() } - TextField { - id: searchField - placeholderText: "search" - Layout.minimumWidth: 200 - Layout.fillWidth: true - Image { - visible: searchField.text !== "" - source: "resources/edit-clear.svg" - anchors { - right: parent.right - top: parent.top - bottom: parent.bottom - margins: 3 - rightMargin: 5 - } - TapHandler { - onTapped: searchField.clear() - } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "find previous" + } + TextField { + id: searchField + placeholderText: "search" + Layout.minimumWidth: 200 + Layout.fillWidth: true + onAccepted: searchDrawer.open() + Image { + visible: searchField.text !== "" + source: "resources/edit-clear.svg" + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + margins: 3 + rightMargin: 5 } - } - ToolButton { - action: Action { - icon.source: "resources/go-down-search.svg" - shortcut: StandardKey.FindNext - onTriggered: view.searchForward() + TapHandler { + onTapped: searchField.clear() } - ToolTip.visible: enabled && hovered - ToolTip.delay: 2000 - ToolTip.text: "find next" } } - ListView { - id: searchResultsList - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - model: view.searchModel - ScrollBar.vertical: ScrollBar { } - delegate: ItemDelegate { - width: parent ? parent.width : 0 - text: "page " + (page + 1) + ": " + context - highlighted: ListView.isCurrentItem - onClicked: { - searchResultsList.currentIndex = index - view.goToLocation(page, location, 0) - view.searchModel.currentResult = indexOnPage - } + ToolButton { + action: Action { + icon.source: "resources/go-down-search.svg" + shortcut: StandardKey.FindNext + onTriggered: view.searchForward() } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "find next" + } + Label { + id: statusLabel + Layout.fillWidth: true + property size implicitPointSize: document.pagePointSize(view.currentPage) + text: "page " + (currentPageSB.value) + " of " + document.pageCount + + " scale " + view.renderScale.toFixed(2) + + " original size " + implicitPointSize.width.toFixed(1) + "x" + implicitPointSize.height.toFixed(1) + " pt" + visible: document.pageCount > 0 } - } - } - - footer: ToolBar { - height: statusLabel.implicitHeight * 1.5 - Label { - id: statusLabel - anchors.verticalCenter: parent.verticalCenter - x: 6 - property size implicitPointSize: document.pagePointSize(view.currentPage) - text: "page " + (currentPageSB.value) + " of " + document.pageCount + - " scale " + view.renderScale.toFixed(2) + - " original size " + implicitPointSize.width.toFixed(1) + "x" + implicitPointSize.height.toFixed(1) + " pt" - visible: document.pageCount > 0 } } } -- 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 --- examples/pdf/multipage/main.cpp | 3 +++ examples/pdf/multipage/multipage.pro | 2 +- examples/pdf/multipage/resources/test.pdf | Bin 0 -> 80045 bytes examples/pdf/multipage/viewer.qml | 3 ++- examples/pdf/multipage/viewer.qrc | 3 ++- examples/pdf/pdfviewer/main.cpp | 2 ++ examples/pdf/pdfviewer/pdfviewer.pro | 2 +- examples/pdf/pdfviewer/resources/test.pdf | Bin 0 -> 80045 bytes examples/pdf/pdfviewer/viewer.qml | 3 ++- examples/pdf/pdfviewer/viewer.qrc | 3 ++- src/pdf/qpdfdocument.cpp | 4 +++- src/pdf/quick/qquickpdfdocument.cpp | 5 ++++- 12 files changed, 22 insertions(+), 8 deletions(-) create mode 100644 examples/pdf/multipage/resources/test.pdf create mode 100644 examples/pdf/pdfviewer/resources/test.pdf diff --git a/examples/pdf/multipage/main.cpp b/examples/pdf/multipage/main.cpp index 7b766d77e..35aaa8c64 100644 --- a/examples/pdf/multipage/main.cpp +++ b/examples/pdf/multipage/main.cpp @@ -64,7 +64,10 @@ int main(int argc, char* argv[]) if (app.arguments().count() > 1) { QUrl toLoad = QUrl::fromUserInput(app.arguments().at(1)); engine.rootObjects().first()->setProperty("source", toLoad); + } else { + engine.rootObjects().first()->setProperty("source", QStringLiteral("resources/test.pdf")); } + return app.exec(); } diff --git a/examples/pdf/multipage/multipage.pro b/examples/pdf/multipage/multipage.pro index 5fff58fe1..5df9e653d 100644 --- a/examples/pdf/multipage/multipage.pro +++ b/examples/pdf/multipage/multipage.pro @@ -1,6 +1,6 @@ TEMPLATE = app -QT += qml quick pdf widgets +QT += qml quick pdf widgets svg SOURCES += main.cpp diff --git a/examples/pdf/multipage/resources/test.pdf b/examples/pdf/multipage/resources/test.pdf new file mode 100644 index 000000000..a9dc1bc29 Binary files /dev/null and b/examples/pdf/multipage/resources/test.pdf differ diff --git a/examples/pdf/multipage/viewer.qml b/examples/pdf/multipage/viewer.qml index ba54065b7..ac6d2cd9a 100644 --- a/examples/pdf/multipage/viewer.qml +++ b/examples/pdf/multipage/viewer.qml @@ -62,7 +62,7 @@ ApplicationWindow { color: "lightgrey" title: document.title visible: true - property alias source: document.source // for main.cpp + property string source // for main.cpp header: ToolBar { RowLayout { @@ -221,6 +221,7 @@ ApplicationWindow { PdfDocument { id: document + source: Qt.resolvedUrl(root.source) onStatusChanged: { if (status === PdfDocument.Error) errorDialog.open() view.document = (status === PdfDocument.Ready ? document : undefined) diff --git a/examples/pdf/multipage/viewer.qrc b/examples/pdf/multipage/viewer.qrc index 9698a2689..1b6fa52f7 100644 --- a/examples/pdf/multipage/viewer.qrc +++ b/examples/pdf/multipage/viewer.qrc @@ -1,6 +1,7 @@ viewer.qml + resources/document-open.svg resources/edit-clear.svg resources/edit-copy.svg resources/go-down-search.svg @@ -9,11 +10,11 @@ resources/go-up-search.svg resources/rotate-left.svg resources/rotate-right.svg + resources/test.pdf resources/zoom-in.svg resources/zoom-fit-best.svg resources/zoom-fit-width.svg resources/zoom-original.svg resources/zoom-out.svg - resources/document-open.svg diff --git a/examples/pdf/pdfviewer/main.cpp b/examples/pdf/pdfviewer/main.cpp index 639b11825..5f65e3061 100644 --- a/examples/pdf/pdfviewer/main.cpp +++ b/examples/pdf/pdfviewer/main.cpp @@ -64,6 +64,8 @@ int main(int argc, char* argv[]) if (app.arguments().count() > 1) { QUrl toLoad = QUrl::fromUserInput(app.arguments().at(1)); engine.rootObjects().first()->setProperty("source", toLoad); + } else { + engine.rootObjects().first()->setProperty("source", QStringLiteral("resources/test.pdf")); } return app.exec(); diff --git a/examples/pdf/pdfviewer/pdfviewer.pro b/examples/pdf/pdfviewer/pdfviewer.pro index 697349cee..b8817c9be 100644 --- a/examples/pdf/pdfviewer/pdfviewer.pro +++ b/examples/pdf/pdfviewer/pdfviewer.pro @@ -1,6 +1,6 @@ TEMPLATE = app -QT += qml quick pdf widgets +QT += qml quick pdf widgets svg SOURCES += main.cpp diff --git a/examples/pdf/pdfviewer/resources/test.pdf b/examples/pdf/pdfviewer/resources/test.pdf new file mode 100644 index 000000000..a9dc1bc29 Binary files /dev/null and b/examples/pdf/pdfviewer/resources/test.pdf differ diff --git a/examples/pdf/pdfviewer/viewer.qml b/examples/pdf/pdfviewer/viewer.qml index 586a822a0..a2bccab44 100644 --- a/examples/pdf/pdfviewer/viewer.qml +++ b/examples/pdf/pdfviewer/viewer.qml @@ -63,7 +63,7 @@ ApplicationWindow { color: "lightgrey" title: document.title visible: true - property alias source: document.source // for main.cpp + property string source // for main.cpp property real scaleStep: Math.sqrt(2) header: ToolBar { @@ -204,6 +204,7 @@ ApplicationWindow { x: searchDrawer.position * searchDrawer.width // TODO binding gets broken during centering document: PdfDocument { id: document + source: Qt.resolvedUrl(root.source) onStatusChanged: if (status === PdfDocument.Error) errorDialog.open() } searchString: searchField.text diff --git a/examples/pdf/pdfviewer/viewer.qrc b/examples/pdf/pdfviewer/viewer.qrc index 9698a2689..1b6fa52f7 100644 --- a/examples/pdf/pdfviewer/viewer.qrc +++ b/examples/pdf/pdfviewer/viewer.qrc @@ -1,6 +1,7 @@ viewer.qml + resources/document-open.svg resources/edit-clear.svg resources/edit-copy.svg resources/go-down-search.svg @@ -9,11 +10,11 @@ resources/go-up-search.svg resources/rotate-left.svg resources/rotate-right.svg + resources/test.pdf resources/zoom-in.svg resources/zoom-fit-best.svg resources/zoom-fit-width.svg resources/zoom-original.svg resources/zoom-out.svg - resources/document-open.svg diff --git a/src/pdf/qpdfdocument.cpp b/src/pdf/qpdfdocument.cpp index 42cd2492d..1e8a0f527 100644 --- a/src/pdf/qpdfdocument.cpp +++ b/src/pdf/qpdfdocument.cpp @@ -420,6 +420,8 @@ QPdfDocument::~QPdfDocument() QPdfDocument::DocumentError QPdfDocument::load(const QString &fileName) { + qCDebug(qLcDoc) << "loading" << fileName; + close(); d->setStatus(QPdfDocument::Loading); @@ -761,7 +763,7 @@ QPdfSelection QPdfDocument::getSelection(int page, QPointF start, QPointF end) return QPdfSelection(text, bounds); } - qDebug(qLcDoc) << page << start << "->" << end << "nothing found"; + qCDebug(qLcDoc) << page << start << "->" << end << "nothing found"; return QPdfSelection(); } 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 --- examples/pdf/multipage/viewer.qml | 6 +- 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 +- 5 files changed, 283 insertions(+), 168 deletions(-) diff --git a/examples/pdf/multipage/viewer.qml b/examples/pdf/multipage/viewer.qml index ac6d2cd9a..9e5f92407 100644 --- a/examples/pdf/multipage/viewer.qml +++ b/examples/pdf/multipage/viewer.qml @@ -249,6 +249,7 @@ ApplicationWindow { y: root.header.height height: view.height dim: false + clip: true ListView { id: searchResultsList anchors.fill: parent @@ -286,7 +287,7 @@ ApplicationWindow { TextField { id: searchField placeholderText: "search" - Layout.minimumWidth: 200 + Layout.minimumWidth: 150 Layout.fillWidth: true onAccepted: searchDrawer.open() Image { @@ -316,11 +317,10 @@ ApplicationWindow { } Label { id: statusLabel - Layout.fillWidth: true property size implicitPointSize: document.pagePointSize(view.currentPage) text: "page " + (currentPageSB.value) + " of " + document.pageCount + " scale " + view.renderScale.toFixed(2) + - " original size " + implicitPointSize.width.toFixed(1) + "x" + implicitPointSize.height.toFixed(1) + " pt" + " original " + implicitPointSize.width.toFixed(1) + "x" + implicitPointSize.height.toFixed(1) + " pt" visible: document.pageCount > 0 } } 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 --- examples/pdf/pdfviewer/viewer.qml | 75 ++++----- 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 + 6 files changed, 283 insertions(+), 46 deletions(-) create mode 100644 src/pdf/quick/qml/PdfScrollablePageView.qml diff --git a/examples/pdf/pdfviewer/viewer.qml b/examples/pdf/pdfviewer/viewer.qml index a2bccab44..777a9660b 100644 --- a/examples/pdf/pdfviewer/viewer.qml +++ b/examples/pdf/pdfviewer/viewer.qml @@ -80,57 +80,57 @@ ApplicationWindow { ToolButton { action: Action { shortcut: StandardKey.ZoomIn - enabled: pageView.sourceSize.width < 10000 + enabled: view.sourceSize.width < 10000 icon.source: "resources/zoom-in.svg" - onTriggered: pageView.renderScale *= root.scaleStep + onTriggered: view.renderScale *= root.scaleStep } } ToolButton { action: Action { shortcut: StandardKey.ZoomOut - enabled: pageView.sourceSize.width > 50 + enabled: view.sourceSize.width > 50 icon.source: "resources/zoom-out.svg" - onTriggered: pageView.renderScale /= root.scaleStep + onTriggered: view.renderScale /= root.scaleStep } } ToolButton { action: Action { icon.source: "resources/zoom-fit-width.svg" - onTriggered: pageView.scaleToWidth(root.contentItem.width, root.contentItem.height) + onTriggered: view.scaleToWidth(root.contentItem.width, root.contentItem.height) } } ToolButton { action: Action { icon.source: "resources/zoom-fit-best.svg" - onTriggered: pageView.scaleToPage(root.contentItem.width, root.contentItem.height) + onTriggered: view.scaleToPage(root.contentItem.width, root.contentItem.height) } } ToolButton { action: Action { shortcut: "Ctrl+0" icon.source: "resources/zoom-original.svg" - onTriggered: pageView.resetScale() + onTriggered: view.resetScale() } } ToolButton { action: Action { shortcut: "Ctrl+L" icon.source: "resources/rotate-left.svg" - onTriggered: pageView.rotation -= 90 + onTriggered: view.pageRotation -= 90 } } ToolButton { action: Action { shortcut: "Ctrl+R" icon.source: "resources/rotate-right.svg" - onTriggered: pageView.rotation += 90 + onTriggered: view.pageRotation += 90 } } ToolButton { action: Action { icon.source: "resources/go-previous-view-page.svg" - enabled: pageView.backEnabled - onTriggered: pageView.back() + enabled: view.backEnabled + onTriggered: view.back() } ToolTip.visible: enabled && hovered ToolTip.delay: 2000 @@ -141,22 +141,22 @@ ApplicationWindow { from: 1 to: document.pageCount editable: true - value: pageView.currentPage + 1 - onValueModified: pageView.goToPage(value - 1) + value: view.currentPage + 1 + onValueModified: view.goToPage(value - 1) Shortcut { sequence: StandardKey.MoveToPreviousPage - onActivated: pageView.goToPage(currentPageSB.value - 2) + onActivated: view.goToPage(currentPageSB.value - 2) } Shortcut { sequence: StandardKey.MoveToNextPage - onActivated: pageView.goToPage(currentPageSB.value) + onActivated: view.goToPage(currentPageSB.value) } } ToolButton { action: Action { icon.source: "resources/go-next-view-page.svg" - enabled: pageView.forwardEnabled - onTriggered: pageView.forward() + enabled: view.forwardEnabled + onTriggered: view.forward() } ToolTip.visible: enabled && hovered ToolTip.delay: 2000 @@ -166,8 +166,8 @@ ApplicationWindow { action: Action { shortcut: StandardKey.Copy icon.source: "resources/edit-copy.svg" - enabled: pageView.selectedText !== "" - onTriggered: pageView.copySelectionToClipboard() + enabled: view.selectedText !== "" + onTriggered: view.copySelectionToClipboard() } } Shortcut { @@ -199,9 +199,9 @@ ApplicationWindow { } } - PdfPageView { - id: pageView - x: searchDrawer.position * searchDrawer.width // TODO binding gets broken during centering + PdfScrollablePageView { + id: view + anchors.fill: parent document: PdfDocument { id: document source: Qt.resolvedUrl(root.source) @@ -210,21 +210,6 @@ ApplicationWindow { searchString: searchField.text } - WheelHandler { - rotationScale: 15 - target: pageView - property: "x" - orientation: Qt.Horizontal - acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad - } - WheelHandler { - rotationScale: 15 - target: pageView - property: "y" - orientation: Qt.Vertical - acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad - } - Drawer { id: searchDrawer edge: Qt.LeftEdge @@ -249,7 +234,7 @@ ApplicationWindow { action: Action { icon.source: "resources/go-up-search.svg" shortcut: StandardKey.FindPrevious - onTriggered: pageView.searchBack() + onTriggered: view.searchBack() } ToolTip.visible: enabled && hovered ToolTip.delay: 2000 @@ -279,7 +264,7 @@ ApplicationWindow { action: Action { icon.source: "resources/go-down-search.svg" shortcut: StandardKey.FindNext - onTriggered: pageView.searchForward() + onTriggered: view.searchForward() } ToolTip.visible: enabled && hovered ToolTip.delay: 2000 @@ -291,7 +276,7 @@ ApplicationWindow { Layout.fillWidth: true Layout.fillHeight: true clip: true - model: pageView.searchModel + model: view.searchModel ScrollBar.vertical: ScrollBar { } delegate: ItemDelegate { width: parent ? parent.width : 0 @@ -299,8 +284,8 @@ ApplicationWindow { highlighted: ListView.isCurrentItem onClicked: { searchResultsList.currentIndex = index - pageView.goToLocation(page, location, 0) - pageView.searchModel.currentResult = indexOnPage + view.goToLocation(page, location, 0) + view.searchModel.currentResult = indexOnPage } } } @@ -308,9 +293,9 @@ ApplicationWindow { } footer: Label { - property size implicitPointSize: document.pagePointSize(pageView.currentPage) - text: "page " + (pageView.currentPage + 1) + " of " + document.pageCount + - " scale " + pageView.renderScale.toFixed(2) + + property size implicitPointSize: document.pagePointSize(view.currentPage) + text: "page " + (view.currentPage + 1) + " of " + document.pageCount + + " scale " + view.renderScale.toFixed(2) + " original " + implicitPointSize.width.toFixed(1) + "x" + implicitPointSize.height.toFixed(1) + "pts" visible: document.status === PdfDocument.Ready } 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 23e2fd1e2b13f4a6587152d8e26bb5de5d75a231 Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Thu, 20 Feb 2020 14:53:32 +0100 Subject: PDF single-page example: move search field to the footer As with a2be3a7a79dc4fabe8675ea80a6ba550e0e4deec, this makes the search feature more discoverable and touch-friendly. Change-Id: I47e37273c583121d60985cc27c22f56e6d655ab0 Reviewed-by: Shawn Rutledge --- examples/pdf/pdfviewer/viewer.qml | 142 +++++++++++++++++++------------------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/examples/pdf/pdfviewer/viewer.qml b/examples/pdf/pdfviewer/viewer.qml index 777a9660b..e3bb4b474 100644 --- a/examples/pdf/pdfviewer/viewer.qml +++ b/examples/pdf/pdfviewer/viewer.qml @@ -170,6 +170,10 @@ ApplicationWindow { onTriggered: view.copySelectionToClipboard() } } + Shortcut { + sequence: StandardKey.Find + onActivated: searchField.forceActiveFocus() + } Shortcut { sequence: StandardKey.Quit onActivated: Qt.quit() @@ -214,89 +218,85 @@ ApplicationWindow { id: searchDrawer edge: Qt.LeftEdge modal: false - width: searchLayout.implicitWidth + width: 300 y: root.header.height - height: root.contentItem.height + height: view.height dim: false - Shortcut { - sequence: StandardKey.Find - onActivated: { - searchDrawer.open() - searchField.forceActiveFocus() + clip: true + ListView { + id: searchResultsList + anchors.fill: parent + anchors.margins: 2 + model: view.searchModel + ScrollBar.vertical: ScrollBar { } + delegate: ItemDelegate { + width: parent ? parent.width : 0 + text: "page " + (page + 1) + ": " + context + highlighted: ListView.isCurrentItem + onClicked: { + searchResultsList.currentIndex = index + view.goToLocation(page, location, 0) + view.searchModel.currentResult = indexOnPage + } } } - ColumnLayout { - id: searchLayout + } + + footer: ToolBar { + height: footerRow.implicitHeight + RowLayout { + id: footerRow anchors.fill: parent - anchors.margins: 2 - RowLayout { - ToolButton { - action: Action { - icon.source: "resources/go-up-search.svg" - shortcut: StandardKey.FindPrevious - onTriggered: view.searchBack() - } - ToolTip.visible: enabled && hovered - ToolTip.delay: 2000 - ToolTip.text: "find previous" + ToolButton { + action: Action { + icon.source: "resources/go-up-search.svg" + shortcut: StandardKey.FindPrevious + onTriggered: view.searchBack() } - TextField { - id: searchField - placeholderText: "search" - Layout.minimumWidth: 200 - Layout.fillWidth: true - Image { - visible: searchField.text !== "" - source: "resources/edit-clear.svg" - anchors { - right: parent.right - top: parent.top - bottom: parent.bottom - margins: 3 - rightMargin: 5 - } - TapHandler { - onTapped: searchField.clear() - } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "find previous" + } + TextField { + id: searchField + placeholderText: "search" + Layout.minimumWidth: 150 + Layout.maximumWidth: 300 + Layout.fillWidth: true + onAccepted: searchDrawer.open() + Image { + visible: searchField.text !== "" + source: "resources/edit-clear.svg" + anchors { + right: parent.right + top: parent.top + bottom: parent.bottom + margins: 3 + rightMargin: 5 } - } - ToolButton { - action: Action { - icon.source: "resources/go-down-search.svg" - shortcut: StandardKey.FindNext - onTriggered: view.searchForward() + TapHandler { + onTapped: searchField.clear() } - ToolTip.visible: enabled && hovered - ToolTip.delay: 2000 - ToolTip.text: "find next" } } - ListView { - id: searchResultsList - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - model: view.searchModel - ScrollBar.vertical: ScrollBar { } - delegate: ItemDelegate { - width: parent ? parent.width : 0 - text: "page " + (page + 1) + ": " + context - highlighted: ListView.isCurrentItem - onClicked: { - searchResultsList.currentIndex = index - view.goToLocation(page, location, 0) - view.searchModel.currentResult = indexOnPage - } + ToolButton { + action: Action { + icon.source: "resources/go-down-search.svg" + shortcut: StandardKey.FindNext + onTriggered: view.searchForward() } + ToolTip.visible: enabled && hovered + ToolTip.delay: 2000 + ToolTip.text: "find next" + } + Label { + Layout.fillWidth: true + property size implicitPointSize: document.pagePointSize(view.currentPage) + text: "page " + (view.currentPage + 1) + " of " + document.pageCount + + " scale " + view.renderScale.toFixed(2) + + " original " + implicitPointSize.width.toFixed(1) + "x" + implicitPointSize.height.toFixed(1) + "pts" + visible: document.status === PdfDocument.Ready } } } - - footer: Label { - property size implicitPointSize: document.pagePointSize(view.currentPage) - text: "page " + (view.currentPage + 1) + " of " + document.pageCount + - " scale " + view.renderScale.toFixed(2) + - " original " + implicitPointSize.width.toFixed(1) + "x" + implicitPointSize.height.toFixed(1) + "pts" - visible: document.status === PdfDocument.Ready - } } -- 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(-) 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