diff --git a/examples/pdf/multipage/viewer.qml b/examples/pdf/multipage/viewer.qml index 3d9cd371ad86ad531c47157a43bea97117e32777..8f102a3c112d25af5792ae67999aa6e345588c5b 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 0000000000000000000000000000000000000000..ae17ab93b5f5481b8cd19e5135fac2a8ea98987c --- /dev/null +++ b/examples/pdf/pdfviewer/resources/go-down-search.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <defs id="defs3051"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + </defs> + <path style="fill:currentColor;fill-opacity:1;stroke:none" + d="M 4.7070312 8 L 4 8.7070312 L 10.125 14.832031 L 12 16.707031 L 13.875 14.832031 L 20 8.7070312 L 19.292969 8 L 13.167969 14.125 L 12 15.292969 L 10.832031 14.125 L 4.7070312 8 z " + class="ColorScheme-Text" + /> +</svg> 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 0000000000000000000000000000000000000000..5cc1558731a56fbd07f192d703e57027ac1ace21 --- /dev/null +++ b/examples/pdf/pdfviewer/resources/go-up-search.svg @@ -0,0 +1,8 @@ +<svg height="24" width="24" xmlns="http://www.w3.org/2000/svg"> + <style type="text/css" id="current-color-scheme"> + .ColorScheme-Text { + color:#232629; + } + </style> + <path d="M4.707 16L4 15.293l8-8 8 8-.707.707L12 8.707" class="ColorScheme-Text" fill="currentColor"/> +</svg> diff --git a/examples/pdf/pdfviewer/viewer.qml b/examples/pdf/pdfviewer/viewer.qml index 095bc39859d48fc55614aab7e93d716fa4d44fb8..e94642c5eadce8807a2e158cb8ed81077125d5a8 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 fa3561caf0a857175c02157a2262294c43c32b61..9698a2689322a587a90bf65f5909d5ed708e68f3 100644 --- a/examples/pdf/pdfviewer/viewer.qrc +++ b/examples/pdf/pdfviewer/viewer.qrc @@ -3,8 +3,10 @@ <file>viewer.qml</file> <file>resources/edit-clear.svg</file> <file>resources/edit-copy.svg</file> + <file>resources/go-down-search.svg</file> <file>resources/go-next-view-page.svg</file> <file>resources/go-previous-view-page.svg</file> + <file>resources/go-up-search.svg</file> <file>resources/rotate-left.svg</file> <file>resources/rotate-right.svg</file> <file>resources/zoom-in.svg</file> diff --git a/src/pdf/api/qpdfdestination.h b/src/pdf/api/qpdfdestination.h index dc5d6314ac951eb793fdacca28dfa556c359cb08..cad0419822dd7c0f3e4f6695a78864606fdadb96 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<QPdfDestinationPrivate> d; }; diff --git a/src/pdf/api/qpdfdestination_p.h b/src/pdf/api/qpdfdestination_p.h index a5aeb804fee51620c06d71af104632bc9b519923..3520fb7951e0c934f459fb3ee58a363ab22d41a7 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 <QPointF> QT_BEGIN_NAMESPACE diff --git a/src/pdf/api/qpdfdocument.h b/src/pdf/api/qpdfdocument.h index 9d3a4bb198ba8985f4f5fccc6f85d6b454625977..40df181f436fbb2756c101e44adbce9e298b8bb5 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 02d2a20d59691dc94118bd0c6a7ab83028e36254..c8190f192f8e52d64d411eaff369eef60af3e2be 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 <QObject> +#include <QtCore/qabstractitemmodel.h> 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<QRectF> matches(int page, const QString &searchString); + QVector<QPdfSearchResult> resultsOnPage(int page) const; + QPdfSearchResult resultAtIndex(int index) const; QPdfDocument *document() const; + QString searchString() const; + + QHash<int, QByteArray> 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<QPdfSearchModelPrivate> d; + QHash<int, QByteArray> m_roleNames; + Q_DECLARE_PRIVATE(QPdfSearchModel) }; QT_END_NAMESPACE diff --git a/src/pdf/qpdfsearchmodel_p.h b/src/pdf/api/qpdfsearchmodel_p.h similarity index 72% rename from src/pdf/qpdfsearchmodel_p.h rename to src/pdf/api/qpdfsearchmodel_p.h index 90490d8e52bcd5c66f83ae656ce6259cd6cb22d6..0855bc2167b4319ff72908047034f236f6f505ad 100644 --- a/src/pdf/qpdfsearchmodel_p.h +++ b/src/pdf/api/qpdfsearchmodel_p.h @@ -37,18 +37,46 @@ #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 <private/qabstractitemmodel_p.h> #include "third_party/pdfium/public/fpdfview.h" QT_BEGIN_NAMESPACE -class QPdfSearchModelPrivate +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<bool> pagesSearched; + QVector<QVector<QPdfSearchResult>> searchResults; + int rowCountSoFar = 0; }; QT_END_NAMESPACE diff --git a/src/pdf/api/qpdfsearchresult.h b/src/pdf/api/qpdfsearchresult.h new file mode 100644 index 0000000000000000000000000000000000000000..db7af3dd9eba376edaab4db67e5dea5b5ccd044c --- /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 <QtCore/qdebug.h> +#include <QtCore/qrect.h> +#include <QtCore/qvector.h> + +QT_BEGIN_NAMESPACE + +class QPdfSearchResultPrivate; + +class Q_PDF_EXPORT QPdfSearchResult : public QPdfDestination +{ + Q_GADGET + Q_PROPERTY(QString context READ context) + Q_PROPERTY(QVector<QRectF> rectangles READ rectangles) + +public: + QPdfSearchResult(); + ~QPdfSearchResult() {} + + QString context() const; + QVector<QRectF> rectangles() const; + +private: + QPdfSearchResult(int page, QVector<QRectF> 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 0000000000000000000000000000000000000000..a0f8e44579de0703ef6487ed033345ebfcef545a --- /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<QRectF> rects, QString context) : + QPdfDestinationPrivate(page, rects.first().topLeft(), 0), + context(context), + rects(rects) {} + + QString context; + QVector<QRectF> rects; +}; + +QT_END_NAMESPACE + +#endif // QPDFSEARCHRESULT_P_H diff --git a/src/pdf/pdfcore.pro b/src/pdf/pdfcore.pro index 951b5699f01c9c5715f7e6bb65cff0b458ecd522..e723a02fd2f86388701d4ef0f2c2eb3287447daa 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 9010d76d320d2acafbda2903f67a3964eb8be821..aa19af5b128b178473299e55c03d85907be52a09 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 <QLoggingCategory> +#include <QtCore/qelapsedtimer.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/QMetaEnum> 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<QRectF> QPdfSearchModel::matches(int page, const QString &searchString) +QHash<int, QByteArray> 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<QPdfSearchModelPrivate*>(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<QPdfSearchResult> QPdfSearchModel::resultsOnPage(int page) const +{ + Q_D(const QPdfSearchModel); + const_cast<QPdfSearchModelPrivate *>(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<QPdfSearchModelPrivate*>(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<QRectF> ret; - if (searchString.isEmpty()) - return ret; FPDF_SCHHANDLE sh = FPDFText_FindStart(textPage, searchString.utf16(), 0, 0); + QVector<QPdfSearchResult> 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<QRectF> 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<ushort> 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("<b>") + searchString + QLatin1String("</b>")); + } } + 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/qpdfsearchresult.cpp b/src/pdf/qpdfsearchresult.cpp new file mode 100644 index 0000000000000000000000000000000000000000..1164a1d430cc2a96fda55151419821ab255bd803 --- /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<QRectF> rects, QString context) : + QPdfSearchResult(new QPdfSearchResultPrivate(page, rects, context)) { } + +QPdfSearchResult::QPdfSearchResult(QPdfSearchResultPrivate *d) : + QPdfDestination(static_cast<QPdfDestinationPrivate *>(d)) { } + +QString QPdfSearchResult::context() const +{ + return static_cast<QPdfSearchResultPrivate *>(d.data())->context; +} + +QVector<QRectF> QPdfSearchResult::rectangles() const +{ + return static_cast<QPdfSearchResultPrivate *>(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 bc5134267bcdb539ac7a6022f5d3a6de902ff51c..b64f4457671b7ac04264c1c81825a22c99912d50 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 d03e9dc9dca14e918c6f688799fb84214435bd24..f4d7da0af9b9ad517eb86611ad4edfd8230f37e3 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 8b0e8867354c8ec4a6d9c5cba8b4fd0a57bc6625..ec998ef0c63ef7cd0cd97ee974ac456b41727ee1 100644 --- a/src/pdf/quick/qquickpdfsearchmodel.cpp +++ b/src/pdf/quick/qquickpdfsearchmodel.cpp @@ -35,13 +35,12 @@ ****************************************************************************/ #include "qquickpdfsearchmodel_p.h" -#include <QQuickItem> -#include <QQmlEngine> -#include <QStandardPaths> -#include <private/qguiapplication_p.h> +#include <QtCore/qloggingcategory.h> 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<list<point>> PdfSearchModel::matchGeometry + \qmlproperty list<list<point>> 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<QPolygonF> QQuickPdfSearchModel::matchGeometry() const +QVector<QPolygonF> QQuickPdfSearchModel::currentResultBoundingPolygons() const { - return m_matchGeometry; + QVector<QPolygonF> ret; + const auto &results = const_cast<QQuickPdfSearchModel *>(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<list<point>> 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<QPolygonF> QQuickPdfSearchModel::currentPageBoundingPolygons() const +{ + return const_cast<QQuickPdfSearchModel *>(this)->boundingPolygonsOnPage(m_currentPage); } /*! - \qmlproperty int PdfSearchModel::page + \qmlfunction list<list<point>> 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<QPolygonF> QQuickPdfSearchModel::boundingPolygonsOnPage(int page) { - return m_page; + if (!document() || searchString().isEmpty() || page < 0 || page > document()->pageCount()) + return {}; + + updatePage(page); + + QVector<QPolygonF> 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<QRectF> m = QPdfSearchModel::matches(m_page, m_searchString); - QVector<QPolygonF> 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 82a6289d028f21938d8237e711701a0f3973e04a..3e05f80e36ba19012097cd464fa7a0c3f6233baa 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 <QVariant> +#include <QtCore/qvariant.h> #include <QtQml/qqml.h> 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<QPolygonF> 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<QPolygonF> currentPageBoundingPolygons READ currentPageBoundingPolygons NOTIFY currentPageBoundingPolygonsChanged) + Q_PROPERTY(QVector<QPolygonF> 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<QPolygonF> boundingPolygonsOnPage(int page); - QString searchString() const; - void setSearchString(QString searchString); + int currentPage() const { return m_currentPage; } + void setCurrentPage(int currentPage); - QVector<QPolygonF> matchGeometry() const; + int currentResult() const { return m_currentResult; } + void setCurrentResult(int currentResult); + + QVector<QPolygonF> currentPageBoundingPolygons() const; + QVector<QPolygonF> 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<QPolygonF> m_matchGeometry; - int m_page; + int m_currentPage = 0; + int m_currentResult = 0; + bool m_suspendSignals = false; Q_DISABLE_COPY(QQuickPdfSearchModel) };