summaryrefslogtreecommitdiffstats
path: root/src/pdf
diff options
context:
space:
mode:
authorShawn Rutledge <shawn.rutledge@qt.io>2020-02-10 10:49:33 +0100
committerShawn Rutledge <shawn.rutledge@qt.io>2020-02-17 17:50:01 +0100
commit0b6a4d94945a975390b2574e6aff2568ebb7f061 (patch)
tree480d54e4146ee0d23dd3f2ce69877408162bb512 /src/pdf
parente5a33355798d3277c631b0024f389cdca2f2c683 (diff)
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 <shawn.rutledge@qt.io>
Diffstat (limited to 'src/pdf')
-rw-r--r--src/pdf/api/qpdfdestination.h4
-rw-r--r--src/pdf/api/qpdfdestination_p.h11
-rw-r--r--src/pdf/api/qpdfdocument.h1
-rw-r--r--src/pdf/api/qpdfsearchmodel.h30
-rw-r--r--src/pdf/api/qpdfsearchmodel_p.h (renamed from src/pdf/qpdfsearchmodel_p.h)30
-rw-r--r--src/pdf/api/qpdfsearchresult.h75
-rw-r--r--src/pdf/api/qpdfsearchresult_p.h70
-rw-r--r--src/pdf/pdfcore.pro5
-rw-r--r--src/pdf/qpdfsearchmodel.cpp229
-rw-r--r--src/pdf/qpdfsearchresult.cpp74
-rw-r--r--src/pdf/quick/qml/PdfMultiPageView.qml76
-rw-r--r--src/pdf/quick/qml/PdfPageView.qml39
-rw-r--r--src/pdf/quick/qquickpdfsearchmodel.cpp204
-rw-r--r--src/pdf/quick/qquickpdfsearchmodel_p.h36
14 files changed, 729 insertions, 155 deletions
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<QPdfDestinationPrivate> 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 <QPointF>
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 <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
index 90490d8e5..0855bc216 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 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 <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 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<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 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 <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 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<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 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 <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 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 <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)
};