summaryrefslogtreecommitdiffstats
path: root/src/pdf
diff options
context:
space:
mode:
authorShawn Rutledge <shawn.rutledge@qt.io>2020-03-06 17:59:32 +0100
committerShawn Rutledge <shawn.rutledge@qt.io>2020-04-30 11:06:17 +0200
commit8d13facbd88d821cc89b21f43708cc1a81ac79f3 (patch)
tree4732dd861938f974fe77366bdeab63db76e842cb /src/pdf
parent685430b66ab2830d5e0e5ebafc17294ff1ce1f48 (diff)
Support text selection handles in PDF views
Testing only on iOS so far; QtPdf doesn't work on Android because of QTBUG-83459, so we can only hope that this might perhaps be cross-platform, if only text selection handles actually existed on all platforms. As usual, text selection begins with a long-press; the iOS platform plugin intercepts that, and we get QInputMethodEvent::Cursor. There is no cursor, but we use the opportunity to do hit-testing, because the Cursor event is the only way that we receive the pixel location where the user is interacting. Then a popover menu appears, which contains Select and Select All, and either Copy or Paste depending on the qt_im_readonly property workaround. You don't get handles until you choose Select, which will select a word. That makes the popover menu disappear. You can use the toolbar button to copy to the clipboard. After that, you can drag either handle. inputMethodQuery(query, argument) is only ever called with ImCursorPosition regardless which handle is being dragged, so it doesn't make sense to change the selection there, even though that would be easy if we were given that information. Instead, the iOS platform figures out the character range for itself and sends a QInputMethodEvent::Selection event to tell us which text to select. And yet it still doesn't move the handles without being told: QGuiApplication::inputMethod()->update(Qt::ImCursorRectangle | Qt::ImAnchorRectangle) makes that happen. Then the popover menu will appear again, and now you can use the Copy function on it as an alternative way to copy text to the clipboard. By default, when the user does the initial long-press to start selecting text, the popover menu has Select, Select All, and Paste. In editable controls there is a second possible menu that normally has Cut, Copy, Paste and Delete. We are not able to enter that mode. So as a workaround, to substitute Copy instead of Paste, we set the qt_im_readonly property so that QIOSTextResponder::canPerformAction() can detect it and make the substition. Of course that won't work without the patch to 5.15; so you still get a useless Paste action on earlier Qt versions. Selecting a word via the Select popover menu item happens because iOS sends QKeySequence::MoveToPreviousWord and then QKeySequence::SelectNextWord. We spend time calling getSelectionAtIndex() twice because of that. With an actual keyboard, it should be possible to use keystrokes to extend the selection, but it doesn't seem to work yet with shift-arrows on a physical bluetooth keyboard on iOS. Select All on the popover menu works via inputMethodEvent() with attr QInputMethodEvent::Selection. Copy sends the standard copy key sequence, so keyReleaseEvent() handles it. We must rename the geometryChanged signal to selectedAreaChanged now that we're inheriting from QQuickItem, to avoid shadowing QQuickItem::geometryChanged. For this kind of text selection to work even when the rendering is scaled, it became necessary to inform PdfSelection of the rendering scale; so a renderScale property is added. Thus it is sensible (and a nice simplification in QML code) to use it for the fromPoint and toPoint properties, such that those are now expressed in pixels rather than points. Fixes: QTBUG-82441 Task-number: QTBUG-83811 Change-Id: I16ecd2db55c6a834be6139ce4f3aae23446fed54 Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
Diffstat (limited to 'src/pdf')
-rw-r--r--src/pdf/api/qpdfdocument.h2
-rw-r--r--src/pdf/api/qpdfdocument_p.h11
-rw-r--r--src/pdf/api/qpdfselection.h8
-rw-r--r--src/pdf/api/qpdfselection_p.h10
-rw-r--r--src/pdf/api/qtpdfglobal.h2
-rw-r--r--src/pdf/qpdfdocument.cpp124
-rw-r--r--src/pdf/qpdfselection.cpp34
-rw-r--r--src/pdf/quick/qml/PdfMultiPageView.qml33
-rw-r--r--src/pdf/quick/qml/PdfScrollablePageView.qml33
-rw-r--r--src/pdf/quick/qquickpdfselection.cpp287
-rw-r--r--src/pdf/quick/qquickpdfselection_p.h37
-rw-r--r--src/pdf/quick/quick.pro6
12 files changed, 517 insertions, 70 deletions
diff --git a/src/pdf/api/qpdfdocument.h b/src/pdf/api/qpdfdocument.h
index f80a7832b..54ca687fa 100644
--- a/src/pdf/api/qpdfdocument.h
+++ b/src/pdf/api/qpdfdocument.h
@@ -114,6 +114,7 @@ public:
QImage render(int page, QSize imageSize, QPdfDocumentRenderOptions options = QPdfDocumentRenderOptions());
Q_INVOKABLE QPdfSelection getSelection(int page, QPointF start, QPointF end);
+ Q_INVOKABLE QPdfSelection getSelectionAtIndex(int page, int startIndex, int maxLength);
Q_INVOKABLE QPdfSelection getAllText(int page);
Q_SIGNALS:
@@ -127,6 +128,7 @@ private:
friend class QPdfLinkModelPrivate;
friend class QPdfSearchModel;
friend class QPdfSearchModelPrivate;
+ friend class QQuickPdfSelection;
Q_PRIVATE_SLOT(d, void _q_tryLoadingWithSizeFromContentHeader())
Q_PRIVATE_SLOT(d, void _q_copyFromSequentialSourceDevice())
diff --git a/src/pdf/api/qpdfdocument_p.h b/src/pdf/api/qpdfdocument_p.h
index b69b6f19e..9a737766b 100644
--- a/src/pdf/api/qpdfdocument_p.h
+++ b/src/pdf/api/qpdfdocument_p.h
@@ -66,7 +66,7 @@ public:
QPdfMutexLocker();
};
-class QPdfDocumentPrivate: public FPDF_FILEACCESS, public FX_FILEAVAIL, public FX_DOWNLOADHINTS
+class Q_PDF_PRIVATE_EXPORT QPdfDocumentPrivate: public FPDF_FILEACCESS, public FX_FILEAVAIL, public FX_DOWNLOADHINTS
{
public:
QPdfDocumentPrivate();
@@ -106,6 +106,15 @@ public:
static void fpdf_AddSegment(struct _FX_DOWNLOADHINTS* pThis, size_t offset, size_t size);
void updateLastError();
QString getText(FPDF_TEXTPAGE textPage, int startIndex, int count);
+ QPointF getCharPosition(FPDF_TEXTPAGE textPage, double pageHeight, int charIndex);
+ QRectF getCharBox(FPDF_TEXTPAGE textPage, double pageHeight, int charIndex);
+
+ struct TextPosition {
+ QPointF position;
+ qreal height = 0;
+ int charIndex = -1;
+ };
+ TextPosition hitTest(int page, QPointF position);
};
QT_END_NAMESPACE
diff --git a/src/pdf/api/qpdfselection.h b/src/pdf/api/qpdfselection.h
index 5a6a1cddc..9d91d46c7 100644
--- a/src/pdf/api/qpdfselection.h
+++ b/src/pdf/api/qpdfselection.h
@@ -53,7 +53,10 @@ class Q_PDF_EXPORT QPdfSelection
Q_GADGET
Q_PROPERTY(bool valid READ isValid)
Q_PROPERTY(QVector<QPolygonF> bounds READ bounds)
+ Q_PROPERTY(QRectF boundingRectangle READ boundingRectangle)
Q_PROPERTY(QString text READ text)
+ Q_PROPERTY(int startIndex READ startIndex)
+ Q_PROPERTY(int endIndex READ endIndex)
public:
~QPdfSelection();
@@ -65,13 +68,16 @@ public:
bool isValid() const;
QVector<QPolygonF> bounds() const;
QString text() const;
+ QRectF boundingRectangle() const;
+ int startIndex() const;
+ int endIndex() const;
#if QT_CONFIG(clipboard)
void copyToClipboard(QClipboard::Mode mode = QClipboard::Clipboard) const;
#endif
private:
QPdfSelection();
- QPdfSelection(const QString &text, QVector<QPolygonF> bounds);
+ QPdfSelection(const QString &text, QVector<QPolygonF> bounds, QRectF boundingRect, int startIndex, int endIndex);
QPdfSelection(QPdfSelectionPrivate *d);
friend class QPdfDocument;
friend class QQuickPdfSelection;
diff --git a/src/pdf/api/qpdfselection_p.h b/src/pdf/api/qpdfselection_p.h
index 37145f7f9..0577e5a31 100644
--- a/src/pdf/api/qpdfselection_p.h
+++ b/src/pdf/api/qpdfselection_p.h
@@ -46,12 +46,18 @@ class QPdfSelectionPrivate : public QSharedData
{
public:
QPdfSelectionPrivate() = default;
- QPdfSelectionPrivate(const QString &text, QVector<QPolygonF> bounds)
+ QPdfSelectionPrivate(const QString &text, QVector<QPolygonF> bounds, QRectF boundingRect, int startIndex, int endIndex)
: text(text),
- bounds(bounds) { }
+ bounds(bounds),
+ boundingRect(boundingRect),
+ startIndex(startIndex),
+ endIndex(endIndex) { }
QString text;
QVector<QPolygonF> bounds;
+ QRectF boundingRect;
+ int startIndex;
+ int endIndex;
};
QT_END_NAMESPACE
diff --git a/src/pdf/api/qtpdfglobal.h b/src/pdf/api/qtpdfglobal.h
index 223ec4bcb..8b4b0c206 100644
--- a/src/pdf/api/qtpdfglobal.h
+++ b/src/pdf/api/qtpdfglobal.h
@@ -53,6 +53,8 @@ QT_BEGIN_NAMESPACE
# endif
#endif
+#define Q_PDF_PRIVATE_EXPORT Q_PDF_EXPORT
+
QT_END_NAMESPACE
#endif // QTPDFGLOBAL_H
diff --git a/src/pdf/qpdfdocument.cpp b/src/pdf/qpdfdocument.cpp
index 89b27da8b..7dd54a8e3 100644
--- a/src/pdf/qpdfdocument.cpp
+++ b/src/pdf/qpdfdocument.cpp
@@ -54,7 +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;
+static const double CharacterHitTolerance = 16.0;
Q_LOGGING_CATEGORY(qLcDoc, "qt.pdf.document")
QPdfMutexLocker::QPdfMutexLocker()
@@ -402,6 +402,50 @@ QString QPdfDocumentPrivate::getText(FPDF_TEXTPAGE textPage, int startIndex, int
return QString::fromUtf16(buf.constData(), len - 1);
}
+QPointF QPdfDocumentPrivate::getCharPosition(FPDF_TEXTPAGE textPage, double pageHeight, int charIndex)
+{
+ double x, y;
+ int count = FPDFText_CountChars(textPage);
+ bool ok = FPDFText_GetCharOrigin(textPage, qMin(count - 1, charIndex), &x, &y);
+ if (!ok)
+ return QPointF();
+ return QPointF(x, pageHeight - y);
+}
+
+QRectF QPdfDocumentPrivate::getCharBox(FPDF_TEXTPAGE textPage, double pageHeight, int charIndex)
+{
+ double l, t, r, b;
+ bool ok = FPDFText_GetCharBox(textPage, charIndex, &l, &r, &b, &t);
+ if (!ok)
+ return QRectF();
+ return QRectF(l, pageHeight - t, r - l, t - b);
+}
+
+QPdfDocumentPrivate::TextPosition QPdfDocumentPrivate::hitTest(int page, QPointF position)
+{
+ const QPdfMutexLocker lock;
+ FPDF_PAGE pdfPage = FPDF_LoadPage(doc, page);
+ double pageHeight = FPDF_GetPageHeight(pdfPage);
+ FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage);
+ int hitIndex = FPDFText_GetCharIndexAtPos(textPage, position.x(), pageHeight - position.y(),
+ CharacterHitTolerance, CharacterHitTolerance);
+ if (hitIndex >= 0) {
+ QPointF charPos = getCharPosition(textPage, pageHeight, hitIndex);
+ if (!charPos.isNull()) {
+ QRectF charBox = getCharBox(textPage, pageHeight, hitIndex);
+ // If the given position is past the end of the line, i.e. if the right edge of the found character's
+ // bounding box is closer to it than the left edge is, we say that we "hit" the next character index after
+ if (qAbs(charBox.right() - position.x()) < qAbs(charPos.x() - position.x())) {
+ charPos.setX(charBox.right());
+ ++hitIndex;
+ }
+ qCDebug(qLcDoc) << "on page" << page << "@" << position << "got char position" << charPos << "index" << hitIndex;
+ return { charPos, charBox.height(), hitIndex };
+ }
+ }
+ return {};
+}
+
/*!
\class QPdfDocument
\since 5.10
@@ -751,26 +795,70 @@ QPdfSelection QPdfDocument::getSelection(int page, QPointF start, QPointF end)
int count = endIndex - startIndex + 1;
QString text = d->getText(textPage, startIndex, count);
QVector<QPolygonF> bounds;
+ QRectF hull;
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;
+ QRectF rect(l, pageHeight - t, r - l, t - b);
+ if (hull.isNull())
+ hull = rect;
+ else
+ hull = hull.united(rect);
+ bounds << QPolygonF(rect);
}
qCDebug(qLcDoc) << page << start << "->" << end << "found" << startIndex << "->" << endIndex << text;
- return QPdfSelection(text, bounds);
+ return QPdfSelection(text, bounds, hull, startIndex, endIndex);
}
qCDebug(qLcDoc) << page << start << "->" << end << "nothing found";
return QPdfSelection();
}
+/*!
+ Returns information about the text on the given \a page that can be found
+ beginning at the given \a startIndex with at most \l maxLength characters.
+*/
+QPdfSelection QPdfDocument::getSelectionAtIndex(int page, int startIndex, int maxLength)
+{
+
+ if (page < 0 || startIndex < 0 || maxLength < 0)
+ return {};
+ const QPdfMutexLocker lock;
+ FPDF_PAGE pdfPage = FPDF_LoadPage(d->doc, page);
+ double pageHeight = FPDF_GetPageHeight(pdfPage);
+ FPDF_TEXTPAGE textPage = FPDFText_LoadPage(pdfPage);
+ int pageCount = FPDFText_CountChars(textPage);
+ if (startIndex >= pageCount)
+ return QPdfSelection();
+ QVector<QPolygonF> bounds;
+ QRectF hull;
+ int rectCount = 0;
+ QString text;
+ if (maxLength > 0) {
+ text = d->getText(textPage, startIndex, maxLength);
+ rectCount = FPDFText_CountRects(textPage, startIndex, text.length());
+ for (int i = 0; i < rectCount; ++i) {
+ double l, r, b, t;
+ FPDFText_GetRect(textPage, i, &l, &t, &r, &b);
+ QRectF rect(l, pageHeight - t, r - l, t - b);
+ if (hull.isNull())
+ hull = rect;
+ else
+ hull = hull.united(rect);
+ bounds << QPolygonF(rect);
+ }
+ }
+ if (bounds.isEmpty())
+ hull = QRectF(d->getCharPosition(textPage, pageHeight, startIndex), QSizeF());
+ qCDebug(qLcDoc) << "on page" << page << "at index" << startIndex << "maxLength" << maxLength
+ << "got" << text.length() << "chars," << rectCount << "rects within" << hull;
+ return QPdfSelection(text, bounds, hull, startIndex, startIndex + text.length());
+}
+
+/*!
+ Returns all the text and its bounds on the given \a page.
+*/
QPdfSelection QPdfDocument::getAllText(int page)
{
const QPdfMutexLocker lock;
@@ -782,20 +870,20 @@ QPdfSelection QPdfDocument::getAllText(int page)
return QPdfSelection();
QString text = d->getText(textPage, 0, count);
QVector<QPolygonF> bounds;
+ QRectF hull;
int rectCount = FPDFText_CountRects(textPage, 0, count);
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;
+ QRectF rect(l, pageHeight - t, r - l, t - b);
+ if (hull.isNull())
+ hull = rect;
+ else
+ hull = hull.united(rect);
+ bounds << QPolygonF(rect);
}
- qCDebug(qLcDoc) << "on page" << page << "got" << count << "chars" << rectCount << "rects";
- return QPdfSelection(text, bounds);
+ qCDebug(qLcDoc) << "on page" << page << "got" << count << "chars," << rectCount << "rects within" << hull;
+ return QPdfSelection(text, bounds, hull, 0, count);
}
QT_END_NAMESPACE
diff --git a/src/pdf/qpdfselection.cpp b/src/pdf/qpdfselection.cpp
index e334f0fb6..5f0ee3b20 100644
--- a/src/pdf/qpdfselection.cpp
+++ b/src/pdf/qpdfselection.cpp
@@ -67,8 +67,8 @@ QPdfSelection::QPdfSelection()
\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<QPolygonF> bounds)
- : d(new QPdfSelectionPrivate(text, bounds))
+QPdfSelection::QPdfSelection(const QString &text, QVector<QPolygonF> bounds, QRectF boundingRect, int startIndex, int endIndex)
+ : d(new QPdfSelectionPrivate(text, bounds, boundingRect, startIndex, endIndex))
{
}
@@ -134,6 +134,36 @@ QString QPdfSelection::text() const
return d->text;
}
+/*!
+ \property rect QPdfSelection::boundingRectangle
+
+ This property holds the overall bounding rectangle (convex hull) around \l bounds.
+*/
+QRectF QPdfSelection::boundingRectangle() const
+{
+ return d->boundingRect;
+}
+
+/*!
+ \property int QPdfSelection::startIndex
+
+ This property holds the index at the beginning of \l text within the full text on the page.
+*/
+int QPdfSelection::startIndex() const
+{
+ return d->startIndex;
+}
+
+/*!
+ \property int QPdfSelection::endIndex
+
+ This property holds the index at the end of \l text within the full text on the page.
+*/
+int QPdfSelection::endIndex() const
+{
+ return d->endIndex;
+}
+
#if QT_CONFIG(clipboard)
/*!
Copies \l text to the \l {QGuiApplication::clipboard()}{system clipboard}.
diff --git a/src/pdf/quick/qml/PdfMultiPageView.qml b/src/pdf/quick/qml/PdfMultiPageView.qml
index 70bb5454f..14856b1e2 100644
--- a/src/pdf/quick/qml/PdfMultiPageView.qml
+++ b/src/pdf/quick/qml/PdfMultiPageView.qml
@@ -276,9 +276,17 @@ Item {
target: null
}
TapHandler {
- id: tapHandler
+ id: mouseClickHandler
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus
}
+ TapHandler {
+ id: touchTapHandler
+ acceptedDevices: PointerDevice.TouchScreen
+ onTapped: {
+ selection.clear()
+ selection.forceActiveFocus()
+ }
+ }
Repeater {
model: PdfLinkModel {
id: linkModel
@@ -320,17 +328,18 @@ Item {
}
}
}
- }
- 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
+ PdfSelection {
+ id: selection
+ anchors.fill: parent
+ document: root.document
+ page: image.currentFrame
+ renderScale: image.renderScale
+ fromPoint: textSelectionDrag.centroid.pressPosition
+ toPoint: textSelectionDrag.centroid.position
+ hold: !textSelectionDrag.active && !mouseClickHandler.pressed
+ onTextChanged: root.selectedText = text
+ focus: true
+ }
}
}
ScrollBar.vertical: ScrollBar {
diff --git a/src/pdf/quick/qml/PdfScrollablePageView.qml b/src/pdf/quick/qml/PdfScrollablePageView.qml
index 6076e57df..3a3727cc4 100644
--- a/src/pdf/quick/qml/PdfScrollablePageView.qml
+++ b/src/pdf/quick/qml/PdfScrollablePageView.qml
@@ -133,17 +133,6 @@ Flickable {
navigationStack.update(navigationStack.currentPage, currentLocation, root.renderScale)
}
- 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
@@ -246,9 +235,29 @@ Flickable {
target: null
}
TapHandler {
- id: tapHandler
+ id: mouseClickHandler
acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus
}
+ TapHandler {
+ id: touchTapHandler
+ acceptedDevices: PointerDevice.TouchScreen
+ onTapped: {
+ selection.clear()
+ selection.focus = true
+ }
+ }
+ }
+
+ PdfSelection {
+ id: selection
+ anchors.fill: parent
+ document: root.document
+ page: navigationStack.currentPage
+ renderScale: image.pageScale
+ fromPoint: textSelectionDrag.centroid.pressPosition
+ toPoint: textSelectionDrag.centroid.position
+ hold: !textSelectionDrag.active && !mouseClickHandler.pressed
+ focus: true
}
PinchHandler {
diff --git a/src/pdf/quick/qquickpdfselection.cpp b/src/pdf/quick/qquickpdfselection.cpp
index 88bc25e4c..23fbb80b9 100644
--- a/src/pdf/quick/qquickpdfselection.cpp
+++ b/src/pdf/quick/qquickpdfselection.cpp
@@ -38,12 +38,19 @@
#include "qquickpdfdocument_p.h"
#include <QClipboard>
#include <QGuiApplication>
+#include <QLoggingCategory>
#include <QQuickItem>
#include <QQmlEngine>
+#include <QRegularExpression>
#include <QStandardPaths>
+#include <QtPdf/private/qpdfdocument_p.h>
+
+Q_LOGGING_CATEGORY(qLcIm, "qt.pdf.im")
QT_BEGIN_NAMESPACE
+static const QRegularExpression WordDelimiter("\\s");
+
/*!
\qmltype PdfSelection
\instantiates QQuickPdfSelection
@@ -54,14 +61,29 @@ QT_BEGIN_NAMESPACE
PdfSelection provides the text string and its geometry within a bounding box
from one point to another.
+
+ To modify the selection using the mouse, bind \l fromPoint and \l toPoint
+ to the suitable properties of an input handler so that they will be set to
+ the positions where the drag gesture begins and ends, respectively; and
+ bind the \l hold property so that it will be set to \c true during the drag
+ gesture and \c false when the gesture ends.
+
+ PdfSelection also directly handles Input Method queries so that text
+ selection handles can be used on platforms such as iOS. For this purpose,
+ it must have keyboard focus.
*/
/*!
Constructs a SearchModel.
*/
-QQuickPdfSelection::QQuickPdfSelection(QObject *parent)
- : QObject(parent)
+QQuickPdfSelection::QQuickPdfSelection(QQuickItem *parent)
+ : QQuickItem(parent)
{
+#if QT_CONFIG(im)
+ setFlags(ItemIsFocusScope | ItemAcceptsInputMethod);
+ // workaround to get Copy instead of Paste on the popover menu (QTBUG-83811)
+ setProperty("qt_im_readonly", QVariant(true));
+#endif
}
QQuickPdfDocument *QQuickPdfSelection::document() const
@@ -124,6 +146,24 @@ QVector<QPolygonF> QQuickPdfSelection::geometry() const
return m_geometry;
}
+void QQuickPdfSelection::clear()
+{
+ m_hitPoint = QPointF();
+ m_fromPoint = QPointF();
+ m_toPoint = QPointF();
+ m_heightAtAnchor = 0;
+ m_heightAtCursor = 0;
+ m_fromCharIndex = -1;
+ m_toCharIndex = -1;
+ m_text.clear();
+ m_geometry.clear();
+ emit fromPointChanged();
+ emit toPointChanged();
+ emit textChanged();
+ emit selectedAreaChanged();
+ QGuiApplication::inputMethod()->update(Qt::ImQueryInput);
+}
+
void QQuickPdfSelection::selectAll()
{
QPdfSelection sel = m_document->m_doc.getAllText(m_page);
@@ -136,10 +176,172 @@ void QQuickPdfSelection::selectAll()
if (sel.bounds() != m_geometry) {
m_geometry = sel.bounds();
- emit geometryChanged();
+ emit selectedAreaChanged();
+ }
+#if QT_CONFIG(im)
+ m_fromCharIndex = sel.startIndex();
+ m_toCharIndex = sel.endIndex();
+ if (sel.bounds().isEmpty()) {
+ m_fromPoint = QPointF();
+ m_toPoint = QPointF();
+ } else {
+ m_fromPoint = sel.bounds().first().boundingRect().topLeft() * m_renderScale;
+ m_toPoint = sel.bounds().last().boundingRect().bottomRight() * m_renderScale - QPointF(0, m_heightAtCursor);
+ }
+
+ QGuiApplication::inputMethod()->update(Qt::ImCursorRectangle | Qt::ImAnchorRectangle);
+#endif
+}
+
+#if QT_CONFIG(im)
+void QQuickPdfSelection::keyReleaseEvent(QKeyEvent *ev)
+{
+ qCDebug(qLcIm) << "release" << ev;
+ const auto &allText = pageText();
+ if (ev == QKeySequence::MoveToPreviousWord) {
+ // iOS sends MoveToPreviousWord first to get to the beginning of the word,
+ // and then SelectNextWord to select the whole word.
+ int i = allText.lastIndexOf(WordDelimiter, m_fromCharIndex - allText.length());
+ if (i < 0)
+ i = 0;
+ else
+ i += 1; // don't select the space before the word
+ auto sel = m_document->m_doc.getSelectionAtIndex(m_page, i, m_text.length() + m_fromCharIndex - i);
+ update(sel);
+ QGuiApplication::inputMethod()->update(Qt::ImAnchorRectangle);
+ } else if (ev == QKeySequence::SelectNextWord) {
+ int i = allText.indexOf(WordDelimiter, m_toCharIndex);
+ if (i < 0)
+ i = allText.length(); // go to the end of m_textAfter
+ auto sel = m_document->m_doc.getSelectionAtIndex(m_page, m_fromCharIndex, m_text.length() + i - m_toCharIndex);
+ update(sel);
+ QGuiApplication::inputMethod()->update(Qt::ImCursorRectangle);
+ } else if (ev == QKeySequence::Copy) {
+ copyToClipboard();
+ }
+}
+
+void QQuickPdfSelection::inputMethodEvent(QInputMethodEvent *event)
+{
+ for (auto attr : event->attributes()) {
+ switch (attr.type) {
+ case QInputMethodEvent::Cursor:
+ qCDebug(qLcIm) << "QInputMethodEvent::Cursor: moved to" << attr.start << "len" << attr.length;
+ break;
+ case QInputMethodEvent::Selection: {
+ auto sel = m_document->m_doc.getSelectionAtIndex(m_page, attr.start, attr.length);
+ update(sel);
+ qCDebug(qLcIm) << "QInputMethodEvent::Selection: from" << attr.start << "len" << attr.length
+ << "result:" << m_fromCharIndex << "->" << m_toCharIndex << sel.boundingRectangle();
+ // the iOS plugin decided that it wanted to change the selection, but still has to be told to move the handles (!?)
+ QGuiApplication::inputMethod()->update(Qt::ImCursorRectangle | Qt::ImAnchorRectangle);
+ break;
+ }
+ case QInputMethodEvent::Language:
+ case QInputMethodEvent::Ruby:
+ case QInputMethodEvent::TextFormat:
+ break;
+ }
}
}
+QVariant QQuickPdfSelection::inputMethodQuery(Qt::InputMethodQuery query, const QVariant &argument) const
+{
+ if (!argument.isNull()) {
+ qCDebug(qLcIm) << "IM query" << query << "with arg" << argument;
+ if (query == Qt::ImCursorPosition) {
+ // If it didn't move since last time, return the same result.
+ if (m_hitPoint == argument.toPointF())
+ return inputMethodQuery(query);
+ m_hitPoint = argument.toPointF();
+ auto tp = m_document->m_doc.d->hitTest(m_page, m_hitPoint / m_renderScale);
+ qCDebug(qLcIm) << "ImCursorPosition hit testing in px" << m_hitPoint << "pt" << (m_hitPoint / m_renderScale)
+ << "got char index" << tp.charIndex << "@" << tp.position << "pt," << tp.position * m_renderScale << "px";
+ if (tp.charIndex >= 0) {
+ m_toCharIndex = tp.charIndex;
+ m_toPoint = tp.position * m_renderScale - QPointF(0, m_heightAtCursor);
+ m_heightAtCursor = tp.height * m_renderScale;
+ if (qFuzzyIsNull(m_heightAtAnchor))
+ m_heightAtAnchor = m_heightAtCursor;
+ }
+ }
+ }
+ return inputMethodQuery(query);
+}
+
+QVariant QQuickPdfSelection::inputMethodQuery(Qt::InputMethodQuery query) const
+{
+ QVariant ret;
+ switch (query) {
+ case Qt::ImEnabled:
+ ret = true;
+ break;
+ case Qt::ImHints:
+ ret = QVariant(Qt::ImhMultiLine | Qt::ImhNoPredictiveText);
+ break;
+ case Qt::ImInputItemClipRectangle:
+ ret = boundingRect();
+ break;
+ case Qt::ImAnchorPosition:
+ ret = m_fromCharIndex;
+ break;
+ case Qt::ImAbsolutePosition:
+ ret = m_toCharIndex;
+ break;
+ case Qt::ImCursorPosition:
+ ret = m_toCharIndex;
+ break;
+ case Qt::ImAnchorRectangle:
+ ret = QRectF(m_fromPoint, QSizeF(1, m_heightAtAnchor));
+ break;
+ case Qt::ImCursorRectangle:
+ ret = QRectF(m_toPoint, QSizeF(1, m_heightAtCursor));
+ break;
+ case Qt::ImSurroundingText:
+ ret = QVariant(pageText());
+ break;
+ case Qt::ImTextBeforeCursor:
+ ret = QVariant(pageText().mid(0, m_toCharIndex));
+ break;
+ case Qt::ImTextAfterCursor:
+ ret = QVariant(pageText().mid(m_toCharIndex));
+ break;
+ case Qt::ImCurrentSelection:
+ ret = QVariant(m_text);
+ break;
+ case Qt::ImEnterKeyType:
+ break;
+ case Qt::ImFont: {
+ QFont font = QGuiApplication::font();
+ font.setPointSizeF(m_heightAtCursor);
+ ret = font;
+ break;
+ }
+ case Qt::ImMaximumTextLength:
+ break;
+ case Qt::ImPreferredLanguage:
+ break;
+ case Qt::ImPlatformData:
+ break;
+ case Qt::ImQueryInput:
+ case Qt::ImQueryAll:
+ qWarning() << "unexpected composite query";
+ break;
+ }
+ qCDebug(qLcIm) << "IM query" << query << "returns" << ret;
+ return ret;
+}
+#endif // QT_CONFIG(im)
+
+const QString &QQuickPdfSelection::pageText() const
+{
+ if (m_pageTextDirty) {
+ m_pageText = m_document->m_doc.getAllText(m_page).text();
+ m_pageTextDirty = false;
+ }
+ return m_pageText;
+}
+
void QQuickPdfSelection::resetPoints()
{
bool wasHolding = m_hold;
@@ -167,18 +369,42 @@ void QQuickPdfSelection::setPage(int page)
return;
m_page = page;
+ m_pageTextDirty = true;
emit pageChanged();
resetPoints();
}
/*!
+ \qmlproperty real PdfSelection::renderScale
+ \brief The ratio from points to pixels at which the page is rendered.
+
+ This is used to scale \l fromPoint and \l toPoint to find ranges of
+ selected characters in the document, because positions within the document
+ are always given in points.
+*/
+qreal QQuickPdfSelection::renderScale() const
+{
+ return m_renderScale;
+}
+
+void QQuickPdfSelection::setRenderScale(qreal scale)
+{
+ if (qFuzzyCompare(scale, m_renderScale))
+ return;
+
+ m_renderScale = scale;
+ emit renderScaleChanged();
+ updateResults();
+}
+
+/*!
\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.
+ The beginning location, in pixels from the upper-left corner of the page,
+ from which to find selected text. This can be bound to 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
{
@@ -198,11 +424,10 @@ void QQuickPdfSelection::setFromPoint(QPointF fromPoint)
/*!
\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.
+ The ending location, in pixels from the upper-left corner of the page,
+ from which to find selected text. This can be bound to 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
{
@@ -267,7 +492,13 @@ void QQuickPdfSelection::updateResults()
{
if (!m_document)
return;
- QPdfSelection sel = m_document->document().getSelection(m_page, m_fromPoint, m_toPoint);
+ QPdfSelection sel = m_document->document().getSelection(m_page,
+ m_fromPoint / m_renderScale, m_toPoint / m_renderScale);
+ update(sel, true);
+}
+
+void QQuickPdfSelection::update(const QPdfSelection &sel, bool textAndGeometryOnly)
+{
if (sel.text() != m_text) {
m_text = sel.text();
if (QGuiApplication::clipboard()->supportsSelection())
@@ -277,7 +508,33 @@ void QQuickPdfSelection::updateResults()
if (sel.bounds() != m_geometry) {
m_geometry = sel.bounds();
- emit geometryChanged();
+ emit selectedAreaChanged();
+ }
+
+ if (textAndGeometryOnly)
+ return;
+
+ m_fromCharIndex = sel.startIndex();
+ m_toCharIndex = sel.endIndex();
+ if (sel.bounds().isEmpty()) {
+ m_fromPoint = sel.boundingRectangle().topLeft() * m_renderScale;
+ m_toPoint = m_fromPoint;
+ } else {
+ Qt::InputMethodQueries toUpdate = {};
+ QRectF firstLineBounds = sel.bounds().first().boundingRect();
+ m_fromPoint = firstLineBounds.topLeft() * m_renderScale;
+ if (!qFuzzyCompare(m_heightAtAnchor, firstLineBounds.height())) {
+ m_heightAtAnchor = firstLineBounds.height() * m_renderScale;
+ toUpdate.setFlag(Qt::ImAnchorRectangle);
+ }
+ QRectF lastLineBounds = sel.bounds().last().boundingRect();
+ if (!qFuzzyCompare(m_heightAtCursor, lastLineBounds.height())) {
+ m_heightAtCursor = lastLineBounds.height() * m_renderScale;
+ toUpdate.setFlag(Qt::ImCursorRectangle);
+ }
+ m_toPoint = lastLineBounds.topRight() * m_renderScale;
+ if (toUpdate)
+ QGuiApplication::inputMethod()->update(toUpdate);
}
}
diff --git a/src/pdf/quick/qquickpdfselection_p.h b/src/pdf/quick/qquickpdfselection_p.h
index bb4a50fed..80b363848 100644
--- a/src/pdf/quick/qquickpdfselection_p.h
+++ b/src/pdf/quick/qquickpdfselection_p.h
@@ -52,30 +52,35 @@
#include <QPolygonF>
#include <QVariant>
#include <QtQml/qqml.h>
+#include <QtQuick/qquickitem.h>
QT_BEGIN_NAMESPACE
+class QPdfSelection;
class QQuickPdfDocument;
-class QQuickPdfSelection : public QObject
+class QQuickPdfSelection : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(QQuickPdfDocument *document READ document WRITE setDocument NOTIFY documentChanged)
Q_PROPERTY(int page READ page WRITE setPage NOTIFY pageChanged)
+ Q_PROPERTY(qreal renderScale READ renderScale WRITE setRenderScale NOTIFY renderScaleChanged)
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<QPolygonF> geometry READ geometry NOTIFY geometryChanged)
+ Q_PROPERTY(QVector<QPolygonF> geometry READ geometry NOTIFY selectedAreaChanged)
public:
- explicit QQuickPdfSelection(QObject *parent = nullptr);
+ explicit QQuickPdfSelection(QQuickItem *parent = nullptr);
QQuickPdfDocument *document() const;
void setDocument(QQuickPdfDocument * document);
int page() const;
void setPage(int page);
+ qreal renderScale() const;
+ void setRenderScale(qreal scale);
QPointF fromPoint() const;
void setFromPoint(QPointF fromPoint);
QPointF toPoint() const;
@@ -86,6 +91,7 @@ public:
QString text() const;
QVector<QPolygonF> geometry() const;
+ Q_INVOKABLE void clear();
Q_INVOKABLE void selectAll();
#if QT_CONFIG(clipboard)
Q_INVOKABLE void copyToClipboard() const;
@@ -94,24 +100,43 @@ public:
signals:
void documentChanged();
void pageChanged();
+ void renderScaleChanged();
void fromPointChanged();
void toPointChanged();
void holdChanged();
void textChanged();
- void geometryChanged();
+ void selectedAreaChanged();
+
+protected:
+#if QT_CONFIG(im)
+ void keyReleaseEvent(QKeyEvent *ev) override;
+ void inputMethodEvent(QInputMethodEvent *event) override;
+ Q_INVOKABLE QVariant inputMethodQuery(Qt::InputMethodQuery query, const QVariant &argument) const;
+ QVariant inputMethodQuery(Qt::InputMethodQuery query) const override;
+#endif
private:
void resetPoints();
void updateResults();
+ void update(const QPdfSelection &sel, bool textAndGeometryOnly = false);
+ const QString &pageText() const;
private:
QQuickPdfDocument *m_document = nullptr;
+ mutable QPointF m_hitPoint;
QPointF m_fromPoint;
- QPointF m_toPoint;
- QString m_text;
+ mutable QPointF m_toPoint;
+ qreal m_renderScale = 1;
+ mutable qreal m_heightAtAnchor = 0;
+ mutable qreal m_heightAtCursor = 0;
+ QString m_text; // selected text
+ mutable QString m_pageText; // all text on the page
QVector<QPolygonF> m_geometry;
int m_page = 0;
+ int m_fromCharIndex = -1; // same as anchor position
+ mutable int m_toCharIndex = -1; // same as cursor position
bool m_hold = false;
+ mutable bool m_pageTextDirty = true;
Q_DISABLE_COPY(QQuickPdfSelection)
};
diff --git a/src/pdf/quick/quick.pro b/src/pdf/quick/quick.pro
index f677346fc..ab9a439df 100644
--- a/src/pdf/quick/quick.pro
+++ b/src/pdf/quick/quick.pro
@@ -3,6 +3,10 @@ TARGET = pdfplugin
TARGETPATH = QtQuick/Pdf
IMPORT_VERSION = 1.0
+# qpdfdocument_p.h includes pdfium headers which we must find in order to use private API
+CHROMIUM_SRC_DIR = $$QTWEBENGINE_ROOT/$$getChromiumSrcDir()
+INCLUDEPATH += $$CHROMIUM_SRC_DIR
+
#QMAKE_DOCS = $$PWD/doc/qtquickpdf.qdocconf
PDF_QML_FILES = \
@@ -29,6 +33,6 @@ HEADERS += \
qquickpdfsearchmodel_p.h \
qquickpdfselection_p.h \
-QT += pdf gui core qml quick
+QT += pdf pdf-private gui core qml quick
load(qml_plugin)