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