1 /****************************************************************************
2 **
3 ** Copyright (C) 2016 The Qt Company Ltd.
4 ** Copyright (C) 2012 BogDan Vatra <bogdan@kde.org>
5 ** Copyright (C) 2016 Olivier Goffart <ogoffart@woboq.com>
6 ** Contact: https://www.qt.io/licensing/
7 **
8 ** This file is part of the plugins of the Qt Toolkit.
9 **
10 ** $QT_BEGIN_LICENSE:LGPL$
11 ** Commercial License Usage
12 ** Licensees holding valid commercial Qt licenses may use this file in
13 ** accordance with the commercial license agreement provided with the
14 ** Software or, alternatively, in accordance with the terms contained in
15 ** a written agreement between you and The Qt Company. For licensing terms
16 ** and conditions see https://www.qt.io/terms-conditions. For further
17 ** information use the contact form at https://www.qt.io/contact-us.
18 **
19 ** GNU Lesser General Public License Usage
20 ** Alternatively, this file may be used under the terms of the GNU Lesser
21 ** General Public License version 3 as published by the Free Software
22 ** Foundation and appearing in the file LICENSE.LGPL3 included in the
23 ** packaging of this file. Please review the following information to
24 ** ensure the GNU Lesser General Public License version 3 requirements
25 ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
26 **
27 ** GNU General Public License Usage
28 ** Alternatively, this file may be used under the terms of the GNU
29 ** General Public License version 2.0 or (at your option) the GNU General
30 ** Public license version 3 or any later version approved by the KDE Free
31 ** Qt Foundation. The licenses are as published by the Free Software
32 ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
33 ** included in the packaging of this file. Please review the following
34 ** information to ensure the GNU General Public License requirements will
35 ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
36 ** https://www.gnu.org/licenses/gpl-3.0.html.
37 **
38 ** $QT_END_LICENSE$
39 **
40 ****************************************************************************/
41 
42 #include <android/log.h>
43 
44 #include "qandroidinputcontext.h"
45 #include "androidjnimain.h"
46 #include "androidjniinput.h"
47 #include "qandroideventdispatcher.h"
48 #include "androiddeadlockprotector.h"
49 #include "qandroidplatformintegration.h"
50 #include <QDebug>
51 #include <qevent.h>
52 #include <qguiapplication.h>
53 #include <qsharedpointer.h>
54 #include <qthread.h>
55 #include <qinputmethod.h>
56 #include <qwindow.h>
57 #include <QtCore/private/qjni_p.h>
58 #include <private/qhighdpiscaling_p.h>
59 
60 #include <QTextCharFormat>
61 #include <QTextBoundaryFinder>
62 
63 #include <QDebug>
64 
65 QT_BEGIN_NAMESPACE
66 
67 namespace {
68 
69 class BatchEditLock
70 {
71 public:
72 
BatchEditLock(QAndroidInputContext * context)73     explicit BatchEditLock(QAndroidInputContext *context)
74         : m_context(context)
75     {
76         m_context->beginBatchEdit();
77     }
78 
~BatchEditLock()79     ~BatchEditLock()
80     {
81         m_context->endBatchEdit();
82     }
83 
84     BatchEditLock(const BatchEditLock &) = delete;
85     BatchEditLock &operator=(const BatchEditLock &) = delete;
86 
87 private:
88 
89     QAndroidInputContext *m_context;
90 };
91 
92 } // namespace anonymous
93 
94 static QAndroidInputContext *m_androidInputContext = 0;
95 static char const *const QtNativeInputConnectionClassName = "org/qtproject/qt5/android/QtNativeInputConnection";
96 static char const *const QtExtractedTextClassName = "org/qtproject/qt5/android/QtExtractedText";
97 static jclass m_extractedTextClass = 0;
98 static jmethodID m_classConstructorMethodID = 0;
99 static jfieldID m_partialEndOffsetFieldID = 0;
100 static jfieldID m_partialStartOffsetFieldID = 0;
101 static jfieldID m_selectionEndFieldID = 0;
102 static jfieldID m_selectionStartFieldID = 0;
103 static jfieldID m_startOffsetFieldID = 0;
104 static jfieldID m_textFieldID = 0;
105 
runOnQtThread(const std::function<void ()> & func)106 static void runOnQtThread(const std::function<void()> &func)
107 {
108     AndroidDeadlockProtector protector;
109     if (!protector.acquire())
110         return;
111     QMetaObject::invokeMethod(m_androidInputContext, "safeCall", Qt::BlockingQueuedConnection, Q_ARG(std::function<void()>, func));
112 }
113 
beginBatchEdit(JNIEnv *,jobject)114 static jboolean beginBatchEdit(JNIEnv */*env*/, jobject /*thiz*/)
115 {
116     if (!m_androidInputContext)
117         return JNI_FALSE;
118 
119 #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
120     qDebug("@@@ BEGINBATCH");
121 #endif
122     jboolean res = JNI_FALSE;
123     runOnQtThread([&res]{res = m_androidInputContext->beginBatchEdit();});
124     return res;
125 }
126 
endBatchEdit(JNIEnv *,jobject)127 static jboolean endBatchEdit(JNIEnv */*env*/, jobject /*thiz*/)
128 {
129     if (!m_androidInputContext)
130         return JNI_FALSE;
131 
132 #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
133     qDebug("@@@ ENDBATCH");
134 #endif
135 
136     jboolean res = JNI_FALSE;
137     runOnQtThread([&res]{res = m_androidInputContext->endBatchEdit();});
138     return res;
139 }
140 
141 
commitText(JNIEnv * env,jobject,jstring text,jint newCursorPosition)142 static jboolean commitText(JNIEnv *env, jobject /*thiz*/, jstring text, jint newCursorPosition)
143 {
144     if (!m_androidInputContext)
145         return JNI_FALSE;
146 
147     jboolean isCopy;
148     const jchar *jstr = env->GetStringChars(text, &isCopy);
149     QString str(reinterpret_cast<const QChar *>(jstr), env->GetStringLength(text));
150     env->ReleaseStringChars(text, jstr);
151 
152 #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
153     qDebug() << "@@@ COMMIT" << str << newCursorPosition;
154 #endif
155     jboolean res = JNI_FALSE;
156     runOnQtThread([&]{res = m_androidInputContext->commitText(str, newCursorPosition);});
157     return res;
158 }
159 
deleteSurroundingText(JNIEnv *,jobject,jint leftLength,jint rightLength)160 static jboolean deleteSurroundingText(JNIEnv */*env*/, jobject /*thiz*/, jint leftLength, jint rightLength)
161 {
162     if (!m_androidInputContext)
163         return JNI_FALSE;
164 
165 #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
166     qDebug() << "@@@ DELETE" << leftLength << rightLength;
167 #endif
168     jboolean res = JNI_FALSE;
169     runOnQtThread([&]{res = m_androidInputContext->deleteSurroundingText(leftLength, rightLength);});
170     return res;
171 }
172 
finishComposingText(JNIEnv *,jobject)173 static jboolean finishComposingText(JNIEnv */*env*/, jobject /*thiz*/)
174 {
175     if (!m_androidInputContext)
176         return JNI_FALSE;
177 
178 #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
179     qDebug("@@@ FINISH");
180 #endif
181     jboolean res = JNI_FALSE;
182     runOnQtThread([&]{res = m_androidInputContext->finishComposingText();});
183     return res;
184 }
185 
getCursorCapsMode(JNIEnv *,jobject,jint reqModes)186 static jint getCursorCapsMode(JNIEnv */*env*/, jobject /*thiz*/, jint reqModes)
187 {
188     if (!m_androidInputContext)
189         return 0;
190 
191     jint res = 0;
192     runOnQtThread([&]{res = m_androidInputContext->getCursorCapsMode(reqModes);});
193     return res;
194 }
195 
getExtractedText(JNIEnv * env,jobject,int hintMaxChars,int hintMaxLines,jint flags)196 static jobject getExtractedText(JNIEnv *env, jobject /*thiz*/, int hintMaxChars, int hintMaxLines, jint flags)
197 {
198     if (!m_androidInputContext)
199         return 0;
200 
201     QAndroidInputContext::ExtractedText extractedText;
202     runOnQtThread([&]{extractedText = m_androidInputContext->getExtractedText(hintMaxChars, hintMaxLines, flags);});
203 
204 #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
205     qDebug() << "@@@ GETEX" << hintMaxChars << hintMaxLines << QString::fromLatin1("0x") + QString::number(flags,16) << extractedText.text << "partOff:" << extractedText.partialStartOffset << extractedText.partialEndOffset << "sel:" << extractedText.selectionStart << extractedText.selectionEnd << "offset:" << extractedText.startOffset;
206 #endif
207 
208     jobject object = env->NewObject(m_extractedTextClass, m_classConstructorMethodID);
209     env->SetIntField(object, m_partialStartOffsetFieldID, extractedText.partialStartOffset);
210     env->SetIntField(object, m_partialEndOffsetFieldID, extractedText.partialEndOffset);
211     env->SetIntField(object, m_selectionStartFieldID, extractedText.selectionStart);
212     env->SetIntField(object, m_selectionEndFieldID, extractedText.selectionEnd);
213     env->SetIntField(object, m_startOffsetFieldID, extractedText.startOffset);
214     env->SetObjectField(object,
215                         m_textFieldID,
216                         env->NewString(reinterpret_cast<const jchar *>(extractedText.text.constData()),
217                                        jsize(extractedText.text.length())));
218 
219     return object;
220 }
221 
getSelectedText(JNIEnv * env,jobject,jint flags)222 static jstring getSelectedText(JNIEnv *env, jobject /*thiz*/, jint flags)
223 {
224     if (!m_androidInputContext)
225         return 0;
226 
227     QString text;
228     runOnQtThread([&]{text = m_androidInputContext->getSelectedText(flags);});
229 #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
230     qDebug() << "@@@ GETSEL" << text;
231 #endif
232     if (text.isEmpty())
233         return 0;
234     return env->NewString(reinterpret_cast<const jchar *>(text.constData()), jsize(text.length()));
235 }
236 
getTextAfterCursor(JNIEnv * env,jobject,jint length,jint flags)237 static jstring getTextAfterCursor(JNIEnv *env, jobject /*thiz*/, jint length, jint flags)
238 {
239     if (!m_androidInputContext)
240         return 0;
241 
242     QString text;
243     runOnQtThread([&]{text = m_androidInputContext->getTextAfterCursor(length, flags);});
244 #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
245     qDebug() << "@@@ GETA" << length << text;
246 #endif
247     return env->NewString(reinterpret_cast<const jchar *>(text.constData()), jsize(text.length()));
248 }
249 
getTextBeforeCursor(JNIEnv * env,jobject,jint length,jint flags)250 static jstring getTextBeforeCursor(JNIEnv *env, jobject /*thiz*/, jint length, jint flags)
251 {
252     if (!m_androidInputContext)
253         return 0;
254 
255     QString text;
256     runOnQtThread([&]{text = m_androidInputContext->getTextBeforeCursor(length, flags);});
257 #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
258     qDebug() << "@@@ GETB" << length << text;
259 #endif
260     return env->NewString(reinterpret_cast<const jchar *>(text.constData()), jsize(text.length()));
261 }
262 
setComposingText(JNIEnv * env,jobject,jstring text,jint newCursorPosition)263 static jboolean setComposingText(JNIEnv *env, jobject /*thiz*/, jstring text, jint newCursorPosition)
264 {
265     if (!m_androidInputContext)
266         return JNI_FALSE;
267 
268     jboolean isCopy;
269     const jchar *jstr = env->GetStringChars(text, &isCopy);
270     QString str(reinterpret_cast<const QChar *>(jstr), env->GetStringLength(text));
271     env->ReleaseStringChars(text, jstr);
272 
273 #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
274     qDebug() << "@@@ SET" << str << newCursorPosition;
275 #endif
276     jboolean res = JNI_FALSE;
277     runOnQtThread([&]{res = m_androidInputContext->setComposingText(str, newCursorPosition);});
278     return res;
279 }
280 
setComposingRegion(JNIEnv *,jobject,jint start,jint end)281 static jboolean setComposingRegion(JNIEnv */*env*/, jobject /*thiz*/, jint start, jint end)
282 {
283     if (!m_androidInputContext)
284         return JNI_FALSE;
285 
286 #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
287     qDebug() << "@@@ SETR" << start << end;
288 #endif
289     jboolean res = JNI_FALSE;
290     runOnQtThread([&]{res = m_androidInputContext->setComposingRegion(start, end);});
291     return res;
292 }
293 
294 
setSelection(JNIEnv *,jobject,jint start,jint end)295 static jboolean setSelection(JNIEnv */*env*/, jobject /*thiz*/, jint start, jint end)
296 {
297     if (!m_androidInputContext)
298         return JNI_FALSE;
299 
300 #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
301     qDebug() << "@@@ SETSEL" << start << end;
302 #endif
303     jboolean res = JNI_FALSE;
304     runOnQtThread([&]{res = m_androidInputContext->setSelection(start, end);});
305     return res;
306 
307 }
308 
selectAll(JNIEnv *,jobject)309 static jboolean selectAll(JNIEnv */*env*/, jobject /*thiz*/)
310 {
311     if (!m_androidInputContext)
312         return JNI_FALSE;
313 
314 #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
315     qDebug("@@@ SELALL");
316 #endif
317     jboolean res = JNI_FALSE;
318     runOnQtThread([&]{res = m_androidInputContext->selectAll();});
319     return res;
320 }
321 
cut(JNIEnv *,jobject)322 static jboolean cut(JNIEnv */*env*/, jobject /*thiz*/)
323 {
324     if (!m_androidInputContext)
325         return JNI_FALSE;
326 
327 #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
328     qDebug("@@@");
329 #endif
330     jboolean res = JNI_FALSE;
331     runOnQtThread([&]{res = m_androidInputContext->cut();});
332     return res;
333 }
334 
copy(JNIEnv *,jobject)335 static jboolean copy(JNIEnv */*env*/, jobject /*thiz*/)
336 {
337     if (!m_androidInputContext)
338         return JNI_FALSE;
339 
340 #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
341     qDebug("@@@");
342 #endif
343     jboolean res = JNI_FALSE;
344     runOnQtThread([&]{res = m_androidInputContext->copy();});
345     return res;
346 }
347 
copyURL(JNIEnv *,jobject)348 static jboolean copyURL(JNIEnv */*env*/, jobject /*thiz*/)
349 {
350     if (!m_androidInputContext)
351         return JNI_FALSE;
352 
353 #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
354     qDebug("@@@");
355 #endif
356     jboolean res = JNI_FALSE;
357     runOnQtThread([&]{res = m_androidInputContext->copyURL();});
358     return res;
359 }
360 
paste(JNIEnv *,jobject)361 static jboolean paste(JNIEnv */*env*/, jobject /*thiz*/)
362 {
363     if (!m_androidInputContext)
364         return JNI_FALSE;
365 
366 #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
367     qDebug("@@@ PASTE");
368 #endif
369     jboolean res = JNI_FALSE;
370     runOnQtThread([&]{res = m_androidInputContext->paste();});
371     return res;
372 }
373 
updateCursorPosition(JNIEnv *,jobject)374 static jboolean updateCursorPosition(JNIEnv */*env*/, jobject /*thiz*/)
375 {
376     if (!m_androidInputContext)
377         return JNI_FALSE;
378 
379 #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
380     qDebug("@@@ UPDATECURSORPOS");
381 #endif
382 
383     runOnQtThread([&]{m_androidInputContext->updateCursorPosition();});
384     return true;
385 }
386 
387 
388 static JNINativeMethod methods[] = {
389     {"beginBatchEdit", "()Z", (void *)beginBatchEdit},
390     {"endBatchEdit", "()Z", (void *)endBatchEdit},
391     {"commitText", "(Ljava/lang/String;I)Z", (void *)commitText},
392     {"deleteSurroundingText", "(II)Z", (void *)deleteSurroundingText},
393     {"finishComposingText", "()Z", (void *)finishComposingText},
394     {"getCursorCapsMode", "(I)I", (void *)getCursorCapsMode},
395     {"getExtractedText", "(III)Lorg/qtproject/qt5/android/QtExtractedText;", (void *)getExtractedText},
396     {"getSelectedText", "(I)Ljava/lang/String;", (void *)getSelectedText},
397     {"getTextAfterCursor", "(II)Ljava/lang/String;", (void *)getTextAfterCursor},
398     {"getTextBeforeCursor", "(II)Ljava/lang/String;", (void *)getTextBeforeCursor},
399     {"setComposingText", "(Ljava/lang/String;I)Z", (void *)setComposingText},
400     {"setComposingRegion", "(II)Z", (void *)setComposingRegion},
401     {"setSelection", "(II)Z", (void *)setSelection},
402     {"selectAll", "()Z", (void *)selectAll},
403     {"cut", "()Z", (void *)cut},
404     {"copy", "()Z", (void *)copy},
405     {"copyURL", "()Z", (void *)copyURL},
406     {"paste", "()Z", (void *)paste},
407     {"updateCursorPosition", "()Z", (void *)updateCursorPosition}
408 };
409 
inputItemRectangle()410 static QRect inputItemRectangle()
411 {
412     QRectF itemRect = qGuiApp->inputMethod()->inputItemRectangle();
413     QRect rect = qGuiApp->inputMethod()->inputItemTransform().mapRect(itemRect).toRect();
414     QWindow *window = qGuiApp->focusWindow();
415     if (window)
416         rect = QRect(window->mapToGlobal(rect.topLeft()), rect.size());
417     double pixelDensity = window
418         ? QHighDpiScaling::factor(window)
419         : QHighDpiScaling::factor(QtAndroid::androidPlatformIntegration()->screen());
420     if (pixelDensity != 1.0) {
421         rect.setRect(rect.x() * pixelDensity,
422                      rect.y() * pixelDensity,
423                      rect.width() * pixelDensity,
424                      rect.height() * pixelDensity);
425     }
426     return rect;
427 }
428 
QAndroidInputContext()429 QAndroidInputContext::QAndroidInputContext()
430     : QPlatformInputContext()
431     , m_composingTextStart(-1)
432     , m_composingCursor(-1)
433     , m_handleMode(Hidden)
434     , m_batchEditNestingLevel(0)
435     , m_focusObject(0)
436 {
437     jclass clazz = QJNIEnvironmentPrivate::findClass(QtNativeInputConnectionClassName);
438     if (Q_UNLIKELY(!clazz)) {
439         qCritical() << "Native registration unable to find class '"
440                     << QtNativeInputConnectionClassName
441                     << '\'';
442         return;
443     }
444 
445     QJNIEnvironmentPrivate env;
446     if (Q_UNLIKELY(env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0)) {
447         qCritical() << "RegisterNatives failed for '"
448                     << QtNativeInputConnectionClassName
449                     << '\'';
450         return;
451     }
452 
453     clazz = QJNIEnvironmentPrivate::findClass(QtExtractedTextClassName);
454     if (Q_UNLIKELY(!clazz)) {
455         qCritical() << "Native registration unable to find class '"
456                     << QtExtractedTextClassName
457                     << '\'';
458         return;
459     }
460 
461     m_extractedTextClass = static_cast<jclass>(env->NewGlobalRef(clazz));
462     m_classConstructorMethodID = env->GetMethodID(m_extractedTextClass, "<init>", "()V");
463     if (Q_UNLIKELY(!m_classConstructorMethodID)) {
464         qCritical("GetMethodID failed");
465         return;
466     }
467 
468     m_partialEndOffsetFieldID = env->GetFieldID(m_extractedTextClass, "partialEndOffset", "I");
469     if (Q_UNLIKELY(!m_partialEndOffsetFieldID)) {
470         qCritical("Can't find field partialEndOffset");
471         return;
472     }
473 
474     m_partialStartOffsetFieldID = env->GetFieldID(m_extractedTextClass, "partialStartOffset", "I");
475     if (Q_UNLIKELY(!m_partialStartOffsetFieldID)) {
476         qCritical("Can't find field partialStartOffset");
477         return;
478     }
479 
480     m_selectionEndFieldID = env->GetFieldID(m_extractedTextClass, "selectionEnd", "I");
481     if (Q_UNLIKELY(!m_selectionEndFieldID)) {
482         qCritical("Can't find field selectionEnd");
483         return;
484     }
485 
486     m_selectionStartFieldID = env->GetFieldID(m_extractedTextClass, "selectionStart", "I");
487     if (Q_UNLIKELY(!m_selectionStartFieldID)) {
488         qCritical("Can't find field selectionStart");
489         return;
490     }
491 
492     m_startOffsetFieldID = env->GetFieldID(m_extractedTextClass, "startOffset", "I");
493     if (Q_UNLIKELY(!m_startOffsetFieldID)) {
494         qCritical("Can't find field startOffset");
495         return;
496     }
497 
498     m_textFieldID = env->GetFieldID(m_extractedTextClass, "text", "Ljava/lang/String;");
499     if (Q_UNLIKELY(!m_textFieldID)) {
500         qCritical("Can't find field text");
501         return;
502     }
503     qRegisterMetaType<QInputMethodEvent *>("QInputMethodEvent*");
504     qRegisterMetaType<QInputMethodQueryEvent *>("QInputMethodQueryEvent*");
505     m_androidInputContext = this;
506 
507     QObject::connect(QGuiApplication::inputMethod(), &QInputMethod::cursorRectangleChanged,
508                      this, &QAndroidInputContext::updateSelectionHandles);
509     QObject::connect(QGuiApplication::inputMethod(), &QInputMethod::anchorRectangleChanged,
510                      this, &QAndroidInputContext::updateSelectionHandles);
511     QObject::connect(QGuiApplication::inputMethod(), &QInputMethod::inputItemClipRectangleChanged, this, [this]{
512         auto im = qGuiApp->inputMethod();
513         if (!im->inputItemClipRectangle().contains(im->anchorRectangle()) ||
514                 !im->inputItemClipRectangle().contains(im->cursorRectangle())) {
515             m_handleMode = Hidden;
516             updateSelectionHandles();
517         }
518     });
519     m_hideCursorHandleTimer.setInterval(4000);
520     m_hideCursorHandleTimer.setSingleShot(true);
521     m_hideCursorHandleTimer.setTimerType(Qt::VeryCoarseTimer);
522     connect(&m_hideCursorHandleTimer, &QTimer::timeout, this, [this]{
523         m_handleMode = Hidden;
524         updateSelectionHandles();
525     });
526 }
527 
~QAndroidInputContext()528 QAndroidInputContext::~QAndroidInputContext()
529 {
530     m_androidInputContext = 0;
531     m_extractedTextClass = 0;
532     m_partialEndOffsetFieldID = 0;
533     m_partialStartOffsetFieldID = 0;
534     m_selectionEndFieldID = 0;
535     m_selectionStartFieldID = 0;
536     m_startOffsetFieldID = 0;
537     m_textFieldID = 0;
538 }
539 
androidInputContext()540 QAndroidInputContext *QAndroidInputContext::androidInputContext()
541 {
542     return m_androidInputContext;
543 }
544 
545 // cursor position getter that also works with editors that have not been updated to the new API
getAbsoluteCursorPosition(const QSharedPointer<QInputMethodQueryEvent> & query)546 static inline int getAbsoluteCursorPosition(const QSharedPointer<QInputMethodQueryEvent> &query)
547 {
548     QVariant absolutePos = query->value(Qt::ImAbsolutePosition);
549     return absolutePos.isValid() ? absolutePos.toInt() : query->value(Qt::ImCursorPosition).toInt();
550 }
551 
552 // position of the start of the current block
getBlockPosition(const QSharedPointer<QInputMethodQueryEvent> & query)553 static inline int getBlockPosition(const QSharedPointer<QInputMethodQueryEvent> &query)
554 {
555     QVariant absolutePos = query->value(Qt::ImAbsolutePosition);
556     return  absolutePos.isValid() ? absolutePos.toInt() - query->value(Qt::ImCursorPosition).toInt() : 0;
557 }
558 
reset()559 void QAndroidInputContext::reset()
560 {
561     focusObjectStopComposing();
562     clear();
563     m_batchEditNestingLevel = 0;
564     m_handleMode = Hidden;
565     if (qGuiApp->focusObject()) {
566         QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(Qt::ImEnabled);
567         if (!query.isNull() && query->value(Qt::ImEnabled).toBool()) {
568             QtAndroidInput::resetSoftwareKeyboard();
569             return;
570         }
571     }
572     QtAndroidInput::hideSoftwareKeyboard();
573 }
574 
commit()575 void QAndroidInputContext::commit()
576 {
577     focusObjectStopComposing();
578 }
579 
updateCursorPosition()580 void QAndroidInputContext::updateCursorPosition()
581 {
582     QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
583     if (!query.isNull() && m_batchEditNestingLevel == 0) {
584         const int cursorPos = getAbsoluteCursorPosition(query);
585         const int composeLength = m_composingText.length();
586 
587         //Q_ASSERT(m_composingText.isEmpty() == (m_composingTextStart == -1));
588         if (m_composingText.isEmpty() != (m_composingTextStart == -1))
589             qWarning() << "Input method out of sync" << m_composingText << m_composingTextStart;
590 
591         int realSelectionStart = cursorPos;
592         int realSelectionEnd = cursorPos;
593 
594         int cpos = query->value(Qt::ImCursorPosition).toInt();
595         int anchor = query->value(Qt::ImAnchorPosition).toInt();
596         if (cpos != anchor) {
597             if (!m_composingText.isEmpty()) {
598                 qWarning("Selecting text while preediting may give unpredictable results.");
599                 focusObjectStopComposing();
600             }
601             int blockPos = getBlockPosition(query);
602             realSelectionStart = blockPos + cpos;
603             realSelectionEnd = blockPos + anchor;
604         }
605         // Qt's idea of the cursor position is the start of the preedit area, so we maintain our own preedit cursor pos
606         if (focusObjectIsComposing())
607             realSelectionStart = realSelectionEnd = m_composingCursor;
608 
609         // Some keyboards misbahave when selStart > selEnd
610         if (realSelectionStart > realSelectionEnd)
611             std::swap(realSelectionStart, realSelectionEnd);
612 
613         QtAndroidInput::updateSelection(realSelectionStart, realSelectionEnd,
614                                         m_composingTextStart, m_composingTextStart + composeLength); // pre-edit text
615     }
616 }
617 
updateSelectionHandles()618 void QAndroidInputContext::updateSelectionHandles()
619 {
620     static bool noHandles = qEnvironmentVariableIntValue("QT_QPA_NO_TEXT_HANDLES");
621     if (noHandles)
622         return;
623 
624     auto im = qGuiApp->inputMethod();
625     if (!m_focusObject || ((m_handleMode & 0xff) == Hidden)) {
626         // Hide the handles
627         QtAndroidInput::updateHandles(Hidden);
628         return;
629     }
630     QWindow *window = qGuiApp->focusWindow();
631     double pixelDensity = window
632         ? QHighDpiScaling::factor(window)
633         : QHighDpiScaling::factor(QtAndroid::androidPlatformIntegration()->screen());
634 
635     QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition | Qt::ImEnabled | Qt::ImCurrentSelection | Qt::ImHints | Qt::ImSurroundingText);
636     QCoreApplication::sendEvent(m_focusObject, &query);
637 
638     int cpos = query.value(Qt::ImCursorPosition).toInt();
639     int anchor = query.value(Qt::ImAnchorPosition).toInt();
640 
641     if (cpos == anchor || im->anchorRectangle().isNull()) {
642         if (!query.value(Qt::ImEnabled).toBool()) {
643             QtAndroidInput::updateHandles(Hidden);
644             return;
645         }
646 
647         auto curRect = im->cursorRectangle();
648         QPoint cursorPoint(curRect.center().x(), curRect.bottom());
649         QPoint editMenuPoint(curRect.x(), curRect.y());
650         m_handleMode &= ShowEditPopup;
651         m_handleMode |= ShowCursor;
652         uint32_t buttons = EditContext::PasteButton;
653         if (!query.value(Qt::ImSurroundingText).toString().isEmpty())
654             buttons |= EditContext::SelectAllButton;
655         QtAndroidInput::updateHandles(m_handleMode, editMenuPoint * pixelDensity, buttons, cursorPoint * pixelDensity);
656         // The VK is hidden, reset the timer
657         if (m_hideCursorHandleTimer.isActive())
658             m_hideCursorHandleTimer.start();
659         return;
660     }
661 
662     m_handleMode = ShowSelection | ShowEditPopup ;
663     auto leftRect = im->cursorRectangle();
664     auto rightRect = im->anchorRectangle();
665     if (cpos > anchor)
666         std::swap(leftRect, rightRect);
667 
668     QPoint leftPoint(leftRect.bottomLeft().toPoint() * pixelDensity);
669     QPoint righPoint(rightRect.bottomRight().toPoint() * pixelDensity);
670     QPoint editPoint(leftRect.united(rightRect).topLeft().toPoint() * pixelDensity);
671     QtAndroidInput::updateHandles(m_handleMode, editPoint, EditContext::AllButtons, leftPoint, righPoint,
672                                   query.value(Qt::ImCurrentSelection).toString().isRightToLeft());
673     m_hideCursorHandleTimer.stop();
674 }
675 
676 /*
677    Called from Java when a cursor/selection handle was dragged to a new position
678 
679    handleId of 1 means the cursor handle,  2 means the left handle, 3 means the right handle
680  */
handleLocationChanged(int handleId,int x,int y)681 void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y)
682 {
683     if (m_batchEditNestingLevel != 0) {
684         qWarning() << "QAndroidInputContext::handleLocationChanged returned";
685         return;
686     }
687 
688     auto im = qGuiApp->inputMethod();
689     auto leftRect = im->cursorRectangle();
690     // The handle is down of the cursor, but we want the position in the middle.
691     QWindow *window = qGuiApp->focusWindow();
692     double pixelDensity = window
693         ? QHighDpiScaling::factor(window)
694         : QHighDpiScaling::factor(QtAndroid::androidPlatformIntegration()->screen());
695     QPointF point(x / pixelDensity, y / pixelDensity);
696     point.setY(point.y() - leftRect.width() / 2);
697 
698     QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition
699                                  | Qt::ImAbsolutePosition | Qt::ImCurrentSelection);
700     QCoreApplication::sendEvent(m_focusObject, &query);
701     int cpos = query.value(Qt::ImCursorPosition).toInt();
702     int anchor = query.value(Qt::ImAnchorPosition).toInt();
703     auto rightRect = im->anchorRectangle();
704     if (cpos > anchor)
705         std::swap(leftRect, rightRect);
706 
707     // Do not allow dragging left handle below right handle, or right handle above left handle
708     if (handleId == 2 && point.y() > rightRect.center().y()) {
709         point.setY(rightRect.center().y());
710     } else if (handleId == 3 && point.y() < leftRect.center().y()) {
711         point.setY(leftRect.center().y());
712     }
713 
714     const QPointF pointLocal = im->inputItemTransform().inverted().map(point);
715     bool ok;
716     const int handlePos =
717             QInputMethod::queryFocusObject(Qt::ImCursorPosition, pointLocal).toInt(&ok);
718     if (!ok)
719         return;
720 
721     int newCpos = cpos;
722     int newAnchor = anchor;
723     if (newAnchor > newCpos)
724         std::swap(newAnchor, newCpos);
725 
726     if (handleId == 1) {
727         newCpos = handlePos;
728         newAnchor = handlePos;
729     } else if (handleId == 2) {
730         newAnchor = handlePos;
731     } else if (handleId == 3) {
732         newCpos = handlePos;
733     }
734 
735     /*
736       Do not allow clearing selection by dragging selection handles and do not allow swapping
737       selection handles for consistency with Android's native text editing controls. Ensure that at
738       least one symbol remains selected.
739      */
740     if ((handleId == 2 || handleId == 3) && newCpos <= newAnchor) {
741         QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme,
742                                    query.value(Qt::ImCurrentSelection).toString());
743 
744         const int oldSelectionStartPos = qMin(cpos, anchor);
745 
746         if (handleId == 2) {
747             finder.toEnd();
748             finder.toPreviousBoundary();
749             newAnchor = finder.position() + oldSelectionStartPos;
750         } else {
751             finder.toStart();
752             finder.toNextBoundary();
753             newCpos = finder.position() + oldSelectionStartPos;
754         }
755     }
756 
757     // Check if handle has been dragged far enough
758     if (!focusObjectIsComposing() && newCpos == cpos && newAnchor == anchor)
759         return;
760 
761     /*
762       If the editor is currently in composing state, we have to compare newCpos with
763       m_composingCursor instead of cpos. And since there is nothing to compare with newAnchor, we
764       perform the check only when user drags the cursor handle.
765      */
766     if (focusObjectIsComposing() && handleId == 1) {
767         int absoluteCpos = query.value(Qt::ImAbsolutePosition).toInt(&ok);
768         if (!ok)
769             absoluteCpos = cpos;
770         const int blockPos = absoluteCpos - cpos;
771 
772         if (blockPos + newCpos == m_composingCursor)
773             return;
774     }
775 
776     BatchEditLock batchEditLock(this);
777 
778     focusObjectStopComposing();
779 
780     QList<QInputMethodEvent::Attribute> attributes;
781     attributes.append({ QInputMethodEvent::Selection, newAnchor, newCpos - newAnchor });
782     if (newCpos != newAnchor)
783         attributes.append({ QInputMethodEvent::Cursor, 0, 0 });
784 
785     QInputMethodEvent event(QString(), attributes);
786     QGuiApplication::sendEvent(m_focusObject, &event);
787 }
788 
touchDown(int x,int y)789 void QAndroidInputContext::touchDown(int x, int y)
790 {
791     if (m_focusObject && inputItemRectangle().contains(x, y)) {
792         // If the user touch the input rectangle, we can show the cursor handle
793         m_handleMode = ShowCursor;
794         // The VK will appear in a moment, stop the timer
795         m_hideCursorHandleTimer.stop();
796 
797         if (focusObjectIsComposing()) {
798             const double pixelDensity =
799                     QGuiApplication::focusWindow()
800                     ? QHighDpiScaling::factor(QGuiApplication::focusWindow())
801                     : QHighDpiScaling::factor(QtAndroid::androidPlatformIntegration()->screen());
802 
803             const QPointF touchPointLocal =
804                     QGuiApplication::inputMethod()->inputItemTransform().inverted().map(
805                             QPointF(x / pixelDensity, y / pixelDensity));
806 
807             const int curBlockPos = getBlockPosition(
808                     focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAbsolutePosition));
809             const int touchPosition = curBlockPos
810                     + QInputMethod::queryFocusObject(Qt::ImCursorPosition, touchPointLocal).toInt();
811             if (touchPosition != m_composingCursor)
812                 focusObjectStopComposing();
813         }
814 
815         updateSelectionHandles();
816     }
817 }
818 
longPress(int x,int y)819 void QAndroidInputContext::longPress(int x, int y)
820 {
821     static bool noHandles = qEnvironmentVariableIntValue("QT_QPA_NO_TEXT_HANDLES");
822     if (noHandles)
823         return;
824 
825     if (m_focusObject && inputItemRectangle().contains(x, y)) {
826         BatchEditLock batchEditLock(this);
827 
828         focusObjectStopComposing();
829 
830         const double pixelDensity =
831                 QGuiApplication::focusWindow()
832                 ? QHighDpiScaling::factor(QGuiApplication::focusWindow())
833                 : QHighDpiScaling::factor(QtAndroid::androidPlatformIntegration()->screen());
834         const QPointF touchPoint(x / pixelDensity, y / pixelDensity);
835         setSelectionOnFocusObject(touchPoint, touchPoint);
836 
837         QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition | Qt::ImTextBeforeCursor | Qt::ImTextAfterCursor);
838         QCoreApplication::sendEvent(m_focusObject, &query);
839         int cursor = query.value(Qt::ImCursorPosition).toInt();
840         int anchor = cursor;
841         QString before = query.value(Qt::ImTextBeforeCursor).toString();
842         QString after = query.value(Qt::ImTextAfterCursor).toString();
843         for (const auto &ch : after) {
844             if (!ch.isLetterOrNumber())
845                 break;
846             ++anchor;
847         }
848 
849         for (auto itch = before.rbegin(); itch != after.rend(); ++itch) {
850             if (!itch->isLetterOrNumber())
851                 break;
852             --cursor;
853         }
854         if (cursor == anchor || cursor < 0 || cursor - anchor > 500) {
855             m_handleMode = ShowCursor | ShowEditPopup;
856             updateSelectionHandles();
857             return;
858         }
859         QList<QInputMethodEvent::Attribute> imAttributes;
860         imAttributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, 0, 0, QVariant()));
861         imAttributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection, anchor, cursor - anchor, QVariant()));
862         QInputMethodEvent event(QString(), imAttributes);
863         QGuiApplication::sendEvent(m_focusObject, &event);
864 
865         m_handleMode = ShowSelection | ShowEditPopup;
866         updateSelectionHandles();
867     }
868 }
869 
keyDown()870 void QAndroidInputContext::keyDown()
871 {
872     if (m_handleMode) {
873         // When the user enter text on the keyboard, we hide the cursor handle
874         m_handleMode = Hidden;
875         updateSelectionHandles();
876     }
877 }
878 
hideSelectionHandles()879 void QAndroidInputContext::hideSelectionHandles()
880 {
881     if (m_handleMode & ShowSelection) {
882         m_handleMode = Hidden;
883         updateSelectionHandles();
884     } else {
885         m_hideCursorHandleTimer.start();
886     }
887 }
888 
update(Qt::InputMethodQueries queries)889 void QAndroidInputContext::update(Qt::InputMethodQueries queries)
890 {
891     QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(queries);
892     if (query.isNull())
893         return;
894 #warning TODO extract the needed data from query
895 }
896 
invokeAction(QInputMethod::Action action,int cursorPosition)897 void QAndroidInputContext::invokeAction(QInputMethod::Action action, int cursorPosition)
898 {
899 #warning TODO Handle at least QInputMethod::ContextMenu action
900     Q_UNUSED(action)
901     Q_UNUSED(cursorPosition)
902     //### click should be passed to the IM, but in the meantime it's better to ignore it than to do something wrong
903     // if (action == QInputMethod::Click)
904     //     commit();
905 }
906 
keyboardRect() const907 QRectF QAndroidInputContext::keyboardRect() const
908 {
909     return QtAndroidInput::softwareKeyboardRect();
910 }
911 
isAnimating() const912 bool QAndroidInputContext::isAnimating() const
913 {
914     return false;
915 }
916 
showInputPanel()917 void QAndroidInputContext::showInputPanel()
918 {
919     if (QGuiApplication::applicationState() != Qt::ApplicationActive) {
920         connect(qGuiApp, SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(showInputPanelLater(Qt::ApplicationState)));
921         return;
922     }
923     QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
924     if (query.isNull())
925         return;
926 
927     disconnect(m_updateCursorPosConnection);
928     if (qGuiApp->focusObject()->metaObject()->indexOfSignal("cursorPositionChanged(int,int)") >= 0) // QLineEdit breaks the pattern
929         m_updateCursorPosConnection = connect(qGuiApp->focusObject(), SIGNAL(cursorPositionChanged(int,int)), this, SLOT(updateCursorPosition()));
930     else
931         m_updateCursorPosConnection = connect(qGuiApp->focusObject(), SIGNAL(cursorPositionChanged()), this, SLOT(updateCursorPosition()));
932 
933     QRect rect = inputItemRectangle();
934     QtAndroidInput::showSoftwareKeyboard(rect.left(), rect.top(), rect.width(), rect.height(),
935                                          query->value(Qt::ImHints).toUInt(),
936                                          query->value(Qt::ImEnterKeyType).toUInt());
937 }
938 
showInputPanelLater(Qt::ApplicationState state)939 void QAndroidInputContext::showInputPanelLater(Qt::ApplicationState state)
940 {
941     if (state != Qt::ApplicationActive)
942         return;
943     disconnect(qGuiApp, SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(showInputPanelLater(Qt::ApplicationState)));
944     showInputPanel();
945 }
946 
safeCall(const std::function<void ()> & func,Qt::ConnectionType conType)947 void QAndroidInputContext::safeCall(const std::function<void()> &func, Qt::ConnectionType conType)
948 {
949     if (qGuiApp->thread() == QThread::currentThread())
950         func();
951     else
952         QMetaObject::invokeMethod(this, "safeCall", conType, Q_ARG(std::function<void()>, func));
953 }
954 
hideInputPanel()955 void QAndroidInputContext::hideInputPanel()
956 {
957     QtAndroidInput::hideSoftwareKeyboard();
958 }
959 
isInputPanelVisible() const960 bool QAndroidInputContext::isInputPanelVisible() const
961 {
962     return QtAndroidInput::isSoftwareKeyboardVisible();
963 }
964 
isComposing() const965 bool QAndroidInputContext::isComposing() const
966 {
967     return m_composingText.length();
968 }
969 
clear()970 void QAndroidInputContext::clear()
971 {
972     m_composingText.clear();
973     m_composingTextStart  = -1;
974     m_composingCursor = -1;
975     m_extractedText.clear();
976 }
977 
978 
setFocusObject(QObject * object)979 void QAndroidInputContext::setFocusObject(QObject *object)
980 {
981     if (object != m_focusObject) {
982         focusObjectStopComposing();
983         m_focusObject = object;
984         reset();
985     }
986     QPlatformInputContext::setFocusObject(object);
987     updateSelectionHandles();
988 }
989 
beginBatchEdit()990 jboolean QAndroidInputContext::beginBatchEdit()
991 {
992     ++m_batchEditNestingLevel;
993     return JNI_TRUE;
994 }
995 
endBatchEdit()996 jboolean QAndroidInputContext::endBatchEdit()
997 {
998     if (--m_batchEditNestingLevel == 0) { //ending batch edit mode
999         focusObjectStartComposing();
1000         updateCursorPosition();
1001     }
1002     return JNI_TRUE;
1003 }
1004 
1005 /*
1006   Android docs say: This behaves like calling setComposingText(text, newCursorPosition) then
1007   finishComposingText().
1008 */
commitText(const QString & text,jint newCursorPosition)1009 jboolean QAndroidInputContext::commitText(const QString &text, jint newCursorPosition)
1010 {
1011     BatchEditLock batchEditLock(this);
1012     return setComposingText(text, newCursorPosition) && finishComposingText();
1013 }
1014 
deleteSurroundingText(jint leftLength,jint rightLength)1015 jboolean QAndroidInputContext::deleteSurroundingText(jint leftLength, jint rightLength)
1016 {
1017     BatchEditLock batchEditLock(this);
1018 
1019     focusObjectStopComposing();
1020 
1021     QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1022     if (query.isNull())
1023         return JNI_TRUE;
1024 
1025     if (leftLength < 0) {
1026         rightLength += -leftLength;
1027         leftLength = 0;
1028     }
1029 
1030     const int initialBlockPos = getBlockPosition(query);
1031     const int initialCursorPos = getAbsoluteCursorPosition(query);
1032     const int initialAnchorPos = initialBlockPos + query->value(Qt::ImAnchorPosition).toInt();
1033 
1034     /*
1035       According to documentation, we should delete leftLength characters before current selection
1036       and rightLength characters after current selection (without affecting selection). But that is
1037       absolutely not what Android's native EditText does. It deletes leftLength characters before
1038       min(selection start, composing region start) and rightLength characters after max(selection
1039       end, composing region end). There are no known keyboards that depend on this behavior, but
1040       it is better to be consistent with EditText behavior, because there definetly should be no
1041       keyboards that depend on documented behavior.
1042      */
1043     const int leftEnd =
1044             m_composingText.isEmpty()
1045             ? qMin(initialCursorPos, initialAnchorPos)
1046             : qMin(qMin(initialCursorPos, initialAnchorPos), m_composingTextStart);
1047 
1048     const int rightBegin =
1049             m_composingText.isEmpty()
1050             ? qMax(initialCursorPos, initialAnchorPos)
1051             : qMax(qMax(initialCursorPos, initialAnchorPos),
1052                    m_composingTextStart + m_composingText.length());
1053 
1054     int textBeforeCursorLen;
1055     int textAfterCursorLen;
1056 
1057     QVariant textBeforeCursor = query->value(Qt::ImTextBeforeCursor);
1058     QVariant textAfterCursor = query->value(Qt::ImTextAfterCursor);
1059     if (textBeforeCursor.isValid() && textAfterCursor.isValid()) {
1060         textBeforeCursorLen = textBeforeCursor.toString().length();
1061         textAfterCursorLen = textAfterCursor.toString().length();
1062     } else {
1063         textBeforeCursorLen = initialCursorPos - initialBlockPos;
1064         textAfterCursorLen =
1065                 query->value(Qt::ImSurroundingText).toString().length() - textBeforeCursorLen;
1066     }
1067 
1068     leftLength = qMin(qMax(0, textBeforeCursorLen - (initialCursorPos - leftEnd)), leftLength);
1069     rightLength = qMin(qMax(0, textAfterCursorLen - (rightBegin - initialCursorPos)), rightLength);
1070 
1071     if (leftLength == 0 && rightLength == 0)
1072         return JNI_TRUE;
1073 
1074     if (leftEnd == rightBegin) {
1075         // We have no selection and no composing region; we can do everything using one event
1076         QInputMethodEvent event;
1077         event.setCommitString({}, -leftLength, leftLength + rightLength);
1078         QGuiApplication::sendEvent(m_focusObject, &event);
1079     } else {
1080         if (initialCursorPos != initialAnchorPos) {
1081             QInputMethodEvent event({}, {
1082                 { QInputMethodEvent::Selection, initialCursorPos - initialBlockPos, 0 }
1083             });
1084 
1085             QGuiApplication::sendEvent(m_focusObject, &event);
1086         }
1087 
1088         int currentCursorPos = initialCursorPos;
1089 
1090         if (rightLength > 0) {
1091             QInputMethodEvent event;
1092             event.setCommitString({}, rightBegin - currentCursorPos, rightLength);
1093             QGuiApplication::sendEvent(m_focusObject, &event);
1094 
1095             currentCursorPos = rightBegin;
1096         }
1097 
1098         if (leftLength > 0) {
1099             const int leftBegin = leftEnd - leftLength;
1100 
1101             QInputMethodEvent event;
1102             event.setCommitString({}, leftBegin - currentCursorPos, leftLength);
1103             QGuiApplication::sendEvent(m_focusObject, &event);
1104 
1105             currentCursorPos = leftBegin;
1106 
1107             if (!m_composingText.isEmpty())
1108                 m_composingTextStart -= leftLength;
1109         }
1110 
1111         // Restore cursor position or selection
1112         if (currentCursorPos != initialCursorPos - leftLength
1113                 || initialCursorPos != initialAnchorPos) {
1114             // If we have deleted a newline character, we are now in a new block
1115             const int currentBlockPos = getBlockPosition(
1116                     focusObjectInputMethodQuery(Qt::ImAbsolutePosition | Qt::ImCursorPosition));
1117 
1118             QInputMethodEvent event({}, {
1119                 { QInputMethodEvent::Selection, initialCursorPos - leftLength - currentBlockPos,
1120                   initialAnchorPos - initialCursorPos },
1121                 { QInputMethodEvent::Cursor, 0, 0 }
1122             });
1123 
1124             QGuiApplication::sendEvent(m_focusObject, &event);
1125         }
1126     }
1127 
1128     return JNI_TRUE;
1129 }
1130 
1131 // Android docs say the cursor must not move
finishComposingText()1132 jboolean QAndroidInputContext::finishComposingText()
1133 {
1134     BatchEditLock batchEditLock(this);
1135 
1136     if (!focusObjectStopComposing())
1137         return JNI_FALSE;
1138 
1139     clear();
1140     return JNI_TRUE;
1141 }
1142 
focusObjectIsComposing() const1143 bool QAndroidInputContext::focusObjectIsComposing() const
1144 {
1145     return m_composingCursor != -1;
1146 }
1147 
focusObjectStartComposing()1148 void QAndroidInputContext::focusObjectStartComposing()
1149 {
1150     if (focusObjectIsComposing() || m_composingText.isEmpty())
1151         return;
1152 
1153     // Composing strings containing newline characters are rare and may cause problems
1154     if (m_composingText.contains(QLatin1Char('\n')))
1155         return;
1156 
1157     QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1158     if (!query)
1159         return;
1160 
1161     if (query->value(Qt::ImCursorPosition).toInt() != query->value(Qt::ImAnchorPosition).toInt())
1162         return;
1163 
1164     const int absoluteCursorPos = getAbsoluteCursorPosition(query);
1165     if (absoluteCursorPos < m_composingTextStart
1166             || absoluteCursorPos > m_composingTextStart + m_composingText.length())
1167         return;
1168 
1169     m_composingCursor = absoluteCursorPos;
1170 
1171     QTextCharFormat underlined;
1172     underlined.setFontUnderline(true);
1173 
1174     QInputMethodEvent event(m_composingText, {
1175         { QInputMethodEvent::Cursor, absoluteCursorPos - m_composingTextStart, 1 },
1176         { QInputMethodEvent::TextFormat, 0, m_composingText.length(), underlined }
1177     });
1178 
1179     event.setCommitString({}, m_composingTextStart - absoluteCursorPos, m_composingText.length());
1180 
1181     QGuiApplication::sendEvent(m_focusObject, &event);
1182 }
1183 
focusObjectStopComposing()1184 bool QAndroidInputContext::focusObjectStopComposing()
1185 {
1186     if (!focusObjectIsComposing())
1187         return true; // not composing
1188 
1189     QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1190     if (query.isNull())
1191         return false;
1192 
1193     const int blockPos = getBlockPosition(query);
1194     const int localCursorPos = m_composingCursor - blockPos;
1195 
1196     m_composingCursor = -1;
1197 
1198     // Moving Qt's cursor to where the preedit cursor used to be
1199     QList<QInputMethodEvent::Attribute> attributes;
1200     attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection, localCursorPos, 0));
1201 
1202     QInputMethodEvent event(QString(), attributes);
1203     event.setCommitString(m_composingText);
1204     sendInputMethodEvent(&event);
1205 
1206     return true;
1207 }
1208 
getCursorCapsMode(jint)1209 jint QAndroidInputContext::getCursorCapsMode(jint /*reqModes*/)
1210 {
1211     jint res = 0;
1212     QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1213     if (query.isNull())
1214         return res;
1215 
1216     const uint qtInputMethodHints = query->value(Qt::ImHints).toUInt();
1217     const int localPos = query->value(Qt::ImCursorPosition).toInt();
1218 
1219     bool atWordBoundary =
1220             localPos == 0
1221             && (!focusObjectIsComposing() || m_composingCursor == m_composingTextStart);
1222 
1223     if (!atWordBoundary) {
1224         QString surroundingText = query->value(Qt::ImSurroundingText).toString();
1225         surroundingText.truncate(localPos);
1226         if (focusObjectIsComposing())
1227             surroundingText += m_composingText.leftRef(m_composingCursor - m_composingTextStart);
1228         // Add a character to see if it is at the end of the sentence or not
1229         QTextBoundaryFinder finder(QTextBoundaryFinder::Sentence, surroundingText + QLatin1Char('A'));
1230         finder.setPosition(surroundingText.length());
1231         if (finder.isAtBoundary())
1232             atWordBoundary = finder.isAtBoundary();
1233     }
1234     if (atWordBoundary && !(qtInputMethodHints & Qt::ImhLowercaseOnly) && !(qtInputMethodHints & Qt::ImhNoAutoUppercase))
1235         res |= CAP_MODE_SENTENCES;
1236 
1237     if (qtInputMethodHints & Qt::ImhUppercaseOnly)
1238         res |= CAP_MODE_CHARACTERS;
1239 
1240     return res;
1241 }
1242 
1243 
1244 
getExtractedText(jint,jint,jint)1245 const QAndroidInputContext::ExtractedText &QAndroidInputContext::getExtractedText(jint /*hintMaxChars*/, jint /*hintMaxLines*/, jint /*flags*/)
1246 {
1247     // Note to self: "if the GET_EXTRACTED_TEXT_MONITOR flag is set, you should be calling
1248     // updateExtractedText(View, int, ExtractedText) whenever you call
1249     // updateSelection(View, int, int, int, int)."  QTBUG-37980
1250 
1251     QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(
1252             Qt::ImCursorPosition | Qt::ImAbsolutePosition | Qt::ImAnchorPosition);
1253     if (query.isNull())
1254         return m_extractedText;
1255 
1256     const int cursorPos = getAbsoluteCursorPosition(query);
1257     const int blockPos = getBlockPosition(query);
1258 
1259     // It is documented that we should try to return hintMaxChars
1260     // characters, but standard Android controls always return all text, and
1261     // there are input methods out there that (surprise) seem to depend on
1262     // what happens in reality rather than what's documented.
1263 
1264     QVariant textBeforeCursor = QInputMethod::queryFocusObject(Qt::ImTextBeforeCursor, INT_MAX);
1265     QVariant textAfterCursor = QInputMethod::queryFocusObject(Qt::ImTextAfterCursor, INT_MAX);
1266     if (textBeforeCursor.isValid() && textAfterCursor.isValid()) {
1267         if (focusObjectIsComposing()) {
1268             m_extractedText.text =
1269                     textBeforeCursor.toString() + m_composingText + textAfterCursor.toString();
1270         } else {
1271             m_extractedText.text = textBeforeCursor.toString() + textAfterCursor.toString();
1272         }
1273 
1274         m_extractedText.startOffset = qMax(0, cursorPos - textBeforeCursor.toString().length());
1275     } else {
1276         m_extractedText.text = focusObjectInputMethodQuery(Qt::ImSurroundingText)
1277                 ->value(Qt::ImSurroundingText).toString();
1278 
1279         if (focusObjectIsComposing())
1280             m_extractedText.text.insert(cursorPos - blockPos, m_composingText);
1281 
1282         m_extractedText.startOffset = blockPos;
1283     }
1284 
1285     if (focusObjectIsComposing()) {
1286         m_extractedText.selectionStart = m_composingCursor - m_extractedText.startOffset;
1287         m_extractedText.selectionEnd = m_extractedText.selectionStart;
1288     } else {
1289         m_extractedText.selectionStart = cursorPos - m_extractedText.startOffset;
1290         m_extractedText.selectionEnd =
1291                 blockPos + query->value(Qt::ImAnchorPosition).toInt() - m_extractedText.startOffset;
1292 
1293         // Some keyboards misbehave when selectionStart > selectionEnd
1294         if (m_extractedText.selectionStart > m_extractedText.selectionEnd)
1295             std::swap(m_extractedText.selectionStart, m_extractedText.selectionEnd);
1296     }
1297 
1298     return m_extractedText;
1299 }
1300 
getSelectedText(jint)1301 QString QAndroidInputContext::getSelectedText(jint /*flags*/)
1302 {
1303     QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1304     if (query.isNull())
1305         return QString();
1306 
1307     return query->value(Qt::ImCurrentSelection).toString();
1308 }
1309 
getTextAfterCursor(jint length,jint)1310 QString QAndroidInputContext::getTextAfterCursor(jint length, jint /*flags*/)
1311 {
1312     if (length <= 0)
1313         return QString();
1314 
1315     QString text;
1316 
1317     QVariant reportedTextAfter = QInputMethod::queryFocusObject(Qt::ImTextAfterCursor, length);
1318     if (reportedTextAfter.isValid()) {
1319         text = reportedTextAfter.toString();
1320     } else {
1321         // Compatibility code for old controls that do not implement the new API
1322         QSharedPointer<QInputMethodQueryEvent> query =
1323                 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImSurroundingText);
1324         if (query) {
1325             const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1326             text = query->value(Qt::ImSurroundingText).toString().mid(cursorPos);
1327         }
1328     }
1329 
1330     if (focusObjectIsComposing()) {
1331         // Controls do not report preedit text, so we have to add it
1332         const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart;
1333         text = m_composingText.midRef(cursorPosInsidePreedit) + text;
1334     } else {
1335         // We must not return selected text if there is any
1336         QSharedPointer<QInputMethodQueryEvent> query =
1337                 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAnchorPosition);
1338         if (query) {
1339             const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1340             const int anchorPos = query->value(Qt::ImAnchorPosition).toInt();
1341             if (anchorPos > cursorPos)
1342                 text.remove(0, anchorPos - cursorPos);
1343         }
1344     }
1345 
1346     text.truncate(length);
1347     return text;
1348 }
1349 
getTextBeforeCursor(jint length,jint)1350 QString QAndroidInputContext::getTextBeforeCursor(jint length, jint /*flags*/)
1351 {
1352     if (length <= 0)
1353         return QString();
1354 
1355     QString text;
1356 
1357     QVariant reportedTextBefore = QInputMethod::queryFocusObject(Qt::ImTextBeforeCursor, length);
1358     if (reportedTextBefore.isValid()) {
1359         text = reportedTextBefore.toString();
1360     } else {
1361         // Compatibility code for old controls that do not implement the new API
1362         QSharedPointer<QInputMethodQueryEvent> query =
1363                 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImSurroundingText);
1364         if (query) {
1365             const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1366             text = query->value(Qt::ImSurroundingText).toString().left(cursorPos);
1367         }
1368     }
1369 
1370     if (focusObjectIsComposing()) {
1371         // Controls do not report preedit text, so we have to add it
1372         const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart;
1373         text += m_composingText.leftRef(cursorPosInsidePreedit);
1374     } else {
1375         // We must not return selected text if there is any
1376         QSharedPointer<QInputMethodQueryEvent> query =
1377                 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAnchorPosition);
1378         if (query) {
1379             const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1380             const int anchorPos = query->value(Qt::ImAnchorPosition).toInt();
1381             if (anchorPos < cursorPos)
1382                 text.chop(cursorPos - anchorPos);
1383         }
1384     }
1385 
1386     if (text.length() > length)
1387         text = text.right(length);
1388     return text;
1389 }
1390 
1391 /*
1392   Android docs say that this function should:
1393   - remove the current composing text, if there is any
1394   - otherwise remove currently selected text, if there is any
1395   - insert new text in place of old composing text or, if there was none, at current cursor position
1396   - mark the inserted text as composing
1397   - move cursor as specified by newCursorPosition: if > 0, it is relative to the end of inserted
1398     text - 1; if <= 0, it is relative to the start of inserted text
1399  */
1400 
setComposingText(const QString & text,jint newCursorPosition)1401 jboolean QAndroidInputContext::setComposingText(const QString &text, jint newCursorPosition)
1402 {
1403     QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1404     if (query.isNull())
1405         return JNI_FALSE;
1406 
1407     BatchEditLock batchEditLock(this);
1408 
1409     const int absoluteCursorPos = getAbsoluteCursorPosition(query);
1410     int absoluteAnchorPos = getBlockPosition(query) + query->value(Qt::ImAnchorPosition).toInt();
1411 
1412     auto setCursorPosition = [=]() {
1413             const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1414             QInputMethodEvent event({}, { { QInputMethodEvent::Selection, cursorPos, 0 } });
1415             QGuiApplication::sendEvent(m_focusObject, &event);
1416         };
1417 
1418     // If we have composing region and selection (and therefore focusObjectIsComposing() == false),
1419     // we must clear selection so that we won't delete it when we will be replacing composing text
1420     if (!m_composingText.isEmpty() && absoluteCursorPos != absoluteAnchorPos) {
1421         setCursorPosition();
1422         absoluteAnchorPos = absoluteCursorPos;
1423     }
1424 
1425     // The value of Qt::ImCursorPosition is not updated at the start
1426     // when the first character is added, so we must update it (QTBUG-85090)
1427     if (absoluteCursorPos == 0 && text.length() == 1 && getTextAfterCursor(1,1).length() >= 0) {
1428         setCursorPosition();
1429     }
1430 
1431     // If we had no composing region, pretend that we had a zero-length composing region at current
1432     // cursor position to simplify code. Also account for that we must delete selected text if there
1433     // (still) is any.
1434     const int effectiveAbsoluteCursorPos = qMin(absoluteCursorPos, absoluteAnchorPos);
1435     if (m_composingTextStart == -1)
1436         m_composingTextStart = effectiveAbsoluteCursorPos;
1437 
1438     const int oldComposingTextLen = m_composingText.length();
1439     m_composingText = text;
1440 
1441     const int newAbsoluteCursorPos =
1442             newCursorPosition <= 0
1443             ? m_composingTextStart + newCursorPosition
1444             : m_composingTextStart + m_composingText.length() + newCursorPosition - 1;
1445 
1446     const bool focusObjectWasComposing = focusObjectIsComposing();
1447 
1448     // Same checks as in focusObjectStartComposing()
1449     if (!m_composingText.isEmpty() && !m_composingText.contains(QLatin1Char('\n'))
1450             && newAbsoluteCursorPos >= m_composingTextStart
1451             && newAbsoluteCursorPos <= m_composingTextStart + m_composingText.length())
1452         m_composingCursor = newAbsoluteCursorPos;
1453     else
1454         m_composingCursor = -1;
1455 
1456     QInputMethodEvent event;
1457     if (focusObjectIsComposing()) {
1458         QTextCharFormat underlined;
1459         underlined.setFontUnderline(true);
1460 
1461         event = QInputMethodEvent(m_composingText, {
1462             { QInputMethodEvent::TextFormat, 0, m_composingText.length(), underlined },
1463             { QInputMethodEvent::Cursor, m_composingCursor - m_composingTextStart, 1 }
1464         });
1465 
1466         if (oldComposingTextLen > 0 && !focusObjectWasComposing) {
1467             event.setCommitString({}, m_composingTextStart - effectiveAbsoluteCursorPos,
1468                                   oldComposingTextLen);
1469         }
1470     } else {
1471         event = QInputMethodEvent({}, {});
1472 
1473         if (focusObjectWasComposing) {
1474             event.setCommitString(m_composingText);
1475         } else {
1476             event.setCommitString(m_composingText,
1477                                   m_composingTextStart - effectiveAbsoluteCursorPos,
1478                                   oldComposingTextLen);
1479         }
1480     }
1481 
1482     if (m_composingText.isEmpty())
1483         clear();
1484 
1485     QGuiApplication::sendEvent(m_focusObject, &event);
1486 
1487     if (!focusObjectIsComposing() && newCursorPosition != 1) {
1488         // Move cursor using a separate event because if we have inserted or deleted a newline
1489         // character, then we are now inside an another block
1490 
1491         const int newBlockPos = getBlockPosition(
1492                 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAbsolutePosition));
1493 
1494         event = QInputMethodEvent({}, {
1495             { QInputMethodEvent::Selection, newAbsoluteCursorPos - newBlockPos, 0 }
1496         });
1497 
1498         QGuiApplication::sendEvent(m_focusObject, &event);
1499     }
1500 
1501     keyDown();
1502 
1503     return JNI_TRUE;
1504 }
1505 
1506 // Android docs say:
1507 // * start may be after end, same meaning as if swapped
1508 // * this function should not trigger updateSelection, but Android's native EditText does trigger it
1509 // * if start == end then we should stop composing
setComposingRegion(jint start,jint end)1510 jboolean QAndroidInputContext::setComposingRegion(jint start, jint end)
1511 {
1512     BatchEditLock batchEditLock(this);
1513 
1514     // Qt will not include the current preedit text in the query results, and interprets all
1515     // parameters relative to the text excluding the preedit. The simplest solution is therefore to
1516     // tell Qt that we commit the text before we set the new region. This may cause a little flicker, but is
1517     // much more robust than trying to keep the two different world views in sync
1518 
1519     finishComposingText();
1520 
1521     QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1522     if (query.isNull())
1523         return JNI_FALSE;
1524 
1525     if (start == end)
1526         return JNI_TRUE;
1527     if (start > end)
1528         qSwap(start, end);
1529 
1530     QString text = query->value(Qt::ImSurroundingText).toString();
1531     int textOffset = getBlockPosition(query);
1532 
1533     if (start < textOffset || end > textOffset + text.length()) {
1534         const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1535 
1536         if (end - textOffset > text.length()) {
1537             const QString after = query->value(Qt::ImTextAfterCursor).toString();
1538             const int additionalSuffixLen = after.length() - (text.length() - cursorPos);
1539 
1540             if (additionalSuffixLen > 0)
1541                 text += after.rightRef(additionalSuffixLen);
1542         }
1543 
1544         if (start < textOffset) {
1545             QString before = query->value(Qt::ImTextBeforeCursor).toString();
1546             before.chop(cursorPos);
1547 
1548             if (!before.isEmpty()) {
1549                 text = before + text;
1550                 textOffset -= before.length();
1551             }
1552         }
1553 
1554         if (start < textOffset || end - textOffset > text.length()) {
1555 #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
1556             qWarning("setComposingRegion: failed to retrieve text from composing region");
1557 #endif
1558 
1559             return JNI_TRUE;
1560         }
1561     }
1562 
1563     m_composingText = text.mid(start - textOffset, end - start);
1564     m_composingTextStart = start;
1565 
1566     return JNI_TRUE;
1567 }
1568 
setSelection(jint start,jint end)1569 jboolean QAndroidInputContext::setSelection(jint start, jint end)
1570 {
1571     QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1572     if (query.isNull())
1573         return JNI_FALSE;
1574 
1575     BatchEditLock batchEditLock(this);
1576 
1577     int blockPosition = getBlockPosition(query);
1578     int localCursorPos = start - blockPosition;
1579 
1580     if (focusObjectIsComposing() && start == end && start >= m_composingTextStart
1581             && start <= m_composingTextStart + m_composingText.length()) {
1582         // not actually changing the selection; just moving the
1583         // preedit cursor
1584         int localOldPos = query->value(Qt::ImCursorPosition).toInt();
1585         int pos = localCursorPos - localOldPos;
1586         QList<QInputMethodEvent::Attribute> attributes;
1587         attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, pos, 1));
1588 
1589         //but we have to tell Qt about the compose text all over again
1590 
1591         // Show compose text underlined
1592         QTextCharFormat underlined;
1593         underlined.setFontUnderline(true);
1594         attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat,0, m_composingText.length(),
1595                                                    QVariant(underlined)));
1596         m_composingCursor = start;
1597 
1598         QInputMethodEvent event(m_composingText, attributes);
1599         QGuiApplication::sendEvent(m_focusObject, &event);
1600     } else {
1601         // actually changing the selection
1602         focusObjectStopComposing();
1603         QList<QInputMethodEvent::Attribute> attributes;
1604         attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection,
1605                                                        localCursorPos,
1606                                                        end - start));
1607         QInputMethodEvent event({}, attributes);
1608         QGuiApplication::sendEvent(m_focusObject, &event);
1609     }
1610     return JNI_TRUE;
1611 }
1612 
selectAll()1613 jboolean QAndroidInputContext::selectAll()
1614 {
1615     BatchEditLock batchEditLock(this);
1616 
1617     focusObjectStopComposing();
1618     m_handleMode = ShowCursor;
1619     sendShortcut(QKeySequence::SelectAll);
1620     return JNI_TRUE;
1621 }
1622 
cut()1623 jboolean QAndroidInputContext::cut()
1624 {
1625     BatchEditLock batchEditLock(this);
1626 
1627     // This is probably not what native EditText would do, but normally if there is selection, then
1628     // there will be no composing region
1629     finishComposingText();
1630 
1631     m_handleMode = ShowCursor;
1632     sendShortcut(QKeySequence::Cut);
1633     return JNI_TRUE;
1634 }
1635 
copy()1636 jboolean QAndroidInputContext::copy()
1637 {
1638     BatchEditLock batchEditLock(this);
1639 
1640     focusObjectStopComposing();
1641     m_handleMode = ShowCursor;
1642     sendShortcut(QKeySequence::Copy);
1643     return JNI_TRUE;
1644 }
1645 
copyURL()1646 jboolean QAndroidInputContext::copyURL()
1647 {
1648 #warning TODO
1649     return JNI_FALSE;
1650 }
1651 
paste()1652 jboolean QAndroidInputContext::paste()
1653 {
1654     BatchEditLock batchEditLock(this);
1655 
1656     // TODO: This is not what native EditText does
1657     finishComposingText();
1658 
1659     m_handleMode = ShowCursor;
1660     sendShortcut(QKeySequence::Paste);
1661     return JNI_TRUE;
1662 }
1663 
sendShortcut(const QKeySequence & sequence)1664 void QAndroidInputContext::sendShortcut(const QKeySequence &sequence)
1665 {
1666     for (int i = 0; i < sequence.count(); ++i) {
1667         const int keys = sequence[i];
1668         Qt::Key key = Qt::Key(keys & ~Qt::KeyboardModifierMask);
1669         Qt::KeyboardModifiers mod = Qt::KeyboardModifiers(keys & Qt::KeyboardModifierMask);
1670 
1671         QKeyEvent pressEvent(QEvent::KeyPress, key, mod);
1672         QKeyEvent releaseEvent(QEvent::KeyRelease, key, mod);
1673 
1674         QGuiApplication::sendEvent(m_focusObject, &pressEvent);
1675         QGuiApplication::sendEvent(m_focusObject, &releaseEvent);
1676     }
1677 }
1678 
focusObjectInputMethodQuery(Qt::InputMethodQueries queries)1679 QSharedPointer<QInputMethodQueryEvent> QAndroidInputContext::focusObjectInputMethodQuery(Qt::InputMethodQueries queries) {
1680     if (!qGuiApp)
1681         return {};
1682 
1683     QObject *focusObject = qGuiApp->focusObject();
1684     if (!focusObject)
1685         return {};
1686 
1687     QInputMethodQueryEvent *ret = new QInputMethodQueryEvent(queries);
1688     QCoreApplication::sendEvent(focusObject, ret);
1689     return QSharedPointer<QInputMethodQueryEvent>(ret);
1690 }
1691 
sendInputMethodEvent(QInputMethodEvent * event)1692 void QAndroidInputContext::sendInputMethodEvent(QInputMethodEvent *event)
1693 {
1694     if (!qGuiApp)
1695         return;
1696 
1697     QObject *focusObject = qGuiApp->focusObject();
1698     if (!focusObject)
1699         return;
1700 
1701     QCoreApplication::sendEvent(focusObject, event);
1702 }
1703 
1704 QT_END_NAMESPACE
1705