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