diff options
Diffstat (limited to 'src/pdfquick/qquickpdfselection.cpp')
-rw-r--r-- | src/pdfquick/qquickpdfselection.cpp | 543 |
1 files changed, 543 insertions, 0 deletions
diff --git a/src/pdfquick/qquickpdfselection.cpp b/src/pdfquick/qquickpdfselection.cpp new file mode 100644 index 000000000..54a13828a --- /dev/null +++ b/src/pdfquick/qquickpdfselection.cpp @@ -0,0 +1,543 @@ +/**************************************************************************** +** +** Copyright (C) 2020 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the QtPDF module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL3$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see http://www.qt.io/terms-conditions. For further +** information use the contact form at http://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPLv3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or later as published by the Free +** Software Foundation and appearing in the file LICENSE.GPL included in +** the packaging of this file. Please review the following information to +** ensure the GNU General Public License version 2.0 requirements will be +** met: http://www.gnu.org/licenses/gpl-2.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qquickpdfselection_p.h" +#include "qquickpdfdocument_p.h" +#include <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(QStringLiteral("\\s")); + +/*! + \qmltype PdfSelection + \instantiates QQuickPdfSelection + \inqmlmodule QtQuick.Pdf + \ingroup pdf + \brief A representation of a text selection within a PDF Document. + \since 5.15 + + PdfSelection provides the text string and its geometry within a bounding box + from one point to another. + + 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(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 +{ + return m_document; +} + +void QQuickPdfSelection::setDocument(QQuickPdfDocument *document) +{ + if (m_document == document) + return; + + if (m_document) { + disconnect(m_document, &QQuickPdfDocument::sourceChanged, + this, &QQuickPdfSelection::resetPoints); + } + m_document = document; + emit documentChanged(); + resetPoints(); + connect(m_document, &QQuickPdfDocument::sourceChanged, + this, &QQuickPdfSelection::resetPoints); +} + +/*! + \qmlproperty list<list<point>> PdfSelection::geometry + + A set of paths in a form that can be bound to the \c paths property of a + \l {QtQuick::PathMultiline}{PathMultiline} instance to render a batch of + rectangles around the text regions that are included in the selection: + + \qml + PdfDocument { + id: doc + } + PdfSelection { + id: selection + document: doc + fromPoint: textSelectionDrag.centroid.pressPosition + toPoint: textSelectionDrag.centroid.position + hold: !textSelectionDrag.active + } + Shape { + ShapePath { + PathMultiline { + paths: selection.geometry + } + } + } + DragHandler { + id: textSelectionDrag + acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus + target: null + } + \endqml + + \sa PathMultiline +*/ +QList<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); + if (sel.text() != m_text) { + m_text = sel.text(); + if (QGuiApplication::clipboard()->supportsSelection()) + sel.copyToClipboard(QClipboard::Selection); + emit textChanged(); + } + + if (sel.bounds() != m_geometry) { + m_geometry = sel.bounds(); + emit 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::ImReadOnly: + 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; + m_hold = false; + setFromPoint(QPointF()); + setToPoint(QPointF()); + m_hold = wasHolding; +} + +/*! + \qmlproperty int PdfSelection::page + + The page number on which to search. + + \sa QtQuick::Image::currentFrame +*/ +int QQuickPdfSelection::page() const +{ + return m_page; +} + +void QQuickPdfSelection::setPage(int page) +{ + if (m_page == page) + return; + + m_page = page; + 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 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 +{ + return m_fromPoint; +} + +void QQuickPdfSelection::setFromPoint(QPointF fromPoint) +{ + if (m_hold || m_fromPoint == fromPoint) + return; + + m_fromPoint = fromPoint; + emit fromPointChanged(); + updateResults(); +} + +/*! + \qmlproperty point PdfSelection::toPoint + + The ending location, in 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 +{ + return m_toPoint; +} + +void QQuickPdfSelection::setToPoint(QPointF toPoint) +{ + if (m_hold || m_toPoint == toPoint) + return; + + m_toPoint = toPoint; + emit toPointChanged(); + updateResults(); +} + +/*! + \qmlproperty bool PdfSelection::hold + + Controls whether to hold the existing selection regardless of changes to + \l fromPoint and \l toPoint. This property can be set to \c true when the mouse + or touchpoint is released, so that the selection is not lost due to the + point bindings changing. +*/ +bool QQuickPdfSelection::hold() const +{ + return m_hold; +} + +void QQuickPdfSelection::setHold(bool hold) +{ + if (m_hold == hold) + return; + + m_hold = hold; + emit holdChanged(); +} + +/*! + \qmlproperty string PdfSelection::string + + The string found. +*/ +QString QQuickPdfSelection::text() const +{ + return m_text; +} + +#if QT_CONFIG(clipboard) +/*! + \qmlmethod void PdfSelection::copyToClipboard() + + Copies plain text from the \l string property to the system clipboard. +*/ +void QQuickPdfSelection::copyToClipboard() const +{ + QGuiApplication::clipboard()->setText(m_text); +} +#endif + +void QQuickPdfSelection::updateResults() +{ + if (!m_document) + return; + QPdfSelection sel = m_document->document().getSelection(m_page, + m_fromPoint / m_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()) + sel.copyToClipboard(QClipboard::Selection); + emit textChanged(); + } + + if (sel.bounds() != m_geometry) { + m_geometry = sel.bounds(); + 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); + } +} + +QT_END_NAMESPACE |