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