diff options
author | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-05-14 10:49:00 +0200 |
---|---|---|
committer | Allan Sandfeld Jensen <allan.jensen@qt.io> | 2020-05-15 17:26:19 +0200 |
commit | bc6df3888128e3a0e0d4e2f8a69970ac36d8abe7 (patch) | |
tree | 2cb49ad5fffa0f2011b3d98faa363f105135901e /src/pdf | |
parent | 10e66c6dd0b8a8dd17252d6408c13b689fac6995 (diff) | |
parent | 585da6f74012bd09e8a873080e368cff99c97cbf (diff) |
Merge remote-tracking branch 'origin/5.15' into dev
Conflicts:
src/pdf/quick/qquickpdfselection_p.h
Change-Id: I6eec37a01347c2d47cbfc1114326dfc6b58719ff
Diffstat (limited to 'src/pdf')
22 files changed, 958 insertions, 130 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/qpdflinkmodel_p_p.h b/src/pdf/api/qpdflinkmodel_p_p.h index 3e44f1651..0454d6755 100644 --- a/src/pdf/api/qpdflinkmodel_p_p.h +++ b/src/pdf/api/qpdflinkmodel_p_p.h @@ -74,7 +74,7 @@ public: // destination inside PDF int page = -1; // -1 means look at the url instead QPointF location; - qreal zoom = 1; + qreal zoom = 0; // 0 means no specified zoom: don't change when clicking // web destination QUrl url; 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..e4ec363ce 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 @@ -748,29 +792,80 @@ QPdfSelection QPdfDocument::getSelection(int page, QPointF start, QPointF end) if (startIndex >= 0 && endIndex != startIndex) { if (startIndex > endIndex) qSwap(startIndex, endIndex); - int count = endIndex - startIndex + 1; + + // If the given end position is past the end of the line, i.e. if the right edge of the last character's + // bounding box is closer to it than the left edge is, then extend the char range by one + QRectF endCharBox = d->getCharBox(textPage, pageHeight, endIndex); + if (qAbs(endCharBox.right() - end.x()) < qAbs(endCharBox.x() - end.x())) + ++endIndex; + + int count = endIndex - startIndex; 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 +877,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/plugin.cpp b/src/pdf/quick/plugin.cpp index 670fe0bf9..b082fcb4a 100644 --- a/src/pdf/quick/plugin.cpp +++ b/src/pdf/quick/plugin.cpp @@ -43,6 +43,7 @@ #include "qquickpdfnavigationstack_p.h" #include "qquickpdfsearchmodel_p.h" #include "qquickpdfselection_p.h" +#include "qquicktableviewextra_p.h" QT_BEGIN_NAMESPACE @@ -89,6 +90,7 @@ public: qmlRegisterType<QQuickPdfNavigationStack>(uri, 5, 15, "PdfNavigationStack"); qmlRegisterType<QQuickPdfSearchModel>(uri, 5, 15, "PdfSearchModel"); qmlRegisterType<QQuickPdfSelection>(uri, 5, 15, "PdfSelection"); + qmlRegisterType<QQuickTableViewExtra>(uri, 5, 15, "TableViewExtra"); qmlRegisterType(QUrl("qrc:/qt-project.org/qtpdf/qml/PdfPageView.qml"), uri, 5, 15, "PdfPageView"); qmlRegisterType(QUrl("qrc:/qt-project.org/qtpdf/qml/PdfMultiPageView.qml"), uri, 5, 15, "PdfMultiPageView"); diff --git a/src/pdf/quick/qml/PdfMultiPageView.qml b/src/pdf/quick/qml/PdfMultiPageView.qml index 70bb5454f..71485c214 100644 --- a/src/pdf/quick/qml/PdfMultiPageView.qml +++ b/src/pdf/quick/qml/PdfMultiPageView.qml @@ -48,15 +48,15 @@ Item { property string selectedText function selectAll() { - var currentItem = tableView.itemAtPos(0, tableView.contentY + root.height / 2) - if (currentItem !== null) + var currentItem = tableHelper.itemAtCell(tableHelper.cellAtPos(root.width / 2, root.height / 2)) + if (currentItem) currentItem.selection.selectAll() } function copySelectionToClipboard() { - var currentItem = tableView.itemAtPos(0, tableView.contentY + root.height / 2) + var currentItem = tableHelper.itemAtCell(tableHelper.cellAtPos(root.width / 2, root.height / 2)) if (debug) console.log("currentItem", currentItem, "sel", currentItem.selection.text) - if (currentItem !== null) + if (currentItem) currentItem.selection.copyToClipboard() } @@ -69,14 +69,19 @@ Item { function goToPage(page) { if (page === navigationStack.currentPage) return - goToLocation(page, Qt.point(0, 0), 0) + goToLocation(page, Qt.point(-1, -1), 0) } function goToLocation(page, location, zoom) { - if (zoom > 0) + if (zoom > 0) { + navigationStack.jumping = true // don't call navigationStack.update() because we will push() instead root.renderScale = zoom - navigationStack.push(page, location, zoom) - searchModel.currentPage = page + tableView.forceLayout() // but do ensure that the table layout is correct before we try to jump + navigationStack.jumping = false + } + navigationStack.push(page, location, zoom) // actually jump } + property vector2d jumpLocationMargin: Qt.vector2d(10, 10) // px from top-left corner + property int currentPageRenderingStatus: Image.Null // page scaling property real renderScale: 1 @@ -115,29 +120,27 @@ Item { id: tableView anchors.fill: parent anchors.leftMargin: 2 - model: root.document === undefined ? 0 : root.document.pageCount + model: modelInUse && root.document !== undefined ? root.document.pageCount : 0 + // workaround to make TableView do scheduleRebuildTable(RebuildOption::All) in cases when forceLayout() doesn't + property bool modelInUse: true + function rebuild() { + modelInUse = false + modelInUse = true + } + // end workaround rowSpacing: 6 property real rotationNorm: Math.round((360 + (root.pageRotation % 360)) % 360) property bool rot90: rotationNorm == 90 || rotationNorm == 270 onRot90Changed: forceLayout() property size firstPagePointSize: document === undefined ? Qt.size(0, 0) : document.pagePointSize(0) - contentWidth: document === undefined ? 0 : (rot90 ? document.maxPageHeight : document.maxPageWidth) * root.renderScale + vscroll.width + 2 - // workaround for missing function (see https://codereview.qt-project.org/c/qt/qtdeclarative/+/248464) - function itemAtPos(x, y, includeSpacing) { - // we don't care about x (assume col 0), and assume includeSpacing is true - var ret = null - for (var i = 0; i < contentItem.children.length; ++i) { - var child = contentItem.children[i]; - if (root.debug) - console.log(child, "@y", child.y) - if (child.y < y && (!ret || child.y > ret.y)) - ret = child - } - if (root.debug && ret !== null) - console.log("given y", y, "found", ret, "@", ret.y) - return ret // the delegate with the largest y that is less than the given y - } + property real pageHolderWidth: Math.max(root.width, document === undefined ? 0 : + (rot90 ? document.maxPageHeight : document.maxPageWidth) * root.renderScale) + contentWidth: document === undefined ? 0 : pageHolderWidth + vscroll.width + 2 rowHeightProvider: function(row) { return (rot90 ? document.pagePointSize(row).width : document.pagePointSize(row).height) * root.renderScale } + TableViewExtra { + id: tableHelper + tableView: tableView + } delegate: Rectangle { id: pageHolder color: root.debug ? "beige" : "transparent" @@ -147,11 +150,8 @@ Item { rotation: -90; text: pageHolder.width.toFixed(1) + "x" + pageHolder.height.toFixed(1) + "\n" + image.width.toFixed(1) + "x" + image.height.toFixed(1) } - implicitWidth: Math.max(root.width, (tableView.rot90 ? document.maxPageHeight : document.maxPageWidth) * root.renderScale) + implicitWidth: tableView.pageHolderWidth implicitHeight: tableView.rot90 ? image.width : image.height - onImplicitWidthChanged: tableView.forceLayout() - objectName: "page " + index - property int delegateIndex: row // expose the context property for JS outside of the delegate property alias selection: selection Rectangle { id: paper @@ -177,6 +177,10 @@ Item { paper.scale = 1 searchHighlights.update() } + onStatusChanged: { + if (index === navigationStack.currentPage) + root.currentPageRenderingStatus = status + } } Shape { anchors.fill: parent @@ -276,9 +280,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 @@ -290,6 +302,7 @@ Item { y: rect.y * paper.pageScale width: rect.width * paper.pageScale height: rect.height * paper.pageScale + visible: image.status === Image.Ready ShapePath { strokeWidth: style.linkUnderscoreStrokeWidth strokeColor: style.linkUnderscoreColor @@ -320,17 +333,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 { @@ -338,42 +352,83 @@ Item { property bool moved: false onPositionChanged: moved = true onActiveChanged: { - var currentItem = tableView.itemAtPos(0, tableView.contentY + root.height / 2) - var currentPage = currentItem.delegateIndex - var currentLocation = Qt.point((tableView.contentX - currentItem.x + root.width / 2) / root.renderScale, - (tableView.contentY - currentItem.y + root.height / 2) / root.renderScale) + var cell = tableHelper.cellAtPos(root.width / 2, root.height / 2) + var currentItem = tableHelper.itemAtCell(cell) + var currentLocation = Qt.point(0, 0) + if (currentItem) { // maybe the delegate wasn't loaded yet + currentLocation = Qt.point((tableView.contentX - currentItem.x + jumpLocationMargin.x) / root.renderScale, + (tableView.contentY - currentItem.y + jumpLocationMargin.y) / root.renderScale) + } if (active) { moved = false - navigationStack.push(currentPage, currentLocation, root.renderScale) + // emitJumped false to avoid interrupting a pinch if TableView thinks it should scroll at the same time + navigationStack.push(cell.y, currentLocation, root.renderScale, false) } else if (moved) { - navigationStack.update(currentPage, currentLocation, root.renderScale) + navigationStack.update(cell.y, currentLocation, root.renderScale) } } } ScrollBar.horizontal: ScrollBar { } } onRenderScaleChanged: { - tableView.forceLayout() - var currentItem = tableView.itemAtPos(tableView.contentX + root.width / 2, tableView.contentY + root.height / 2) - if (currentItem !== undefined) - navigationStack.update(currentItem.delegateIndex, Qt.point(currentItem.x / renderScale, currentItem.y / renderScale), renderScale) + // if navigationStack.jumped changes the scale, don't turn around and update the stack again; + // and don't force layout either, because positionViewAtCell() will do that + if (navigationStack.jumping) + return + // make TableView rebuild from scratch, because otherwise it doesn't know the delegates are changing size + tableView.rebuild() + var cell = tableHelper.cellAtPos(root.width / 2, root.height / 2) + var currentItem = tableHelper.itemAtCell(cell) + if (currentItem) { + var currentLocation = Qt.point((tableView.contentX - currentItem.x + jumpLocationMargin.x) / root.renderScale, + (tableView.contentY - currentItem.y + jumpLocationMargin.y) / root.renderScale) + navigationStack.update(cell.y, currentLocation, renderScale) + } } PdfNavigationStack { id: navigationStack + property bool jumping: false + property int previousPage: 0 onJumped: { + jumping = true root.renderScale = zoom - tableView.contentX = Math.max(0, location.x - root.width / 2) * root.renderScale - tableView.contentY = tableView.originY + root.document.heightSumBeforePage(page, tableView.rowSpacing / root.renderScale) * root.renderScale - if (root.debug) { - console.log("going to page", page, - "@y", root.document.heightSumBeforePage(page, tableView.rowSpacing / root.renderScale) * root.renderScale, - "ended up @", tableView.contentY, "originY is", tableView.originY) + if (location.y < 0) { + // invalid to indicate that a specific location was not needed, + // so attempt to position the new page just as the current page is + var currentYOffset = 0 + var previousPageDelegate = tableHelper.itemAtCell(0, previousPage) + if (previousPageDelegate) + currentYOffset = tableView.contentY - previousPageDelegate.y + tableHelper.positionViewAtRow(page, Qt.AlignTop, currentYOffset) + if (root.debug) { + console.log("going from page", previousPage, "to", page, "offset", currentYOffset, + "ended up @", tableView.contentX.toFixed(1) + ", " + tableView.contentY.toFixed(1)) + } + } else { + // jump to a page and position the given location relative to the top-left corner of the viewport + var pageSize = root.document.pagePointSize(page) + pageSize.width *= root.renderScale + pageSize.height *= root.renderScale + var xOffsetLimit = Math.max(0, pageSize.width - root.width) / 2 + var offset = Qt.point(Math.max(-xOffsetLimit, Math.min(xOffsetLimit, + location.x * root.renderScale - jumpLocationMargin.x)), + Math.max(0, location.y * root.renderScale - jumpLocationMargin.y)) + tableHelper.positionViewAtCell(0, page, Qt.AlignLeft | Qt.AlignTop, offset) + if (root.debug) { + console.log("going to zoom", zoom, "loc", location, "on page", page, + "ended up @", tableView.contentX.toFixed(1) + ", " + tableView.contentY.toFixed(1)) + } } + jumping = false + previousPage = page } + onCurrentPageChanged: searchModel.currentPage = currentPage } PdfSearchModel { id: searchModel document: root.document === undefined ? null : root.document - onCurrentPageChanged: if (currentPage != navigationStack.currentPage) root.goToPage(currentPage) + // TODO maybe avoid jumping if the result is already fully visible in the viewport + onCurrentResultBoundingRectChanged: root.goToLocation(currentPage, + Qt.point(currentResultBoundingRect.x, currentResultBoundingRect.y), 0) } } diff --git a/src/pdf/quick/qml/PdfScrollablePageView.qml b/src/pdf/quick/qml/PdfScrollablePageView.qml index 6076e57df..51d9e530d 100644 --- a/src/pdf/quick/qml/PdfScrollablePageView.qml +++ b/src/pdf/quick/qml/PdfScrollablePageView.qml @@ -133,32 +133,29 @@ 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 - onCurrentPageChanged: root.goToPage(currentPage) + // TODO maybe avoid jumping if the result is already fully visible in the viewport + onCurrentResultBoundingRectChanged: root.goToLocation(currentPage, + Qt.point(currentResultBoundingRect.x, currentResultBoundingRect.y), 0) } PdfNavigationStack { id: navigationStack onJumped: { root.renderScale = zoom - root.contentX = Math.max(0, location.x * root.renderScale - root.width / 2) - root.contentY = Math.max(0, location.y * root.renderScale - root.height / 2) - if (root.debug) + var dx = Math.max(0, location.x * root.renderScale - root.width / 2) - root.contentX + var dy = Math.max(0, location.y * root.renderScale - root.height / 2) - root.contentY + // don't jump if location is in the viewport already, i.e. if the "error" between desired and actual contentX/Y is small + if (Math.abs(dx) > root.width / 3) + root.contentX += dx + if (Math.abs(dy) > root.height / 3) + root.contentY += dy + if (root.debug) { console.log("going to zoom", zoom, "loc", location, "on page", page, "ended up @", root.contentX + ", " + root.contentY) + } } onCurrentPageChanged: searchModel.currentPage = currentPage } @@ -246,9 +243,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/qquickpdfdocument.cpp b/src/pdf/quick/qquickpdfdocument.cpp index 3d5f0fa10..ab5910523 100644 --- a/src/pdf/quick/qquickpdfdocument.cpp +++ b/src/pdf/quick/qquickpdfdocument.cpp @@ -38,7 +38,6 @@ #include <QQuickItem> #include <QQmlEngine> #include <QStandardPaths> -#include <private/qguiapplication_p.h> QT_BEGIN_NAMESPACE diff --git a/src/pdf/quick/qquickpdflinkmodel.cpp b/src/pdf/quick/qquickpdflinkmodel.cpp index f2ff3fd22..4f3958337 100644 --- a/src/pdf/quick/qquickpdflinkmodel.cpp +++ b/src/pdf/quick/qquickpdflinkmodel.cpp @@ -38,7 +38,6 @@ #include <QQuickItem> #include <QQmlEngine> #include <QStandardPaths> -#include <private/qguiapplication_p.h> QT_BEGIN_NAMESPACE diff --git a/src/pdf/quick/qquickpdfnavigationstack.cpp b/src/pdf/quick/qquickpdfnavigationstack.cpp index 7ba317557..044023ef6 100644 --- a/src/pdf/quick/qquickpdfnavigationstack.cpp +++ b/src/pdf/quick/qquickpdfnavigationstack.cpp @@ -90,6 +90,8 @@ void QQuickPdfNavigationStack::forward() if (forwardAvailableWas != forwardAvailable()) emit forwardAvailableChanged(); m_changing = false; + qCDebug(qLcNav) << "forward: index" << m_currentHistoryIndex << "page" << currentPage() + << "@" << currentLocation() << "zoom" << currentZoom(); } /*! @@ -120,6 +122,8 @@ void QQuickPdfNavigationStack::back() if (!forwardAvailableWas) emit forwardAvailableChanged(); m_changing = false; + qCDebug(qLcNav) << "back: index" << m_currentHistoryIndex << "page" << currentPage() + << "@" << currentLocation() << "zoom" << currentZoom(); } /*! @@ -163,13 +167,14 @@ qreal QQuickPdfNavigationStack::currentZoom() const \qmlmethod void PdfNavigationStack::push(int page, point location, qreal zoom) Adds the given destination, consisting of \a page, \a location and \a zoom, - to the history of visited locations. + to the history of visited locations. If \a emitJumped is \c false, the + \l jumped() signal will not be emitted. If forwardAvailable is \c true, calling this function represents a branch in the timeline which causes the "future" to be lost, and therefore forwardAvailable will change to \c false. */ -void QQuickPdfNavigationStack::push(int page, QPointF location, qreal zoom) +void QQuickPdfNavigationStack::push(int page, QPointF location, qreal zoom, bool emitJumped) { if (page == currentPage() && location == currentLocation() && zoom == currentZoom()) return; @@ -192,7 +197,8 @@ void QQuickPdfNavigationStack::push(int page, QPointF location, qreal zoom) emit backAvailableChanged(); if (forwardAvailableWas) emit forwardAvailableChanged(); - emit jumped(page, location, zoom); + if (emitJumped) + emit jumped(page, location, zoom); qCDebug(qLcNav) << "push: index" << m_currentHistoryIndex << "page" << page << "@" << location << "zoom" << zoom << "-> history" << [this]() { diff --git a/src/pdf/quick/qquickpdfnavigationstack_p.h b/src/pdf/quick/qquickpdfnavigationstack_p.h index 8d7102fb1..0d88d62fd 100644 --- a/src/pdf/quick/qquickpdfnavigationstack_p.h +++ b/src/pdf/quick/qquickpdfnavigationstack_p.h @@ -67,7 +67,7 @@ class QQuickPdfNavigationStack : public QObject public: explicit QQuickPdfNavigationStack(QObject *parent = nullptr); - Q_INVOKABLE void push(int page, QPointF location, qreal zoom); + Q_INVOKABLE void push(int page, QPointF location, qreal zoom, bool emitJumped = true); Q_INVOKABLE void update(int page, QPointF location, qreal zoom); Q_INVOKABLE void forward(); Q_INVOKABLE void back(); diff --git a/src/pdf/quick/qquickpdfsearchmodel.cpp b/src/pdf/quick/qquickpdfsearchmodel.cpp index a4b457841..1f62fbad0 100644 --- a/src/pdf/quick/qquickpdfsearchmodel.cpp +++ b/src/pdf/quick/qquickpdfsearchmodel.cpp @@ -116,6 +116,29 @@ QVector<QPolygonF> QQuickPdfSearchModel::currentResultBoundingPolygons() const return ret; } +/*! + \qmlproperty point PdfSearchModel::currentResultBoundingRect + + The bounding box containing all \l currentResultBoundingPolygons. + + When this property changes, a scrollable view should automatically scroll + itself in such a way as to ensure that this region is visible; for example, + it could try to position the upper-left corner near the upper-left of its + own viewport, subject to the constraints of the scrollable area. +*/ +QRectF QQuickPdfSearchModel::currentResultBoundingRect() const +{ + QRectF ret; + const auto &results = const_cast<QQuickPdfSearchModel *>(this)->resultsOnPage(m_currentPage); + if (m_currentResult < 0 || m_currentResult >= results.count()) + return ret; + auto rects = results[m_currentResult].rectangles(); + ret = rects.takeFirst(); + for (auto rect : rects) + ret = ret.united(rect); + return ret; +} + void QQuickPdfSearchModel::onResultsChanged() { emit currentPageBoundingPolygonsChanged(); @@ -266,6 +289,7 @@ void QQuickPdfSearchModel::setCurrentResult(int currentResult) m_currentResult = currentResult; emit currentResultChanged(); emit currentResultBoundingPolygonsChanged(); + emit currentResultBoundingRectChanged(); } /*! diff --git a/src/pdf/quick/qquickpdfsearchmodel_p.h b/src/pdf/quick/qquickpdfsearchmodel_p.h index 3e05f80e3..66fc583d9 100644 --- a/src/pdf/quick/qquickpdfsearchmodel_p.h +++ b/src/pdf/quick/qquickpdfsearchmodel_p.h @@ -64,6 +64,7 @@ class QQuickPdfSearchModel : public QPdfSearchModel 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) + Q_PROPERTY(QRectF currentResultBoundingRect READ currentResultBoundingRect NOTIFY currentResultBoundingRectChanged) public: explicit QQuickPdfSearchModel(QObject *parent = nullptr); @@ -81,6 +82,7 @@ public: QVector<QPolygonF> currentPageBoundingPolygons() const; QVector<QPolygonF> currentResultBoundingPolygons() const; + QRectF currentResultBoundingRect() const; signals: void documentChanged(); @@ -88,6 +90,7 @@ signals: void currentResultChanged(); void currentPageBoundingPolygonsChanged(); void currentResultBoundingPolygonsChanged(); + void currentResultBoundingRectChanged(); private: void updateResults(); diff --git a/src/pdf/quick/qquickpdfselection.cpp b/src/pdf/quick/qquickpdfselection.cpp index 5371e85e5..23fbb80b9 100644 --- a/src/pdf/quick/qquickpdfselection.cpp +++ b/src/pdf/quick/qquickpdfselection.cpp @@ -37,13 +37,20 @@ #include "qquickpdfselection_p.h" #include "qquickpdfdocument_p.h" #include <QClipboard> +#include <QGuiApplication> +#include <QLoggingCategory> #include <QQuickItem> #include <QQmlEngine> +#include <QRegularExpression> #include <QStandardPaths> -#include <private/qguiapplication_p.h> +#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 d231c0d11..ee7e1f85f 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> #include "qquickpdfdocument_p.h" QT_BEGIN_NAMESPACE +class QPdfSelection; -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) }; @@ -119,5 +144,5 @@ private: QT_END_NAMESPACE QML_DECLARE_TYPE(QQuickPdfSelection) -\ + #endif // QQUICKPDFSELECTION_P_H diff --git a/src/pdf/quick/qquicktableviewextra.cpp b/src/pdf/quick/qquicktableviewextra.cpp new file mode 100644 index 000000000..2b59d6c6e --- /dev/null +++ b/src/pdf/quick/qquicktableviewextra.cpp @@ -0,0 +1,193 @@ +/**************************************************************************** +** +** 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 "qquicktableviewextra_p.h" +#include <QtQml> +#include <QQmlContext> + +Q_LOGGING_CATEGORY(qLcTVE, "qt.pdf.tableextra") + +QT_BEGIN_NAMESPACE + +/*! + \internal + \qmltype TableViewExtra + \instantiates QQuickTableViewExtra + \inqmlmodule QtQuick.Pdf + \ingroup pdf + \brief A helper class with missing TableView functions + \since 5.15 + + TableViewExtra provides equivalents for some functions that will be added + to TableView in Qt 6. +*/ + +QQuickTableViewExtra::QQuickTableViewExtra(QObject *parent) : QObject(parent) +{ +} + +QPoint QQuickTableViewExtra::cellAtPos(qreal x, qreal y) const +{ + QPointF position(x, y); +#if QT_VERSION >= QT_VERSION_CHECK(6,0,0) + return m_tableView->cellAtPos(position); +#else + if (!m_tableView->boundingRect().contains(position)) + return QPoint(-1, -1); + + const QQuickItem *contentItem = m_tableView->contentItem(); + + for (const QQuickItem *child : contentItem->childItems()) { + const QPointF posInChild = m_tableView->mapToItem(child, position); + if (child->boundingRect().contains(posInChild)) { + const auto context = qmlContext(child); + const int column = context->contextProperty("column").toInt(); + const int row = context->contextProperty("row").toInt(); + return QPoint(column, row); + } + } + + return QPoint(-1, -1); +#endif +} + +QQuickItem *QQuickTableViewExtra::itemAtCell(const QPoint &cell) const +{ +#if QT_VERSION >= QT_VERSION_CHECK(6,0,0) + return m_tableView->itemAtCell(cell); +#else + const QQuickItem *contentItem = m_tableView->contentItem(); + + for (QQuickItem *child : contentItem->childItems()) { + const auto context = qmlContext(child); + const int column = context->contextProperty("column").toInt(); + const int row = context->contextProperty("row").toInt(); + if (QPoint(column, row) == cell) + return child; + } + + return nullptr; +#endif +} + +void QQuickTableViewExtra::positionViewAtCell(const QPoint &cell, Qt::Alignment alignment, const QPointF &offset) +{ +#if QT_VERSION >= QT_VERSION_CHECK(6,0,0) + m_tableView->positionViewAtCell(cell, alignment, offset); +#else + // Note: this fallback implementation assumes all cells to be of the same size! + + if (cell.x() < 0 || cell.x() > m_tableView->columns() - 1) + return; + if (cell.y() < 0 || cell.y() > m_tableView->rows() - 1) + return; + + Qt::Alignment verticalAlignment = alignment & (Qt::AlignTop | Qt::AlignVCenter | Qt::AlignBottom); + Qt::Alignment horizontalAlignment = alignment & (Qt::AlignLeft | Qt::AlignHCenter | Qt::AlignRight); + + const QQuickItem *contentItem = m_tableView->contentItem(); + const QQuickItem *randomChild = contentItem->childItems().first(); + const qreal cellWidth = randomChild->width(); + const qreal cellHeight = randomChild->height(); + + if (!verticalAlignment && !horizontalAlignment) { + qmlWarning(this) << "No valid alignment specified"; + return; + } + + if (horizontalAlignment) { + qreal newPosX = 0; + const qreal columnPosLeft = int(cell.x() * (cellWidth + m_tableView->columnSpacing())); + m_tableView->setContentX(0); + m_tableView->forceLayout(); + m_tableView->setContentX(columnPosLeft); + m_tableView->forceLayout(); + + switch (horizontalAlignment) { + case Qt::AlignLeft: + newPosX = m_tableView->contentX() + offset.x(); + break; + case Qt::AlignHCenter: + newPosX = m_tableView->contentX() + - m_tableView->width() / 2 + + (cellWidth / 2) + + offset.x(); + break; + case Qt::AlignRight: + newPosX = m_tableView->contentX() + - m_tableView->width() + + cellWidth + + offset.x(); + break; + } + + m_tableView->setContentX(newPosX); + m_tableView->forceLayout(); + } + + if (verticalAlignment) { + qreal newPosY = 0; + const qreal rowPosTop = int(cell.y() * (cellHeight + m_tableView->rowSpacing())); + m_tableView->setContentY(0); + m_tableView->forceLayout(); + m_tableView->setContentY(rowPosTop); + m_tableView->forceLayout(); + + switch (verticalAlignment) { + case Qt::AlignTop: + newPosY = m_tableView->contentY() + offset.y(); + break; + case Qt::AlignVCenter: + newPosY = m_tableView->contentY() + - m_tableView->height() / 2 + + (cellHeight / 2) + + offset.y(); + break; + case Qt::AlignBottom: + newPosY = m_tableView->contentY() + - m_tableView->height() + + cellHeight + + offset.y(); + break; + } + + m_tableView->setContentY(newPosY); + m_tableView->forceLayout(); + } +#endif +} + +QT_END_NAMESPACE diff --git a/src/pdf/quick/qquicktableviewextra_p.h b/src/pdf/quick/qquicktableviewextra_p.h new file mode 100644 index 000000000..11b4955a1 --- /dev/null +++ b/src/pdf/quick/qquicktableviewextra_p.h @@ -0,0 +1,92 @@ +/**************************************************************************** +** +** 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 QQUICKTABLEVIEWEXTRA_P_H +#define QQUICKTABLEVIEWEXTRA_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> +#include <QPolygonF> +#include <QVariant> +#include <QtQml/qqml.h> +#include <QtQuick/qquickitem.h> +#include <QtQuick/private/qquicktableview_p.h> + +QT_BEGIN_NAMESPACE + +class QQuickTableViewExtra : public QObject +{ + Q_OBJECT + Q_PROPERTY(QQuickTableView *tableView READ tableView WRITE setTableView) + +public: + QQuickTableViewExtra(QObject *parent = nullptr); + + QQuickTableView * tableView() const { return m_tableView; } + void setTableView(QQuickTableView * tableView) { m_tableView = tableView; } + + Q_INVOKABLE QPoint cellAtPos(qreal x, qreal y) const; + Q_INVOKABLE QQuickItem *itemAtCell(int column, int row) const { + return itemAtCell(QPoint(column, row)); + } + Q_INVOKABLE QQuickItem *itemAtCell(const QPoint &cell) const; + Q_INVOKABLE void positionViewAtCell(int column, int row, Qt::Alignment alignment, const QPointF &offset = QPointF()) { + positionViewAtCell(QPoint(column, row), alignment, offset); + } + Q_INVOKABLE void positionViewAtCell(const QPoint &cell, Qt::Alignment alignment, const QPointF &offset); + Q_INVOKABLE void positionViewAtRow(int row, Qt::Alignment alignment, qreal offset = 0) { + positionViewAtCell(QPoint(0, row), alignment & Qt::AlignVertical_Mask, QPointF(0, offset)); + } + +private: + QQuickTableView *m_tableView = nullptr; +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickTableViewExtra) + +#endif // QQUICKTABLEVIEWEXTRA_P_H diff --git a/src/pdf/quick/quick.pro b/src/pdf/quick/quick.pro index b62b80346..bd6bc8827 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 = \ @@ -21,6 +25,7 @@ SOURCES += \ qquickpdfnavigationstack.cpp \ qquickpdfsearchmodel.cpp \ qquickpdfselection.cpp \ + qquicktableviewextra.cpp \ HEADERS += \ qquickpdfdocument_p.h \ @@ -28,7 +33,8 @@ HEADERS += \ qquickpdfnavigationstack_p.h \ qquickpdfsearchmodel_p.h \ qquickpdfselection_p.h \ + qquicktableviewextra_p.h \ -QT += pdf quick-private gui gui-private core core-private qml qml-private +QT += pdf pdf-private gui core qml quick quick-private load(qml_plugin) |