Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qquickpdfselection.cpp
Go to the documentation of this file.
1// Copyright (C) 2020 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
6#include <QClipboard>
7#include <QGuiApplication>
8#include <QLoggingCategory>
9#include <QQuickItem>
10#include <QQmlEngine>
11#include <QRegularExpression>
12#include <QStandardPaths>
13#include <QtPdf/private/qpdfdocument_p.h>
14#include <QtPdf/private/qtpdfglobal_p.h>
15
16Q_PDF_LOGGING_CATEGORY(qLcIm, "qt.pdf.im")
17
18QT_BEGIN_NAMESPACE
19
20static const QRegularExpression WordDelimiter(QStringLiteral("\\s"));
21
22/*!
23 \qmltype PdfSelection
24//! \nativetype QQuickPdfSelection
25 \inqmlmodule QtQuick.Pdf
26 \ingroup pdf
27 \inherits Item
28 \brief A representation of a text selection within a PDF Document.
29 \since 5.15
30
31 PdfSelection provides the text string and its geometry within a bounding box
32 from one point to another.
33
34 To modify the selection using the mouse, bind \l from and \l to
35 to the suitable properties of an input handler so that they will be set to
36 the positions where the drag gesture begins and ends, respectively; and
37 bind the \l hold property so that it will be set to \c true during the drag
38 gesture and \c false when the gesture ends.
39
40 PdfSelection also directly handles Input Method queries so that text
41 selection handles can be used on platforms such as iOS. For this purpose,
42 it must have keyboard focus.
43*/
44
45QQuickPdfSelection::QQuickPdfSelection(QQuickItem *parent)
46 : QQuickItem(parent)
47{
48#if QT_CONFIG(im)
49 setFlags(ItemIsFocusScope | ItemAcceptsInputMethod);
50#endif
51}
52
53/*!
54 \internal
55*/
56QQuickPdfSelection::~QQuickPdfSelection() = default;
57
58/*!
59 \qmlproperty PdfDocument PdfSelection::document
60
61 This property holds the PDF document in which to select text.
62*/
63QQuickPdfDocument *QQuickPdfSelection::document() const
64{
65 return m_document;
66}
67
68void QQuickPdfSelection::setDocument(QQuickPdfDocument *document)
69{
70 if (m_document == document)
71 return;
72
73 if (m_document) {
74 disconnect(m_document, &QQuickPdfDocument::sourceChanged,
75 this, &QQuickPdfSelection::resetPoints);
76 }
77 m_document = document;
78 emit documentChanged();
79 resetPoints();
80 connect(m_document, &QQuickPdfDocument::sourceChanged,
81 this, &QQuickPdfSelection::resetPoints);
82}
83
84/*!
85 \qmlproperty list<list<point>> PdfSelection::geometry
86
87 A set of paths in a form that can be bound to the \c paths property of a
88 \l {QtQuick::PathMultiline}{PathMultiline} instance to render a batch of
89 rectangles around the text regions that are included in the selection:
90
91 \qml
92 PdfDocument {
93 id: doc
94 }
95 PdfSelection {
96 id: selection
97 document: doc
98 from: textSelectionDrag.centroid.pressPosition
99 to: textSelectionDrag.centroid.position
100 hold: !textSelectionDrag.active
101 }
102 Shape {
103 ShapePath {
104 PathMultiline {
105 paths: selection.geometry
106 }
107 }
108 }
109 DragHandler {
110 id: textSelectionDrag
111 acceptedDevices: PointerDevice.Mouse | PointerDevice.Stylus
112 target: null
113 }
114 \endqml
115
116 \sa PathMultiline
117*/
118QList<QPolygonF> QQuickPdfSelection::geometry() const
119{
120 return m_geometry;
121}
122
123/*!
124 \qmlmethod void PdfSelection::clear()
125
126 Clears the current selection.
127*/
128void QQuickPdfSelection::clear()
129{
130 m_hitPoint = QPointF();
131 m_from = QPointF();
132 m_to = QPointF();
133 m_heightAtAnchor = 0;
134 m_heightAtCursor = 0;
135 m_fromCharIndex = -1;
136 m_toCharIndex = -1;
137 m_text.clear();
138 m_geometry.clear();
139 emit fromChanged();
140 emit toChanged();
141 emit textChanged();
142 emit selectedAreaChanged();
143 QGuiApplication::inputMethod()->update(Qt::ImQueryInput);
144}
145
146/*!
147 \qmlmethod void PdfSelection::selectAll()
148
149 Selects all text on the current \l page.
150*/
151void QQuickPdfSelection::selectAll()
152{
153 if (!m_document)
154 return;
155 QPdfSelection sel = m_document->document()->getAllText(m_page);
156 if (sel.text() != m_text) {
157 m_text = sel.text();
158 if (QGuiApplication::clipboard()->supportsSelection())
159 sel.copyToClipboard(QClipboard::Selection);
160 emit textChanged();
161 }
162
163 if (sel.bounds() != m_geometry) {
164 m_geometry = sel.bounds();
165 emit selectedAreaChanged();
166 }
167#if QT_CONFIG(im)
168 m_fromCharIndex = sel.startIndex();
169 m_toCharIndex = sel.endIndex();
170 if (sel.bounds().isEmpty()) {
171 m_from = QPointF();
172 m_to = QPointF();
173 } else {
174 m_from = sel.bounds().first().boundingRect().topLeft() * m_renderScale;
175 m_to = sel.bounds().last().boundingRect().bottomRight() * m_renderScale - QPointF(0, m_heightAtCursor);
176 }
177
178 QGuiApplication::inputMethod()->update(Qt::ImCursorRectangle | Qt::ImAnchorRectangle);
179#endif
180}
181
182#if QT_CONFIG(im)
183void QQuickPdfSelection::keyReleaseEvent(QKeyEvent *ev)
184{
185 qCDebug(qLcIm) << "release" << ev;
186 const auto &allText = pageText();
187 if (ev == QKeySequence::MoveToPreviousWord) {
188 if (!m_document)
189 return;
190 // iOS sends MoveToPreviousWord first to get to the beginning of the word,
191 // and then SelectNextWord to select the whole word.
192 int i = allText.lastIndexOf(WordDelimiter, m_fromCharIndex - allText.size());
193 if (i < 0)
194 i = 0;
195 else
196 i += 1; // don't select the space before the word
197 auto sel = m_document->document()->getSelectionAtIndex(m_page, i, m_text.size() + m_fromCharIndex - i);
198 update(sel);
199 QGuiApplication::inputMethod()->update(Qt::ImAnchorRectangle);
200 } else if (ev == QKeySequence::SelectNextWord) {
201 if (!m_document)
202 return;
203 int i = allText.indexOf(WordDelimiter, m_toCharIndex);
204 if (i < 0)
205 i = allText.size(); // go to the end of m_textAfter
206 auto sel = m_document->document()->getSelectionAtIndex(m_page, m_fromCharIndex, m_text.size() + i - m_toCharIndex);
207 update(sel);
208 QGuiApplication::inputMethod()->update(Qt::ImCursorRectangle);
209 } else if (ev == QKeySequence::Copy) {
210 copyToClipboard();
211 }
212}
213
214void QQuickPdfSelection::inputMethodEvent(QInputMethodEvent *event)
215{
216 for (auto attr : event->attributes()) {
217 switch (attr.type) {
218 case QInputMethodEvent::Cursor:
219 qCDebug(qLcIm) << "QInputMethodEvent::Cursor: moved to" << attr.start << "len" << attr.length;
220 break;
221 case QInputMethodEvent::Selection: {
222 if (!m_document)
223 return;
224 auto sel = m_document->document()->getSelectionAtIndex(m_page, attr.start, attr.length);
225 update(sel);
226 qCDebug(qLcIm) << "QInputMethodEvent::Selection: from" << attr.start << "len" << attr.length
227 << "result:" << m_fromCharIndex << "->" << m_toCharIndex << sel.boundingRectangle();
228 // the iOS plugin decided that it wanted to change the selection, but still has to be told to move the handles (!?)
229 QGuiApplication::inputMethod()->update(Qt::ImCursorRectangle | Qt::ImAnchorRectangle);
230 break;
231 }
232 case QInputMethodEvent::Language:
233 case QInputMethodEvent::Ruby:
234 case QInputMethodEvent::TextFormat:
235#if QT_VERSION >= QT_VERSION_CHECK(6, 10, 0)
236 case QInputMethodEvent::MimeData:
237#endif
238 break;
239 }
240 }
241}
242
243QVariant QQuickPdfSelection::inputMethodQuery(Qt::InputMethodQuery query, const QVariant &argument) const
244{
245 if (!argument.isNull()) {
246 qCDebug(qLcIm) << "IM query" << query << "with arg" << argument;
247 if (query == Qt::ImCursorPosition) {
248 if (!m_document)
249 return {};
250 // If it didn't move since last time, return the same result.
251 if (m_hitPoint == argument.toPointF())
252 return inputMethodQuery(query);
253 m_hitPoint = argument.toPointF();
254 auto tp = m_document->document()->d->hitTest(m_page, m_hitPoint / m_renderScale);
255 qCDebug(qLcIm) << "ImCursorPosition hit testing in px" << m_hitPoint << "pt" << (m_hitPoint / m_renderScale)
256 << "got char index" << tp.charIndex << "@" << tp.position << "pt," << tp.position * m_renderScale << "px";
257 if (tp.charIndex >= 0) {
258 m_toCharIndex = tp.charIndex;
259 m_to = tp.position * m_renderScale - QPointF(0, m_heightAtCursor);
260 m_heightAtCursor = tp.height * m_renderScale;
261 if (qFuzzyIsNull(m_heightAtAnchor))
262 m_heightAtAnchor = m_heightAtCursor;
263 }
264 }
265 }
266 return inputMethodQuery(query);
267}
268
269QVariant QQuickPdfSelection::inputMethodQuery(Qt::InputMethodQuery query) const
270{
271 QVariant ret;
272 switch (query) {
273 case Qt::ImEnabled:
274 ret = true;
275 break;
276 case Qt::ImHints:
277 ret = QVariant(Qt::ImhMultiLine | Qt::ImhNoPredictiveText);
278 break;
279 case Qt::ImInputItemClipRectangle:
280 ret = boundingRect();
281 break;
282 case Qt::ImAnchorPosition:
283 ret = m_fromCharIndex;
284 break;
285 case Qt::ImAbsolutePosition:
286 ret = m_toCharIndex;
287 break;
288 case Qt::ImCursorPosition:
289 ret = m_toCharIndex;
290 break;
291 case Qt::ImAnchorRectangle:
292 ret = QRectF(m_from, QSizeF(1, m_heightAtAnchor));
293 break;
294 case Qt::ImCursorRectangle:
295 ret = QRectF(m_to, QSizeF(1, m_heightAtCursor));
296 break;
297 case Qt::ImSurroundingText:
298 ret = QVariant(pageText());
299 break;
300 case Qt::ImTextBeforeCursor:
301 ret = QVariant(pageText().mid(0, m_toCharIndex));
302 break;
303 case Qt::ImTextAfterCursor:
304 ret = QVariant(pageText().mid(m_toCharIndex));
305 break;
306 case Qt::ImCurrentSelection:
307 ret = QVariant(m_text);
308 break;
309 case Qt::ImEnterKeyType:
310 break;
311 case Qt::ImFont: {
312 QFont font = QGuiApplication::font();
313 font.setPointSizeF(m_heightAtCursor);
314 ret = font;
315 break;
316 }
317 case Qt::ImMaximumTextLength:
318 break;
319 case Qt::ImPreferredLanguage:
320 break;
321 case Qt::ImPlatformData:
322 break;
323 case Qt::ImReadOnly:
324 ret = true;
325 break;
326 case Qt::ImQueryInput:
327 case Qt::ImQueryAll:
328 qWarning() << "unexpected composite query";
329 break;
330 }
331 qCDebug(qLcIm) << "IM query" << query << "returns" << ret;
332 return ret;
333}
334#endif // QT_CONFIG(im)
335
336const QString &QQuickPdfSelection::pageText() const
337{
338 if (m_pageTextDirty) {
339 if (!m_document)
340 return m_pageText;
341 m_pageText = m_document->document()->getAllText(m_page).text();
342 m_pageTextDirty = false;
343 }
344 return m_pageText;
345}
346
347void QQuickPdfSelection::resetPoints()
348{
349 bool wasHolding = m_hold;
350 m_hold = false;
351 setFrom(QPointF());
352 setTo(QPointF());
353 m_hold = wasHolding;
354}
355
356/*!
357 \qmlproperty int PdfSelection::page
358
359 The page number on which to search.
360
361 \sa QtQuick::Image::currentFrame
362*/
363int QQuickPdfSelection::page() const
364{
365 return m_page;
366}
367
368void QQuickPdfSelection::setPage(int page)
369{
370 if (m_page == page)
371 return;
372
373 m_page = page;
374 m_pageTextDirty = true;
375 emit pageChanged();
376 resetPoints();
377}
378
379/*!
380 \qmlproperty real PdfSelection::renderScale
381 \brief The ratio from points to pixels at which the page is rendered.
382
383 This is used to scale \l from and \l to to find ranges of
384 selected characters in the document, because positions within the document
385 are always given in points.
386*/
387qreal QQuickPdfSelection::renderScale() const
388{
389 return m_renderScale;
390}
391
392void QQuickPdfSelection::setRenderScale(qreal scale)
393{
394 if (qFuzzyIsNull(scale)) {
395 qWarning() << "PdfSelection.renderScale cannot be set to 0.";
396 return;
397 }
398
399 if (qFuzzyCompare(scale, m_renderScale))
400 return;
401
402 m_renderScale = scale;
403 emit renderScaleChanged();
404 updateResults();
405}
406
407/*!
408 \qmlproperty point PdfSelection::from
409
410 The beginning location, in pixels from the upper-left corner of the page,
411 from which to find selected text. This can be bound to the
412 \c centroid.pressPosition of a \l DragHandler to begin selecting text from
413 the position where the user presses the mouse button and begins dragging,
414 for example.
415*/
416QPointF QQuickPdfSelection::from() const
417{
418 return m_from;
419}
420
421void QQuickPdfSelection::setFrom(QPointF from)
422{
423 if (m_hold || m_from == from)
424 return;
425
426 m_from = from;
427 emit fromChanged();
428 updateResults();
429}
430
431/*!
432 \qmlproperty point PdfSelection::to
433
434 The ending location, in pixels from the upper-left corner of the page,
435 from which to find selected text. This can be bound to the
436 \c centroid.position of a \l DragHandler to end selection of text at the
437 position where the user is currently dragging the mouse, for example.
438*/
439QPointF QQuickPdfSelection::to() const
440{
441 return m_to;
442}
443
444void QQuickPdfSelection::setTo(QPointF to)
445{
446 if (m_hold || m_to == to)
447 return;
448
449 m_to = to;
450 emit toChanged();
451 updateResults();
452}
453
454/*!
455 \qmlproperty bool PdfSelection::hold
456
457 Controls whether to hold the existing selection regardless of changes to
458 \l from and \l to. This property can be set to \c true when the mouse
459 or touchpoint is released, so that the selection is not lost due to the
460 point bindings changing.
461*/
462bool QQuickPdfSelection::hold() const
463{
464 return m_hold;
465}
466
467void QQuickPdfSelection::setHold(bool hold)
468{
469 if (m_hold == hold)
470 return;
471
472 m_hold = hold;
473 emit holdChanged();
474}
475
476/*!
477 \qmlproperty string PdfSelection::text
478
479 The text that was found in the rectangular area between \l from and \l to,
480 or all text on the \l page if selectAll() was called.
481*/
482QString QQuickPdfSelection::text() const
483{
484 return m_text;
485}
486
487#if QT_CONFIG(clipboard)
488/*!
489 \qmlmethod void PdfSelection::copyToClipboard()
490
491 Copies plain text from the \l text property to the system clipboard.
492*/
493void QQuickPdfSelection::copyToClipboard() const
494{
495 QGuiApplication::clipboard()->setText(m_text);
496}
497#endif
498
499void QQuickPdfSelection::updateResults()
500{
501 if (!m_document)
502 return;
503 QPdfSelection sel = m_document->document()->getSelection(m_page,
504 m_from / m_renderScale, m_to / m_renderScale);
505 update(sel, true);
506}
507
508void QQuickPdfSelection::update(const QPdfSelection &sel, bool textAndGeometryOnly)
509{
510 if (sel.text() != m_text) {
511 m_text = sel.text();
512 if (QGuiApplication::clipboard()->supportsSelection())
513 sel.copyToClipboard(QClipboard::Selection);
514 emit textChanged();
515 }
516
517 if (sel.bounds() != m_geometry) {
518 m_geometry = sel.bounds();
519 emit selectedAreaChanged();
520 }
521
522 if (textAndGeometryOnly)
523 return;
524
525 m_fromCharIndex = sel.startIndex();
526 m_toCharIndex = sel.endIndex();
527 if (sel.bounds().isEmpty()) {
528 m_from = sel.boundingRectangle().topLeft() * m_renderScale;
529 m_to = m_from;
530 } else {
531 Qt::InputMethodQueries toUpdate = {};
532 QRectF firstLineBounds = sel.bounds().first().boundingRect();
533 m_from = firstLineBounds.topLeft() * m_renderScale;
534 if (!qFuzzyCompare(m_heightAtAnchor, firstLineBounds.height())) {
535 m_heightAtAnchor = firstLineBounds.height() * m_renderScale;
536 toUpdate.setFlag(Qt::ImAnchorRectangle);
537 }
538 QRectF lastLineBounds = sel.bounds().last().boundingRect();
539 if (!qFuzzyCompare(m_heightAtCursor, lastLineBounds.height())) {
540 m_heightAtCursor = lastLineBounds.height() * m_renderScale;
541 toUpdate.setFlag(Qt::ImCursorRectangle);
542 }
543 m_to = lastLineBounds.topRight() * m_renderScale;
544 if (toUpdate)
545 QGuiApplication::inputMethod()->update(toUpdate);
546 }
547}
548
549QT_END_NAMESPACE
550
551#include "moc_qquickpdfselection_p.cpp"