Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qandroidinputcontext.cpp
Go to the documentation of this file.
1// Copyright (C) 2016 The Qt Company Ltd.
2// Copyright (C) 2012 BogDan Vatra <bogdan@kde.org>
3// Copyright (C) 2016 Olivier Goffart <ogoffart@woboq.com>
4// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
5// Qt-Security score:significant reason:default
6
7#include <android/log.h>
8
10#include "androidjnimain.h"
14#include "private/qhighdpiscaling_p.h"
15
16#include <QTextBoundaryFinder>
17#include <QTextCharFormat>
18#include <QtCore/QJniEnvironment>
19#include <QtCore/QJniObject>
20#include <qevent.h>
21#include <qguiapplication.h>
22#include <qinputmethod.h>
23#include <qsharedpointer.h>
24#if QT_CONFIG(accessibility)
25#include <qaccessible.h>
26#endif
27#include <qthread.h>
28#include <qwindow.h>
29#include <qpa/qplatformwindow.h>
30
32
33using namespace Qt::StringLiterals;
34
35namespace {
36
37class BatchEditLock
38{
39public:
40
41 explicit BatchEditLock(QAndroidInputContext *context)
42 : m_context(context)
43 {
44 m_context->beginBatchEdit();
45 }
46
47 ~BatchEditLock()
48 {
49 m_context->endBatchEdit();
50 }
51
52 BatchEditLock(const BatchEditLock &) = delete;
53 BatchEditLock &operator=(const BatchEditLock &) = delete;
54
55private:
56
57 QAndroidInputContext *m_context;
58};
59
60} // namespace anonymous
61
63static char const *const QtNativeInputConnectionClassName = "org/qtproject/qt/android/QtNativeInputConnection";
64static char const *const QtExtractedTextClassName = "org/qtproject/qt/android/QtExtractedText";
65static int m_selectHandleWidth = 0;
74
75static void runOnQtThread(const std::function<void()> &func)
76{
77 QtAndroidPrivate::AndroidDeadlockProtector protector(
78 u"QAndroidInputContext::runOnQtThread()"_s);
79 if (!protector.acquire())
80 return;
81 QMetaObject::invokeMethod(m_androidInputContext, "safeCall", Qt::BlockingQueuedConnection, Q_ARG(std::function<void()>, func));
82}
83
85{
87 return false;
88
89 const auto focusObject = m_androidInputContext->focusObject();
90 if (!focusObject)
91 return false;
92
93 if (!focusObject->property("inputMethodHints").isValid())
94 return false;
95
96 return true;
97}
98
99static jboolean beginBatchEdit(JNIEnv */*env*/, jobject /*thiz*/)
100{
101 if (!hasValidFocusObject())
102 return JNI_FALSE;
103
104 qCDebug(lcQpaInputMethods) << "@@@ BEGINBATCH";
105 jboolean res = JNI_FALSE;
106 runOnQtThread([&res]{res = m_androidInputContext->beginBatchEdit();});
107 return res;
108}
109
110static jboolean endBatchEdit(JNIEnv */*env*/, jobject /*thiz*/)
111{
112 if (!hasValidFocusObject())
113 return JNI_FALSE;
114
115 qCDebug(lcQpaInputMethods) << "@@@ ENDBATCH";
116
117 jboolean res = JNI_FALSE;
118 runOnQtThread([&res]{res = m_androidInputContext->endBatchEdit();});
119 return res;
120}
121
122
123static jboolean commitText(JNIEnv *env, jobject /*thiz*/, jstring text, jint newCursorPosition)
124{
125 if (!hasValidFocusObject())
126 return JNI_FALSE;
127
128 jboolean isCopy;
129 const jchar *jstr = env->GetStringChars(text, &isCopy);
130 QString str(reinterpret_cast<const QChar *>(jstr), env->GetStringLength(text));
131 env->ReleaseStringChars(text, jstr);
132
133 qCDebug(lcQpaInputMethods) << "@@@ COMMIT" << str << newCursorPosition;
134 jboolean res = JNI_FALSE;
135 runOnQtThread([&]{res = m_androidInputContext->commitText(str, newCursorPosition);});
136 return res;
137}
138
139static jboolean deleteSurroundingText(JNIEnv */*env*/, jobject /*thiz*/, jint leftLength, jint rightLength)
140{
141 if (!hasValidFocusObject())
142 return JNI_FALSE;
143
144 qCDebug(lcQpaInputMethods) << "@@@ DELETE" << leftLength << rightLength;
145 jboolean res = JNI_FALSE;
146 runOnQtThread([&]{res = m_androidInputContext->deleteSurroundingText(leftLength, rightLength);});
147 return res;
148}
149
150static jboolean finishComposingText(JNIEnv */*env*/, jobject /*thiz*/)
151{
152 if (!hasValidFocusObject())
153 return JNI_FALSE;
154
155 qCDebug(lcQpaInputMethods) << "@@@ FINISH";
156 jboolean res = JNI_FALSE;
157 runOnQtThread([&]{res = m_androidInputContext->finishComposingText();});
158 return res;
159}
160
161static jboolean replaceText(JNIEnv *env, jobject /*thiz*/, jint start, jint end, jstring text, jint newCursorPosition)
162{
163 if (!hasValidFocusObject())
164 return JNI_FALSE;
165
166 jboolean isCopy;
167 const jchar *jstr = env->GetStringChars(text, &isCopy);
168 QString str(reinterpret_cast<const QChar *>(jstr), env->GetStringLength(text));
169 env->ReleaseStringChars(text, jstr);
170
171 qCDebug(lcQpaInputMethods) << "@@@ REPLACE" << start << end << str << newCursorPosition;
172 jboolean res = JNI_FALSE;
173 runOnQtThread([&]{res = m_androidInputContext->replaceText(start, end, str, newCursorPosition);});
174
175 return res;
176}
177
178static jint getCursorCapsMode(JNIEnv */*env*/, jobject /*thiz*/, jint reqModes)
179{
181 return 0;
182
183 jint res = 0;
184 runOnQtThread([&]{res = m_androidInputContext->getCursorCapsMode(reqModes);});
185 return res;
186}
187
188static jobject getExtractedText(JNIEnv *env, jobject /*thiz*/, int hintMaxChars, int hintMaxLines, jint flags)
189{
191 return 0;
192
193 QAndroidInputContext::ExtractedText extractedText;
194 runOnQtThread([&]{extractedText = m_androidInputContext->getExtractedText(hintMaxChars, hintMaxLines, flags);});
195
196 qCDebug(lcQpaInputMethods) << "@@@ 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;
197
198 jobject object = env->NewObject(m_extractedTextClass, m_classConstructorMethodID);
199 env->SetIntField(object, m_partialStartOffsetFieldID, extractedText.partialStartOffset);
200 env->SetIntField(object, m_partialEndOffsetFieldID, extractedText.partialEndOffset);
201 env->SetIntField(object, m_selectionStartFieldID, extractedText.selectionStart);
202 env->SetIntField(object, m_selectionEndFieldID, extractedText.selectionEnd);
203 env->SetIntField(object, m_startOffsetFieldID, extractedText.startOffset);
204 env->SetObjectField(object,
205 m_textFieldID,
206 env->NewString(reinterpret_cast<const jchar *>(extractedText.text.constData()),
207 jsize(extractedText.text.length())));
208
209 return object;
210}
211
212static jstring getSelectedText(JNIEnv *env, jobject /*thiz*/, jint flags)
213{
215 return 0;
216
217 QString text;
218 runOnQtThread([&]{text = m_androidInputContext->getSelectedText(flags);});
219 qCDebug(lcQpaInputMethods) << "@@@ GETSEL" << text;
220 if (text.isEmpty())
221 return 0;
222 return env->NewString(reinterpret_cast<const jchar *>(text.constData()), jsize(text.length()));
223}
224
225static jstring getTextAfterCursor(JNIEnv *env, jobject /*thiz*/, jint length, jint flags)
226{
228 return 0;
229
230 QString text;
231 runOnQtThread([&]{text = m_androidInputContext->getTextAfterCursor(length, flags);});
232 qCDebug(lcQpaInputMethods) << "@@@ GETA" << length << text;
233 return env->NewString(reinterpret_cast<const jchar *>(text.constData()), jsize(text.length()));
234}
235
236static jstring getTextBeforeCursor(JNIEnv *env, jobject /*thiz*/, jint length, jint flags)
237{
239 return 0;
240
241 QString text;
242 runOnQtThread([&]{text = m_androidInputContext->getTextBeforeCursor(length, flags);});
243 qCDebug(lcQpaInputMethods) << "@@@ GETB" << length << text;
244 return env->NewString(reinterpret_cast<const jchar *>(text.constData()), jsize(text.length()));
245}
246
247static jboolean setComposingText(JNIEnv *env, jobject /*thiz*/, jstring text, jint newCursorPosition)
248{
249 if (!hasValidFocusObject())
250 return JNI_FALSE;
251
252 jboolean isCopy;
253 const jchar *jstr = env->GetStringChars(text, &isCopy);
254 QString str(reinterpret_cast<const QChar *>(jstr), env->GetStringLength(text));
255 env->ReleaseStringChars(text, jstr);
256
257 qCDebug(lcQpaInputMethods) << "@@@ SET" << str << newCursorPosition;
258 jboolean res = JNI_FALSE;
259 runOnQtThread([&]{res = m_androidInputContext->setComposingText(str, newCursorPosition);});
260 return res;
261}
262
263static jboolean setComposingRegion(JNIEnv */*env*/, jobject /*thiz*/, jint start, jint end)
264{
265 if (!hasValidFocusObject())
266 return JNI_FALSE;
267
268 qCDebug(lcQpaInputMethods) << "@@@ SETR" << start << end;
269 jboolean res = JNI_FALSE;
270 runOnQtThread([&]{res = m_androidInputContext->setComposingRegion(start, end);});
271 return res;
272}
273
274
275static jboolean setSelection(JNIEnv */*env*/, jobject /*thiz*/, jint start, jint end)
276{
277 if (!hasValidFocusObject())
278 return JNI_FALSE;
279
280 qCDebug(lcQpaInputMethods) << "@@@ SETSEL" << start << end;
281 jboolean res = JNI_FALSE;
282 runOnQtThread([&]{res = m_androidInputContext->setSelection(start, end);});
283 return res;
284
285}
286
287static jboolean selectAll(JNIEnv */*env*/, jobject /*thiz*/)
288{
289 if (!hasValidFocusObject())
290 return JNI_FALSE;
291
292 qCDebug(lcQpaInputMethods) << "@@@ SELALL";
293 jboolean res = JNI_FALSE;
294 runOnQtThread([&]{res = m_androidInputContext->selectAll();});
295 return res;
296}
297
298static jboolean cut(JNIEnv */*env*/, jobject /*thiz*/)
299{
300 if (!hasValidFocusObject())
301 return JNI_FALSE;
302
303 qCDebug(lcQpaInputMethods) << "@@@";
304 jboolean res = JNI_FALSE;
305 runOnQtThread([&]{res = m_androidInputContext->cut();});
306 return res;
307}
308
309static jboolean copy(JNIEnv */*env*/, jobject /*thiz*/)
310{
311 if (!hasValidFocusObject())
312 return JNI_FALSE;
313
314 qCDebug(lcQpaInputMethods) << "@@@";
315 jboolean res = JNI_FALSE;
316 runOnQtThread([&]{res = m_androidInputContext->copy();});
317 return res;
318}
319
320static jboolean copyURL(JNIEnv */*env*/, jobject /*thiz*/)
321{
322 if (!hasValidFocusObject())
323 return JNI_FALSE;
324
325 qCDebug(lcQpaInputMethods) << "@@@";
326 jboolean res = JNI_FALSE;
327 runOnQtThread([&]{res = m_androidInputContext->copyURL();});
328 return res;
329}
330
331static jboolean paste(JNIEnv */*env*/, jobject /*thiz*/)
332{
333 if (!hasValidFocusObject())
334 return JNI_FALSE;
335
336 qCDebug(lcQpaInputMethods) << "@@@ PASTE";
337 jboolean res = JNI_FALSE;
338 runOnQtThread([&]{res = m_androidInputContext->paste();});
339 return res;
340}
341
342static jboolean updateCursorPosition(JNIEnv */*env*/, jobject /*thiz*/)
343{
344 if (!hasValidFocusObject())
345 return JNI_FALSE;
346
347 qCDebug(lcQpaInputMethods) << "@@@ UPDATECURSORPOS";
348
350 return true;
351}
352
353static void reportFullscreenMode(JNIEnv */*env*/, jobject /*thiz*/, jboolean enabled)
354{
356 return;
357
358 runOnQtThread([&]{m_androidInputContext->reportFullscreenMode(enabled);});
359}
360
361static jboolean fullscreenMode(JNIEnv */*env*/, jobject /*thiz*/)
362{
364 return false;
365
366 return m_androidInputContext->fullscreenMode();
367}
368
370 {"beginBatchEdit", "()Z", (void *)beginBatchEdit},
371 {"endBatchEdit", "()Z", (void *)endBatchEdit},
372 {"commitText", "(Ljava/lang/String;I)Z", (void *)commitText},
373 {"deleteSurroundingText", "(II)Z", (void *)deleteSurroundingText},
374 {"finishComposingText", "()Z", (void *)finishComposingText},
375 {"getCursorCapsMode", "(I)I", (void *)getCursorCapsMode},
376 {"getExtractedText", "(III)Lorg/qtproject/qt/android/QtExtractedText;", (void *)getExtractedText},
377 {"getSelectedText", "(I)Ljava/lang/String;", (void *)getSelectedText},
378 {"getTextAfterCursor", "(II)Ljava/lang/String;", (void *)getTextAfterCursor},
379 {"getTextBeforeCursor", "(II)Ljava/lang/String;", (void *)getTextBeforeCursor},
380 {"replaceText", "(IILjava/lang/String;I)Z", (void *)replaceText},
381 {"setComposingText", "(Ljava/lang/String;I)Z", (void *)setComposingText},
382 {"setComposingRegion", "(II)Z", (void *)setComposingRegion},
383 {"setSelection", "(II)Z", (void *)setSelection},
384 {"selectAll", "()Z", (void *)selectAll},
385 {"cut", "()Z", (void *)cut},
386 {"copy", "()Z", (void *)copy},
387 {"copyURL", "()Z", (void *)copyURL},
388 {"paste", "()Z", (void *)paste},
389 {"updateCursorPosition", "()Z", (void *)updateCursorPosition},
390 {"reportFullscreenMode", "(Z)V", (void *)reportFullscreenMode},
391 {"fullscreenMode", "()Z", (void *)fullscreenMode}
392};
393
395{
396 QRect windowRect = QPlatformInputContext::inputItemRectangle().toRect();
397 QPlatformWindow *window = qGuiApp->focusWindow()->handle();
398 return QRect(window->mapToGlobal(windowRect.topLeft()), windowRect.size());
399}
400
403 , m_composingTextStart(-1)
404 , m_composingCursor(-1)
406 , m_batchEditNestingLevel(0)
407 , m_focusObject(0)
408 , m_fullScreenMode(false)
409{
410 QJniEnvironment env;
411 jclass clazz = env.findClass(QtNativeInputConnectionClassName);
412 if (Q_UNLIKELY(!clazz)) {
413 qCritical() << "Native registration unable to find class '"
415 << '\'';
416 return;
417 }
418
419 if (Q_UNLIKELY(env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0)) {
420 qCritical() << "RegisterNatives failed for '"
422 << '\'';
423 return;
424 }
425
426 clazz = env.findClass(QtExtractedTextClassName);
427 if (Q_UNLIKELY(!clazz)) {
428 qCritical() << "Native registration unable to find class '"
430 << '\'';
431 return;
432 }
433
434 m_extractedTextClass = static_cast<jclass>(env->NewGlobalRef(clazz));
435 m_classConstructorMethodID = env->GetMethodID(m_extractedTextClass, "<init>", "()V");
436 if (Q_UNLIKELY(!m_classConstructorMethodID)) {
437 qCritical("GetMethodID failed");
438 return;
439 }
440
441 m_partialEndOffsetFieldID = env->GetFieldID(m_extractedTextClass, "partialEndOffset", "I");
442 if (Q_UNLIKELY(!m_partialEndOffsetFieldID)) {
443 qCritical("Can't find field partialEndOffset");
444 return;
445 }
446
447 m_partialStartOffsetFieldID = env->GetFieldID(m_extractedTextClass, "partialStartOffset", "I");
448 if (Q_UNLIKELY(!m_partialStartOffsetFieldID)) {
449 qCritical("Can't find field partialStartOffset");
450 return;
451 }
452
453 m_selectionEndFieldID = env->GetFieldID(m_extractedTextClass, "selectionEnd", "I");
454 if (Q_UNLIKELY(!m_selectionEndFieldID)) {
455 qCritical("Can't find field selectionEnd");
456 return;
457 }
458
459 m_selectionStartFieldID = env->GetFieldID(m_extractedTextClass, "selectionStart", "I");
460 if (Q_UNLIKELY(!m_selectionStartFieldID)) {
461 qCritical("Can't find field selectionStart");
462 return;
463 }
464
465 m_startOffsetFieldID = env->GetFieldID(m_extractedTextClass, "startOffset", "I");
466 if (Q_UNLIKELY(!m_startOffsetFieldID)) {
467 qCritical("Can't find field startOffset");
468 return;
469 }
470
471 m_textFieldID = env->GetFieldID(m_extractedTextClass, "text", "Ljava/lang/String;");
472 if (Q_UNLIKELY(!m_textFieldID)) {
473 qCritical("Can't find field text");
474 return;
475 }
476 qRegisterMetaType<QInputMethodEvent *>("QInputMethodEvent*");
477 qRegisterMetaType<QInputMethodQueryEvent *>("QInputMethodQueryEvent*");
479
480 QObject::connect(QGuiApplication::inputMethod(), &QInputMethod::cursorRectangleChanged,
481 this, &QAndroidInputContext::updateSelectionHandles);
482 QObject::connect(QGuiApplication::inputMethod(), &QInputMethod::anchorRectangleChanged,
483 this, &QAndroidInputContext::updateSelectionHandles);
484 QObject::connect(QGuiApplication::inputMethod(), &QInputMethod::inputItemClipRectangleChanged, this, [this]{
485 auto im = qGuiApp->inputMethod();
486 if (!im->inputItemClipRectangle().contains(im->anchorRectangle()) ||
487 !im->inputItemClipRectangle().contains(im->cursorRectangle())) {
488 m_handleMode = Hidden;
489 updateSelectionHandles();
490 }
491 });
492 m_hideCursorHandleTimer.setInterval(4000);
493 m_hideCursorHandleTimer.setSingleShot(true);
494 m_hideCursorHandleTimer.setTimerType(Qt::VeryCoarseTimer);
495 connect(&m_hideCursorHandleTimer, &QTimer::timeout, this, [this]{
496 m_handleMode = Hidden;
498 });
499}
500
512
517
518// cursor position getter that also works with editors that have not been updated to the new API
519static inline int getAbsoluteCursorPosition(const QSharedPointer<QInputMethodQueryEvent> &query)
520{
521 QVariant absolutePos = query->value(Qt::ImAbsolutePosition);
522 return absolutePos.isValid() ? absolutePos.toInt() : query->value(Qt::ImCursorPosition).toInt();
523}
524
525// position of the start of the current block
526static inline int getBlockPosition(const QSharedPointer<QInputMethodQueryEvent> &query)
527{
528 QVariant absolutePos = query->value(Qt::ImAbsolutePosition);
529 return absolutePos.isValid() ? absolutePos.toInt() - query->value(Qt::ImCursorPosition).toInt() : 0;
530}
531
533{
534 focusObjectStopComposing();
535 clear();
536 m_batchEditNestingLevel = 0;
537 m_handleMode = Hidden;
538 if (qGuiApp->focusObject()) {
539 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(Qt::ImEnabled);
540 if (!query.isNull() && query->value(Qt::ImEnabled).toBool()) {
541 // reset() runs on the focus change that an accessibility
542 // setFocusAction triggers; resetSoftwareKeyboard()'s restartInput()
543 // re-prompts the IME on some keyboards, which would re-open the
544 // panel we are suppressing in showInputPanel(). Skip it for the
545 // accessibility-focus path so the two stay consistent.
546 if (!m_accessibilityFocusInProgress)
548 return;
549 }
550 }
552}
553
555{
556 focusObjectStopComposing();
557}
558
560{
561 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
562 if (!query.isNull() && m_batchEditNestingLevel == 0) {
563 const int cursorPos = getAbsoluteCursorPosition(query);
564 const int composeLength = m_composingText.length();
565
566 //Q_ASSERT(m_composingText.isEmpty() == (m_composingTextStart == -1));
567 if (m_composingText.isEmpty() != (m_composingTextStart == -1))
568 qWarning() << "Input method out of sync" << m_composingText << m_composingTextStart;
569
570 int realSelectionStart = cursorPos;
571 int realSelectionEnd = cursorPos;
572
573 int cpos = query->value(Qt::ImCursorPosition).toInt();
574 int anchor = query->value(Qt::ImAnchorPosition).toInt();
575 if (cpos != anchor) {
576 if (!m_composingText.isEmpty()) {
577 qWarning("Selecting text while preediting may give unpredictable results.");
578 focusObjectStopComposing();
579 }
580 int blockPos = getBlockPosition(query);
581 realSelectionStart = blockPos + cpos;
582 realSelectionEnd = blockPos + anchor;
583 }
584 // Qt's idea of the cursor position is the start of the preedit area, so we maintain our own preedit cursor pos
585 if (focusObjectIsComposing())
586 realSelectionStart = realSelectionEnd = m_composingCursor;
587
588 // Some keyboards misbahave when selStart > selEnd
589 if (realSelectionStart > realSelectionEnd)
590 std::swap(realSelectionStart, realSelectionEnd);
591
592 QtAndroidInput::updateSelection(realSelectionStart, realSelectionEnd,
593 m_composingTextStart, m_composingTextStart + composeLength); // pre-edit text
594 }
595}
596
597bool QAndroidInputContext::isImhNoEditMenuSet()
598{
599 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
600 if (query.isNull())
601 return false;
602 return query->value(Qt::ImHints).toUInt() & Qt::ImhNoEditMenu;
603}
604
605bool QAndroidInputContext::isImhNoTextHandlesSet()
606{
607 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
608 if (query.isNull())
609 return false;
610 return query->value(Qt::ImHints).toUInt() & Qt::ImhNoTextHandles;
611}
612
614{
615 if (m_fullScreenMode) {
616 QtAndroidInput::updateHandles(Hidden);
617 return;
618 }
619 static bool noHandles = qEnvironmentVariableIntValue("QT_QPA_NO_TEXT_HANDLES");
620 if (noHandles || !m_focusObject)
621 return;
622
623 if (isImhNoTextHandlesSet()) {
624 QtAndroidInput::updateHandles(Hidden);
625 return;
626 }
627
628 auto im = qGuiApp->inputMethod();
629
630 QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition | Qt::ImEnabled
631 | Qt::ImCurrentSelection | Qt::ImHints | Qt::ImSurroundingText
632 | Qt::ImReadOnly);
633 QCoreApplication::sendEvent(m_focusObject, &query);
634
635 int cpos = query.value(Qt::ImCursorPosition).toInt();
636 int anchor = query.value(Qt::ImAnchorPosition).toInt();
637 const QVariant readOnlyVariant = query.value(Qt::ImReadOnly);
638 bool readOnly = readOnlyVariant.toBool();
639 QPlatformWindow *qPlatformWindow = qGuiApp->focusWindow()->handle();
640
641 if (!readOnly && ((m_handleMode & 0xff) == Hidden)) {
642 QtAndroidInput::updateHandles(Hidden);
643 return;
644 }
645
646 if ( cpos == anchor && (!readOnlyVariant.isValid() || readOnly)) {
647 QtAndroidInput::updateHandles(Hidden);
648 return;
649 }
650
651 if (cpos == anchor || im->anchorRectangle().isNull()) {
652 auto curRect = cursorRectangle();
653 QPoint cursorPointGlobal = QPoint(curRect.x() + (curRect.width() / 2), curRect.y() + curRect.height());
654 QPoint cursorPoint(curRect.center().x(), curRect.bottom());
655 int x = curRect.x();
656 int y = curRect.y();
657
658 // Use x and y for the editMenuPoint from the cursorPointGlobal when the cursor is in the Dialog
659 if (cursorPointGlobal != cursorPoint) {
660 x = cursorPointGlobal.x();
661 y = cursorPointGlobal.y();
662 }
663
664 QPoint editMenuPoint(x, y);
665 m_handleMode &= ShowEditPopup;
666 m_handleMode |= ShowCursor;
667 uint32_t buttons = 0;
668 const bool withEditMenu = !isImhNoEditMenuSet();
669 if (withEditMenu) {
670 buttons = readOnly ? 0 : EditContext::PasteButton;
671 if (!query.value(Qt::ImSurroundingText).toString().isEmpty())
673 }
674 QtAndroidInput::updateHandles(m_handleMode, editMenuPoint, buttons, cursorPointGlobal);
675 m_hideCursorHandleTimer.start();
676
677 return;
678 }
679
680 m_handleMode = ShowSelection | ShowEditPopup ;
681 auto leftRect = cursorRectangle();
682 auto rightRect = anchorRectangle();
683 if (cpos > anchor)
684 std::swap(leftRect, rightRect);
685 //Move the left or right select handle to the center from the screen edge
686 //the select handle is close to or over the screen edge. Otherwise, the
687 //select handle might go out of the screen and it would be impossible to drag.
688 QPoint leftPoint(qPlatformWindow->mapToGlobal(leftRect.bottomLeft().toPoint()));
689 QPoint rightPoint(qPlatformWindow->mapToGlobal(rightRect.bottomRight().toPoint()));
690
692 if (platformIntegration) {
693 if (m_selectHandleWidth == 0)
695
696 int rightSideOfScreen = platformIntegration->screen()->availableGeometry().right();
697 if (leftPoint.x() < m_selectHandleWidth)
698 leftPoint.setX(m_selectHandleWidth);
699 leftPoint = qPlatformWindow->mapFromGlobal(leftPoint);
700
701 if (rightPoint.x() > rightSideOfScreen - m_selectHandleWidth)
702 rightPoint.setX(rightSideOfScreen - m_selectHandleWidth);
703 rightPoint = qPlatformWindow->mapFromGlobal(rightPoint);
704
705 QPoint editPoint(leftRect.united(rightRect).topLeft().toPoint());
706 uint32_t buttons = 0;
707 const bool withEditMenu = !isImhNoEditMenuSet();
708 if (withEditMenu) {
711 }
712
713 QtAndroidInput::updateHandles(m_handleMode, editPoint, buttons, leftPoint, rightPoint,
714 query.value(Qt::ImCurrentSelection).toString().isRightToLeft());
715 m_hideCursorHandleTimer.stop();
716 }
717}
718
719/*
720 Called from Java when a cursor/selection handle was dragged to a new position
721
722 handleId of 1 means the cursor handle, 2 means the left handle, 3 means the right handle
723 */
724void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y)
725{
726 if (m_batchEditNestingLevel != 0) {
727 qWarning() << "QAndroidInputContext::handleLocationChanged returned";
728 return;
729 }
730 QPoint point(x, y);
731
732 // The handle is down of the cursor, but we want the position in the middle.
733 QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition
734 | Qt::ImAbsolutePosition | Qt::ImCurrentSelection);
735 QCoreApplication::sendEvent(m_focusObject, &query);
736 int cpos = query.value(Qt::ImCursorPosition).toInt();
737 int anchor = query.value(Qt::ImAnchorPosition).toInt();
738 auto leftRect = cursorRectangle();
739 auto rightRect = anchorRectangle();
740 if (cpos > anchor)
741 std::swap(leftRect, rightRect);
742
743 // Do not allow dragging left handle below right handle, or right handle above left handle
744 if (handleId == 2 && point.y() > rightRect.center().y()) {
745 point.setY(rightRect.center().y());
746 } else if (handleId == 3 && point.y() < leftRect.center().y()) {
747 point.setY(leftRect.center().y());
748 }
749
750 bool ok;
751 auto object = m_focusObject->parent();
752 int dialogMoveX = 0;
753 while (object) {
754 if (QString::compare(object->metaObject()->className(),
755 "QDialog", Qt::CaseInsensitive) == 0) {
756 dialogMoveX += object->property("x").toInt();
757 }
758 object = object->parent();
759 };
760
761 auto position =
762 QPointF(QHighDpi::fromNativePixels(point, QGuiApplication::focusWindow()));
763 const QPointF fixedPosition = QPointF(position.x() - dialogMoveX, position.y());
764 const QInputMethod *im = QGuiApplication::inputMethod();
765 const QTransform mapToLocal = im->inputItemTransform().inverted();
766 const int handlePos = im->queryFocusObject(Qt::ImCursorPosition, mapToLocal.map(fixedPosition)).toInt(&ok);
767
768 if (!ok)
769 return;
770
771 int newCpos = cpos;
772 int newAnchor = anchor;
773 if (newAnchor > newCpos)
774 std::swap(newAnchor, newCpos);
775
776 if (handleId == 1) {
777 newCpos = handlePos;
778 newAnchor = handlePos;
779 } else if (handleId == 2) {
780 newAnchor = handlePos;
781 } else if (handleId == 3) {
782 newCpos = handlePos;
783 }
784
785 /*
786 Do not allow clearing selection by dragging selection handles and do not allow swapping
787 selection handles for consistency with Android's native text editing controls. Ensure that at
788 least one symbol remains selected.
789 */
790 if ((handleId == 2 || handleId == 3) && newCpos <= newAnchor) {
791 QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme,
792 query.value(Qt::ImCurrentSelection).toString());
793
794 const int oldSelectionStartPos = qMin(cpos, anchor);
795
796 if (handleId == 2) {
797 finder.toEnd();
798 finder.toPreviousBoundary();
799 newAnchor = finder.position() + oldSelectionStartPos;
800 } else {
801 finder.toStart();
802 finder.toNextBoundary();
803 newCpos = finder.position() + oldSelectionStartPos;
804 }
805 }
806
807 // Check if handle has been dragged far enough
808 if (!focusObjectIsComposing() && newCpos == cpos && newAnchor == anchor)
809 return;
810
811 /*
812 If the editor is currently in composing state, we have to compare newCpos with
813 m_composingCursor instead of cpos. And since there is nothing to compare with newAnchor, we
814 perform the check only when user drags the cursor handle.
815 */
816 if (focusObjectIsComposing() && handleId == 1) {
817 int absoluteCpos = query.value(Qt::ImAbsolutePosition).toInt(&ok);
818 if (!ok)
819 absoluteCpos = cpos;
820 const int blockPos = absoluteCpos - cpos;
821
822 if (blockPos + newCpos == m_composingCursor)
823 return;
824 }
825
826 BatchEditLock batchEditLock(this);
827
828 focusObjectStopComposing();
829
830 QList<QInputMethodEvent::Attribute> attributes;
831 attributes.append({ QInputMethodEvent::Selection, newAnchor, newCpos - newAnchor });
832 if (newCpos != newAnchor)
833 attributes.append({ QInputMethodEvent::Cursor, 0, 0 });
834
835 QInputMethodEvent event(QString(), attributes);
836 QGuiApplication::sendEvent(m_focusObject, &event);
837}
838
840{
841 if (m_focusObject && screenInputItemRectangle().contains(x, y)) {
842 // If the user touch the input rectangle, we can show the cursor handle
843 m_handleMode = ShowCursor;
844 // The VK will appear in a moment, stop the timer
845 m_hideCursorHandleTimer.stop();
846
847 if (focusObjectIsComposing()) {
848 const int curBlockPos = getBlockPosition(
849 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAbsolutePosition));
850 const int touchPosition = curBlockPos
851 + queryFocusObject(Qt::ImCursorPosition, QPointF(x, y)).toInt();
852 if (touchPosition != m_composingCursor)
853 focusObjectStopComposing();
854 }
855
856 // Check if cursor is visible in focused window before updating handles
857 QPlatformWindow *window = qGuiApp->focusWindow()->handle();
858 const QRectF curRect = cursorRectangle();
859 const QPoint cursorGlobalPoint = window->mapToGlobal(QPoint(curRect.x(), curRect.y()));
860 const QRect windowRect = QPlatformInputContext::inputItemClipRectangle().toRect();
861 const QRect windowGlobalRect = QRect(window->mapToGlobal(windowRect.topLeft()), windowRect.size());
862
863 if (windowGlobalRect.contains(cursorGlobalPoint.x(), cursorGlobalPoint.y()))
865 }
866}
867
869{
870 static bool noHandles = qEnvironmentVariableIntValue("QT_QPA_NO_TEXT_HANDLES");
871 if (noHandles)
872 return;
873
874 if (m_focusObject && screenInputItemRectangle().contains(x, y)) {
875 BatchEditLock batchEditLock(this);
876
877 focusObjectStopComposing();
878 const QPointF touchPoint(x, y);
879 setSelectionOnFocusObject(touchPoint, touchPoint);
880
881 QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition | Qt::ImTextBeforeCursor | Qt::ImTextAfterCursor);
882 QCoreApplication::sendEvent(m_focusObject, &query);
883 int cursor = query.value(Qt::ImCursorPosition).toInt();
884 int anchor = cursor;
885 QString before = query.value(Qt::ImTextBeforeCursor).toString();
886 QString after = query.value(Qt::ImTextAfterCursor).toString();
887 for (const auto &ch : after) {
888 if (!ch.isLetterOrNumber())
889 break;
890 ++anchor;
891 }
892
893 for (auto itch = before.rbegin(); itch != after.rend(); ++itch) {
894 if (!itch->isLetterOrNumber())
895 break;
896 --cursor;
897 }
898 if (cursor == anchor || cursor < 0 || cursor - anchor > 500) {
899 m_handleMode = ShowCursor | ShowEditPopup;
901 return;
902 }
903 QList<QInputMethodEvent::Attribute> imAttributes;
904 imAttributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, 0, 0, QVariant()));
905 imAttributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection, anchor, cursor - anchor, QVariant()));
906 QInputMethodEvent event(QString(), imAttributes);
907 QGuiApplication::sendEvent(m_focusObject, &event);
908
909 m_handleMode = ShowSelection | ShowEditPopup;
911 }
912}
913
915{
916 if (m_handleMode) {
917 // When the user enter text on the keyboard, we hide the cursor handle
918 m_handleMode = Hidden;
920 }
921}
922
924{
925 if (m_handleMode & ShowSelection) {
926 m_handleMode = Hidden;
928 } else {
929 m_hideCursorHandleTimer.start();
930 }
931}
932
933void QAndroidInputContext::update(Qt::InputMethodQueries queries)
934{
935 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(queries);
936 if (query.isNull())
937 return;
938#if QT_CONFIG(accessibility)
939 // Editors report every applied text change here, including edits that bypass
940 // the IME mutators (key-event backspace, hardware keys, programmatic changes).
941 // Announce them; a change arriving mid-batch is deferred to endBatchEdit() so
942 // each committed edit is announced exactly once.
943 if (queries & Qt::ImSurroundingText) {
944 if (m_batchEditNestingLevel == 0)
945 notifyTextChangedForAccessibility();
946 else if (QAccessible::isActive())
947 m_a11yTextEditPending = true;
948 }
949#endif
950#warning TODO extract the needed data from query
951}
952
953void QAndroidInputContext::invokeAction(QInputMethod::Action action, int cursorPosition)
954{
955#warning TODO Handle at least QInputMethod::ContextMenu action
956 Q_UNUSED(action);
957 Q_UNUSED(cursorPosition);
958 //### click should be passed to the IM, but in the meantime it's better to ignore it than to do something wrong
959 // if (action == QInputMethod::Click)
960 // commit();
961}
962
964{
965 return QtAndroidInput::softwareKeyboardRect();
966}
967
969{
970 return false;
971}
972
974{
975 if (QGuiApplication::applicationState() != Qt::ApplicationActive) {
976 connect(qGuiApp, SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(showInputPanelLater(Qt::ApplicationState)));
977 return;
978 }
979
980 // Don't open the keyboard for the input focus that an accessibility
981 // setFocusAction grants while a screen reader navigates fields; a
982 // deliberate activation (double-tap) still opens it normally.
983 if (m_accessibilityFocusInProgress)
984 return;
985
986 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
987 if (query.isNull())
988 return;
989
990 if (!qGuiApp->focusWindow()->handle())
991 return; // not a real window, probably VR/XR
992
993 disconnect(m_updateCursorPosConnection);
994 m_updateCursorPosConnection = {};
995
996 if (qGuiApp->focusObject()->metaObject()->indexOfSignal("cursorPositionChanged(int,int)") >= 0) // QLineEdit breaks the pattern
997 m_updateCursorPosConnection = connect(qGuiApp->focusObject(), SIGNAL(cursorPositionChanged(int,int)), this, SLOT(updateCursorPosition()));
998 else if (qGuiApp->focusObject()->metaObject()->indexOfSignal("cursorPositionChanged()") >= 0)
999 m_updateCursorPosConnection = connect(qGuiApp->focusObject(), SIGNAL(cursorPositionChanged()), this, SLOT(updateCursorPosition()));
1000
1001 QRect rect = QPlatformInputContext::inputItemRectangle().toRect();
1002 QtAndroidInput::showSoftwareKeyboard(rect.left(), rect.top(), rect.width(), rect.height(),
1003 query->value(Qt::ImHints).toUInt(),
1004 query->value(Qt::ImEnterKeyType).toUInt());
1005}
1006
1007void QAndroidInputContext::showInputPanelLater(Qt::ApplicationState state)
1008{
1009 if (state != Qt::ApplicationActive)
1010 return;
1011 disconnect(qGuiApp, SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(showInputPanelLater(Qt::ApplicationState)));
1013}
1014
1015void QAndroidInputContext::safeCall(const std::function<void()> &func, Qt::ConnectionType conType)
1016{
1017 if (qGuiApp->thread() == QThread::currentThread())
1018 func();
1019 else
1020 QMetaObject::invokeMethod(this, "safeCall", conType, Q_ARG(std::function<void()>, func));
1021}
1022
1027
1032
1034{
1035 return m_composingText.length();
1036}
1037
1039{
1040 m_composingText.clear();
1041 m_composingTextStart = -1;
1042 m_composingCursor = -1;
1043 m_extractedText.clear();
1044}
1045
1046
1048{
1049 return m_focusObject;
1050}
1051
1052void QAndroidInputContext::setFocusObject(QObject *object)
1053{
1054 if (object != m_focusObject) {
1055 focusObjectStopComposing();
1056 m_focusObject = object;
1057 reset();
1058#if QT_CONFIG(accessibility)
1059 // (Re)capture the text baseline for the new focus object (when
1060 // accessibility is active; otherwise just invalidate and let the lazy
1061 // fallbacks capture) so both the IME (endBatchEdit) and non-IME
1062 // (update()) announcement paths diff against the field's pre-edit
1063 // content. Capturing fires nothing — the announcement paths are gated
1064 // separately. An unanswered query leaves the baseline invalid so the
1065 // baseline-adopt guard handles it instead of diffing against garbage.
1066 m_a11yTextEditPending = false;
1067 m_a11yLastText.clear();
1068 m_a11yBaselineValid = false;
1069 if (m_focusObject && QAccessible::isActive()) {
1070 QInputMethodQueryEvent query(Qt::ImSurroundingText);
1071 QCoreApplication::sendEvent(m_focusObject, &query);
1072 const QVariant surroundingText = query.value(Qt::ImSurroundingText);
1073 if (surroundingText.isValid()) {
1074 m_a11yLastText = surroundingText.toString();
1075 m_a11yBaselineValid = true;
1076 }
1077 }
1078#endif
1079 }
1081}
1082
1083#if QT_CONFIG(accessibility)
1084void QAndroidInputContext::markTextEditForAccessibility()
1085{
1086 if (!QAccessible::isActive() || !m_focusObject)
1087 return;
1088 m_a11yTextEditPending = true;
1089 // Fallback baseline capture — setFocusObject() captures eagerly, but only
1090 // when accessibility was active at focus time (and only if the query was
1091 // answered). If it wasn't, capture here before this edit is applied so the
1092 // diff in notifyTextChangedForAccessibility() is accurate.
1093 if (!m_a11yBaselineValid) {
1094 QInputMethodQueryEvent query(Qt::ImSurroundingText);
1095 QCoreApplication::sendEvent(m_focusObject, &query);
1096 m_a11yLastText = query.value(Qt::ImSurroundingText).toString();
1097 m_a11yBaselineValid = true;
1098 }
1099}
1100
1101void QAndroidInputContext::notifyTextChangedForAccessibility()
1102{
1103 if (!QAccessible::isActive() || !m_focusObject)
1104 return;
1105
1106 QInputMethodQueryEvent query(Qt::ImSurroundingText);
1107 QCoreApplication::sendEvent(m_focusObject, &query);
1108 const QString after = query.value(Qt::ImSurroundingText).toString();
1109 if (!m_a11yBaselineValid) {
1110 // No pre-edit baseline to diff against — adopt the current text and stay
1111 // silent rather than announcing a bogus whole-field change.
1112 m_a11yLastText = after;
1113 m_a11yBaselineValid = true;
1114 return;
1115 }
1116 const QString before = m_a11yLastText;
1117 if (after == before)
1118 return;
1119 m_a11yLastText = after;
1120
1121 // Character-level diff: the common prefix and suffix bound the changed span,
1122 // giving TalkBack {fromIndex, addedCount, removedCount} to echo just the
1123 // inserted/deleted characters.
1124 const int minLen = qMin(before.size(), after.size());
1125 int prefix = 0;
1126 while (prefix < minLen && before.at(prefix) == after.at(prefix))
1127 ++prefix;
1128 // Don't split a surrogate pair: fromIndex must be a code-point boundary, or
1129 // Android/TalkBack will mis-handle the (non-BMP, e.g. emoji) change.
1130 if (prefix > 0 && before.at(prefix - 1).isHighSurrogate())
1131 --prefix;
1132 int suffix = 0;
1133 while (suffix < minLen - prefix
1134 && before.at(before.size() - 1 - suffix) == after.at(after.size() - 1 - suffix))
1135 ++suffix;
1136 if (suffix > 0 && before.at(before.size() - suffix).isLowSurrogate())
1137 --suffix;
1138 const int removedCount = int(before.size()) - prefix - suffix;
1139 const int addedCount = int(after.size()) - prefix - suffix;
1140
1141 // Pass the input-focus object's accessible id; the Java side sources the
1142 // event from the accessibility-focused virtual view (which may differ).
1143 uint focusUid = 0;
1144 if (QAccessibleInterface *iface = QAccessible::queryAccessibleInterface(m_focusObject))
1145 focusUid = QAccessible::uniqueId(iface);
1146
1147 QtAndroid::notifyTextChanged(focusUid, after, before, prefix, addedCount, removedCount);
1148}
1149#endif
1150
1152{
1153 ++m_batchEditNestingLevel;
1154 return JNI_TRUE;
1155}
1156
1158{
1159 if (--m_batchEditNestingLevel == 0) { //ending batch edit mode
1160 focusObjectStartComposing();
1162#if QT_CONFIG(accessibility)
1163 // Announce the change to TalkBack once the edit is applied — but only if
1164 // this batch actually changed text (flag set by the IME mutators, or by
1165 // update() observing a mid-batch ImSurroundingText change). Focus-time
1166 // batches change nothing, set no flag, and so can't fire a
1167 // (label-clobbering) text-change event. Text changes outside a batch
1168 // (key events, programmatic edits) are announced from update() directly,
1169 // which also keeps the baseline synced.
1170 if (m_a11yTextEditPending) {
1171 m_a11yTextEditPending = false;
1172 notifyTextChangedForAccessibility();
1173 }
1174#endif
1175 }
1176 return JNI_TRUE;
1177}
1178
1179/*
1180 Android docs say: This behaves like calling setComposingText(text, newCursorPosition) then
1181 finishComposingText().
1182*/
1183jboolean QAndroidInputContext::commitText(const QString &text, jint newCursorPosition)
1184{
1185 BatchEditLock batchEditLock(this);
1186 return setComposingText(text, newCursorPosition) && finishComposingText();
1187}
1188
1189jboolean QAndroidInputContext::deleteSurroundingText(jint leftLength, jint rightLength)
1190{
1191 BatchEditLock batchEditLock(this);
1192#if QT_CONFIG(accessibility)
1193 markTextEditForAccessibility();
1194#endif
1195
1196 focusObjectStopComposing();
1197
1198 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1199 if (query.isNull())
1200 return JNI_TRUE;
1201
1202 if (leftLength < 0) {
1203 rightLength += -leftLength;
1204 leftLength = 0;
1205 }
1206
1207 const int initialBlockPos = getBlockPosition(query);
1208 const int initialCursorPos = getAbsoluteCursorPosition(query);
1209 const int initialAnchorPos = initialBlockPos + query->value(Qt::ImAnchorPosition).toInt();
1210
1211 /*
1212 According to documentation, we should delete leftLength characters before current selection
1213 and rightLength characters after current selection (without affecting selection). But that is
1214 absolutely not what Android's native EditText does. It deletes leftLength characters before
1215 min(selection start, composing region start) and rightLength characters after max(selection
1216 end, composing region end). There are no known keyboards that depend on this behavior, but
1217 it is better to be consistent with EditText behavior, because there definitely should be no
1218 keyboards that depend on documented behavior.
1219 */
1220 const int leftEnd =
1221 m_composingText.isEmpty()
1222 ? qMin(initialCursorPos, initialAnchorPos)
1223 : qMin(qMin(initialCursorPos, initialAnchorPos), m_composingTextStart);
1224
1225 const int rightBegin =
1226 m_composingText.isEmpty()
1227 ? qMax(initialCursorPos, initialAnchorPos)
1228 : qMax(qMax(initialCursorPos, initialAnchorPos),
1229 m_composingTextStart + m_composingText.length());
1230
1231 int textBeforeCursorLen;
1232 int textAfterCursorLen;
1233
1234 QVariant textBeforeCursor = query->value(Qt::ImTextBeforeCursor);
1235 QVariant textAfterCursor = query->value(Qt::ImTextAfterCursor);
1236 if (textBeforeCursor.isValid() && textAfterCursor.isValid()) {
1237 textBeforeCursorLen = textBeforeCursor.toString().length();
1238 textAfterCursorLen = textAfterCursor.toString().length();
1239 } else {
1240 textBeforeCursorLen = initialCursorPos - initialBlockPos;
1241 textAfterCursorLen =
1242 query->value(Qt::ImSurroundingText).toString().length() - textBeforeCursorLen;
1243 }
1244
1245 leftLength = qMin(qMax(0, textBeforeCursorLen - (initialCursorPos - leftEnd)), leftLength);
1246 rightLength = qMin(qMax(0, textAfterCursorLen - (rightBegin - initialCursorPos)), rightLength);
1247
1248 if (leftLength == 0 && rightLength == 0)
1249 return JNI_TRUE;
1250
1251 if (leftEnd == rightBegin) {
1252 // We have no selection and no composing region; we can do everything using one event
1253 QInputMethodEvent event;
1254 event.setCommitString({}, -leftLength, leftLength + rightLength);
1255 QGuiApplication::sendEvent(m_focusObject, &event);
1256 } else {
1257 if (initialCursorPos != initialAnchorPos) {
1258 QInputMethodEvent event({}, {
1259 { QInputMethodEvent::Selection, initialCursorPos - initialBlockPos, 0 }
1260 });
1261
1262 QGuiApplication::sendEvent(m_focusObject, &event);
1263 }
1264
1265 int currentCursorPos = initialCursorPos;
1266
1267 if (rightLength > 0) {
1268 QInputMethodEvent event;
1269 event.setCommitString({}, rightBegin - currentCursorPos, rightLength);
1270 QGuiApplication::sendEvent(m_focusObject, &event);
1271
1272 currentCursorPos = rightBegin;
1273 }
1274
1275 if (leftLength > 0) {
1276 const int leftBegin = leftEnd - leftLength;
1277
1278 QInputMethodEvent event;
1279 event.setCommitString({}, leftBegin - currentCursorPos, leftLength);
1280 QGuiApplication::sendEvent(m_focusObject, &event);
1281
1282 currentCursorPos = leftBegin;
1283
1284 if (!m_composingText.isEmpty())
1285 m_composingTextStart -= leftLength;
1286 }
1287
1288 // Restore cursor position or selection
1289 if (currentCursorPos != initialCursorPos - leftLength
1290 || initialCursorPos != initialAnchorPos) {
1291 // If we have deleted a newline character, we are now in a new block
1292 const int currentBlockPos = getBlockPosition(
1293 focusObjectInputMethodQuery(Qt::ImAbsolutePosition | Qt::ImCursorPosition));
1294
1295 QInputMethodEvent event({}, {
1296 { QInputMethodEvent::Selection, initialCursorPos - leftLength - currentBlockPos,
1297 initialAnchorPos - initialCursorPos },
1298 { QInputMethodEvent::Cursor, 0, 0 }
1299 });
1300
1301 QGuiApplication::sendEvent(m_focusObject, &event);
1302 }
1303 }
1304
1305 return JNI_TRUE;
1306}
1307
1308// Android docs say the cursor must not move
1310{
1311 BatchEditLock batchEditLock(this);
1312
1313 if (!focusObjectStopComposing())
1314 return JNI_FALSE;
1315
1316 clear();
1317 return JNI_TRUE;
1318}
1319
1320/*
1321 Android docs say: This behaves like calling finishComposingText(), setSelection(start, end)
1322 and then commitText(text, newCursorPosition, textAttribute)
1323 https://developer.android.com/reference/android/view/inputmethod/InputConnection#replaceText(int,%20int,%20java.lang.CharSequence,%20int,%20android.view.inputmethod.TextAttribute)
1324*/
1325jboolean QAndroidInputContext::replaceText(jint start, jint end, const QString text, jint newCursorPosition)
1326{
1327 if (!finishComposingText())
1328 return JNI_FALSE;
1329 if (!setSelection(start, end))
1330 return JNI_FALSE;
1331
1332 return commitText(text, newCursorPosition);
1333}
1334
1336{
1337 m_fullScreenMode = enabled;
1338 BatchEditLock batchEditLock(this);
1339 if (!focusObjectStopComposing())
1340 return;
1341
1342 if (enabled)
1343 m_handleMode = Hidden;
1344
1346}
1347
1348// Called in calling thread's context
1350{
1351 return m_fullScreenMode;
1352}
1353
1354bool QAndroidInputContext::focusObjectIsComposing() const
1355{
1356 return m_composingCursor != -1;
1357}
1358
1359void QAndroidInputContext::focusObjectStartComposing()
1360{
1361 if (focusObjectIsComposing() || m_composingText.isEmpty())
1362 return;
1363
1364 // Composing strings containing newline characters are rare and may cause problems
1365 if (m_composingText.contains(u'\n'))
1366 return;
1367
1368 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1369 if (!query)
1370 return;
1371
1372 if (query->value(Qt::ImCursorPosition).toInt() != query->value(Qt::ImAnchorPosition).toInt())
1373 return;
1374
1375 const int absoluteCursorPos = getAbsoluteCursorPosition(query);
1376 if (absoluteCursorPos < m_composingTextStart
1377 || absoluteCursorPos > m_composingTextStart + m_composingText.length())
1378 return;
1379
1380 m_composingCursor = absoluteCursorPos;
1381
1382 QTextCharFormat underlined;
1383 underlined.setFontUnderline(true);
1384
1385 QInputMethodEvent event(m_composingText, {
1386 { QInputMethodEvent::Cursor, absoluteCursorPos - m_composingTextStart, 1 },
1387 { QInputMethodEvent::TextFormat, 0, int(m_composingText.length()), underlined }
1388 });
1389
1390 event.setCommitString({}, m_composingTextStart - absoluteCursorPos, m_composingText.length());
1391
1392 QGuiApplication::sendEvent(m_focusObject, &event);
1393}
1394
1395bool QAndroidInputContext::focusObjectStopComposing()
1396{
1397 if (!focusObjectIsComposing())
1398 return true; // not composing
1399
1400 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1401 if (query.isNull())
1402 return false;
1403
1404 const int blockPos = getBlockPosition(query);
1405 const int localCursorPos = m_composingCursor - blockPos;
1406
1407 m_composingCursor = -1;
1408
1409 {
1410 // commit the composing test
1411 QList<QInputMethodEvent::Attribute> attributes;
1412 QInputMethodEvent event(QString(), attributes);
1413 event.setCommitString(m_composingText);
1414 sendInputMethodEvent(&event);
1415 }
1416 {
1417 // Moving Qt's cursor to where the preedit cursor used to be
1418 QList<QInputMethodEvent::Attribute> attributes;
1419 attributes.append(
1420 QInputMethodEvent::Attribute(QInputMethodEvent::Selection, localCursorPos, 0));
1421 QInputMethodEvent event(QString(), attributes);
1422 sendInputMethodEvent(&event);
1423 }
1424
1425 return true;
1426}
1427
1429{
1430 jint res = 0;
1431 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1432 if (query.isNull())
1433 return res;
1434
1435 const uint qtInputMethodHints = query->value(Qt::ImHints).toUInt();
1436 const int localPos = query->value(Qt::ImCursorPosition).toInt();
1437
1438 bool atWordBoundary =
1439 localPos == 0
1440 && (!focusObjectIsComposing() || m_composingCursor == m_composingTextStart);
1441
1442 if (!atWordBoundary) {
1443 QString surroundingText = query->value(Qt::ImSurroundingText).toString();
1444 surroundingText.truncate(localPos);
1445 if (focusObjectIsComposing())
1446 surroundingText += QStringView{m_composingText}.left(m_composingCursor - m_composingTextStart);
1447 // Add a character to see if it is at the end of the sentence or not
1448 QTextBoundaryFinder finder(QTextBoundaryFinder::Sentence, surroundingText + u'A');
1449 finder.setPosition(surroundingText.length());
1450 if (finder.isAtBoundary())
1451 atWordBoundary = finder.isAtBoundary();
1452 }
1453 if (atWordBoundary && !(qtInputMethodHints & Qt::ImhLowercaseOnly) && !(qtInputMethodHints & Qt::ImhNoAutoUppercase))
1454 res |= CAP_MODE_SENTENCES;
1455
1456 if (qtInputMethodHints & Qt::ImhUppercaseOnly)
1457 res |= CAP_MODE_CHARACTERS;
1458
1459 return res;
1460}
1461
1462
1463
1464const QAndroidInputContext::ExtractedText &QAndroidInputContext::getExtractedText(jint /*hintMaxChars*/, jint /*hintMaxLines*/, jint /*flags*/)
1465{
1466 // Note to self: "if the GET_EXTRACTED_TEXT_MONITOR flag is set, you should be calling
1467 // updateExtractedText(View, int, ExtractedText) whenever you call
1468 // updateSelection(View, int, int, int, int)." QTBUG-37980
1469
1470 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(
1471 Qt::ImCursorPosition | Qt::ImAbsolutePosition | Qt::ImAnchorPosition);
1472 if (query.isNull())
1473 return m_extractedText;
1474
1475 const int cursorPos = getAbsoluteCursorPosition(query);
1476 const int blockPos = getBlockPosition(query);
1477
1478 // It is documented that we should try to return hintMaxChars
1479 // characters, but standard Android controls always return all text, and
1480 // there are input methods out there that (surprise) seem to depend on
1481 // what happens in reality rather than what's documented.
1482
1483 QVariant textBeforeCursor = QInputMethod::queryFocusObject(Qt::ImTextBeforeCursor, INT_MAX);
1484 QVariant textAfterCursor = QInputMethod::queryFocusObject(Qt::ImTextAfterCursor, INT_MAX);
1485 if (textBeforeCursor.isValid() && textAfterCursor.isValid()) {
1486 if (focusObjectIsComposing()) {
1487 m_extractedText.text =
1488 textBeforeCursor.toString() + m_composingText + textAfterCursor.toString();
1489 } else {
1490 m_extractedText.text = textBeforeCursor.toString() + textAfterCursor.toString();
1491 }
1492
1493 m_extractedText.startOffset = qMax(0, cursorPos - textBeforeCursor.toString().length());
1494 } else {
1495 m_extractedText.text = focusObjectInputMethodQuery(Qt::ImSurroundingText)
1496 ->value(Qt::ImSurroundingText).toString();
1497
1498 if (focusObjectIsComposing())
1499 m_extractedText.text.insert(cursorPos - blockPos, m_composingText);
1500
1501 m_extractedText.startOffset = blockPos;
1502 }
1503
1504 if (focusObjectIsComposing()) {
1505 m_extractedText.selectionStart = m_composingCursor - m_extractedText.startOffset;
1506 m_extractedText.selectionEnd = m_extractedText.selectionStart;
1507 } else {
1508 m_extractedText.selectionStart = cursorPos - m_extractedText.startOffset;
1509 m_extractedText.selectionEnd =
1510 blockPos + query->value(Qt::ImAnchorPosition).toInt() - m_extractedText.startOffset;
1511
1512 // Some keyboards misbehave when selectionStart > selectionEnd
1513 if (m_extractedText.selectionStart > m_extractedText.selectionEnd)
1514 std::swap(m_extractedText.selectionStart, m_extractedText.selectionEnd);
1515 }
1516
1517 return m_extractedText;
1518}
1519
1521{
1522 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1523 if (query.isNull())
1524 return QString();
1525
1526 return query->value(Qt::ImCurrentSelection).toString();
1527}
1528
1529QString QAndroidInputContext::getTextAfterCursor(jint length, jint /*flags*/)
1530{
1531 if (length <= 0)
1532 return QString();
1533
1534 QString text;
1535
1536 QVariant reportedTextAfter = QInputMethod::queryFocusObject(Qt::ImTextAfterCursor, length);
1537 if (reportedTextAfter.isValid()) {
1538 text = reportedTextAfter.toString();
1539 } else {
1540 // Compatibility code for old controls that do not implement the new API
1541 QSharedPointer<QInputMethodQueryEvent> query =
1542 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImSurroundingText);
1543 if (query) {
1544 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1545 text = query->value(Qt::ImSurroundingText).toString().mid(cursorPos);
1546 }
1547 }
1548
1549 if (focusObjectIsComposing()) {
1550 // Controls do not report preedit text, so we have to add it
1551 const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart;
1552 text = QStringView{m_composingText}.mid(cursorPosInsidePreedit) + text;
1553 } else {
1554 // We must not return selected text if there is any
1555 QSharedPointer<QInputMethodQueryEvent> query =
1556 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAnchorPosition);
1557 if (query) {
1558 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1559 const int anchorPos = query->value(Qt::ImAnchorPosition).toInt();
1560 if (anchorPos > cursorPos)
1561 text.remove(0, anchorPos - cursorPos);
1562 }
1563 }
1564
1565 text.truncate(length);
1566 return text;
1567}
1568
1569QString QAndroidInputContext::getTextBeforeCursor(jint length, jint /*flags*/)
1570{
1571 if (length <= 0)
1572 return QString();
1573
1574 QString text;
1575
1576 QVariant reportedTextBefore = QInputMethod::queryFocusObject(Qt::ImTextBeforeCursor, length);
1577 if (reportedTextBefore.isValid()) {
1578 text = reportedTextBefore.toString();
1579 } else {
1580 // Compatibility code for old controls that do not implement the new API
1581 QSharedPointer<QInputMethodQueryEvent> query =
1582 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImSurroundingText);
1583 if (query) {
1584 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1585 text = query->value(Qt::ImSurroundingText).toString().left(cursorPos);
1586 }
1587 }
1588
1589 if (focusObjectIsComposing()) {
1590 // Controls do not report preedit text, so we have to add it
1591 const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart;
1592 text += QStringView{m_composingText}.left(cursorPosInsidePreedit);
1593 } else {
1594 // We must not return selected text if there is any
1595 QSharedPointer<QInputMethodQueryEvent> query =
1596 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAnchorPosition);
1597 if (query) {
1598 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1599 const int anchorPos = query->value(Qt::ImAnchorPosition).toInt();
1600 if (anchorPos < cursorPos)
1601 text.chop(cursorPos - anchorPos);
1602 }
1603 }
1604
1605 if (text.length() > length)
1606 text = text.right(length);
1607 return text;
1608}
1609
1610/*
1611 Android docs say that this function should:
1612 - remove the current composing text, if there is any
1613 - otherwise remove currently selected text, if there is any
1614 - insert new text in place of old composing text or, if there was none, at current cursor position
1615 - mark the inserted text as composing
1616 - move cursor as specified by newCursorPosition: if > 0, it is relative to the end of inserted
1617 text - 1; if <= 0, it is relative to the start of inserted text
1618 */
1619
1620jboolean QAndroidInputContext::setComposingText(const QString &text, jint newCursorPosition)
1621{
1622 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1623 if (query.isNull())
1624 return JNI_FALSE;
1625
1626 BatchEditLock batchEditLock(this);
1627#if QT_CONFIG(accessibility)
1628 markTextEditForAccessibility();
1629#endif
1630
1631 const int absoluteCursorPos = getAbsoluteCursorPosition(query);
1632 int absoluteAnchorPos = getBlockPosition(query) + query->value(Qt::ImAnchorPosition).toInt();
1633
1634 auto setCursorPosition = [=]() {
1635 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1636 QInputMethodEvent event({}, { { QInputMethodEvent::Selection, cursorPos, 0 } });
1637 QGuiApplication::sendEvent(m_focusObject, &event);
1638 };
1639
1640 // If we have composing region and selection (and therefore focusObjectIsComposing() == false),
1641 // we must clear selection so that we won't delete it when we will be replacing composing text
1642 if (!m_composingText.isEmpty() && absoluteCursorPos != absoluteAnchorPos) {
1643 setCursorPosition();
1644 absoluteAnchorPos = absoluteCursorPos;
1645 }
1646
1647 // The value of Qt::ImCursorPosition is not updated at the start
1648 // when the first character is added, so we must update it (QTBUG-85090)
1649 if (absoluteCursorPos == 0 && text.length() == 1 && getTextAfterCursor(1,1).length() >= 0) {
1650 setCursorPosition();
1651 }
1652
1653 // If we had no composing region, pretend that we had a zero-length composing region at current
1654 // cursor position to simplify code. Also account for that we must delete selected text if there
1655 // (still) is any.
1656 const int effectiveAbsoluteCursorPos = qMin(absoluteCursorPos, absoluteAnchorPos);
1657 if (m_composingTextStart == -1)
1658 m_composingTextStart = effectiveAbsoluteCursorPos;
1659
1660 const int oldComposingTextLen = m_composingText.length();
1661 m_composingText = text;
1662
1663 const int newAbsoluteCursorPos =
1664 newCursorPosition <= 0
1665 ? m_composingTextStart + newCursorPosition
1666 : m_composingTextStart + m_composingText.length() + newCursorPosition - 1;
1667
1668 const bool focusObjectWasComposing = focusObjectIsComposing();
1669
1670 // Same checks as in focusObjectStartComposing()
1671 if (!m_composingText.isEmpty() && !m_composingText.contains(u'\n')
1672 && newAbsoluteCursorPos >= m_composingTextStart
1673 && newAbsoluteCursorPos <= m_composingTextStart + m_composingText.length())
1674 m_composingCursor = newAbsoluteCursorPos;
1675 else
1676 m_composingCursor = -1;
1677
1678 if (focusObjectIsComposing()) {
1679 QTextCharFormat underlined;
1680 underlined.setFontUnderline(true);
1681
1682 QInputMethodEvent event(m_composingText, {
1683 { QInputMethodEvent::TextFormat, 0, int(m_composingText.length()), underlined },
1684 { QInputMethodEvent::Cursor, m_composingCursor - m_composingTextStart, 1 }
1685 });
1686
1687 if (oldComposingTextLen > 0 && !focusObjectWasComposing) {
1688 event.setCommitString({}, m_composingTextStart - effectiveAbsoluteCursorPos,
1689 oldComposingTextLen);
1690 }
1691 if (m_composingText.isEmpty())
1692 clear();
1693
1694 QGuiApplication::sendEvent(m_focusObject, &event);
1695 } else {
1696 QInputMethodEvent event({}, {});
1697
1698 if (focusObjectWasComposing) {
1699 event.setCommitString(m_composingText);
1700 } else {
1701 event.setCommitString(m_composingText,
1702 m_composingTextStart - effectiveAbsoluteCursorPos,
1703 oldComposingTextLen);
1704 }
1705 if (m_composingText.isEmpty())
1706 clear();
1707
1708 QGuiApplication::sendEvent(m_focusObject, &event);
1709 }
1710
1711 if (!focusObjectIsComposing() && newCursorPosition != 1) {
1712 // Move cursor using a separate event because if we have inserted or deleted a newline
1713 // character, then we are now inside an another block
1714
1715 const int newBlockPos = getBlockPosition(
1716 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAbsolutePosition));
1717
1718 QInputMethodEvent event({}, {
1719 { QInputMethodEvent::Selection, newAbsoluteCursorPos - newBlockPos, 0 }
1720 });
1721
1722 QGuiApplication::sendEvent(m_focusObject, &event);
1723 }
1724
1725 keyDown();
1726
1727 return JNI_TRUE;
1728}
1729
1730// Android docs say:
1731// * start may be after end, same meaning as if swapped
1732// * this function should not trigger updateSelection, but Android's native EditText does trigger it
1733// * if start == end then we should stop composing
1735{
1736 BatchEditLock batchEditLock(this);
1737
1738 // Qt will not include the current preedit text in the query results, and interprets all
1739 // parameters relative to the text excluding the preedit. The simplest solution is therefore to
1740 // tell Qt that we commit the text before we set the new region. This may cause a little flicker, but is
1741 // much more robust than trying to keep the two different world views in sync
1742
1743 finishComposingText();
1744
1745 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1746 if (query.isNull())
1747 return JNI_FALSE;
1748
1749 if (start == end)
1750 return JNI_TRUE;
1751 if (start > end)
1752 qSwap(start, end);
1753
1754 QString text = query->value(Qt::ImSurroundingText).toString();
1755 int textOffset = getBlockPosition(query);
1756
1757 if (start < textOffset || end > textOffset + text.length()) {
1758 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1759
1760 if (end - textOffset > text.length()) {
1761 const QString after = query->value(Qt::ImTextAfterCursor).toString();
1762 const int additionalSuffixLen = after.length() - (text.length() - cursorPos);
1763
1764 if (additionalSuffixLen > 0)
1765 text += QStringView{after}.right(additionalSuffixLen);
1766 }
1767
1768 if (start < textOffset) {
1769 QString before = query->value(Qt::ImTextBeforeCursor).toString();
1770 before.chop(cursorPos);
1771
1772 if (!before.isEmpty()) {
1773 text = before + text;
1774 textOffset -= before.length();
1775 }
1776 }
1777
1778 if (start < textOffset || end - textOffset > text.length()) {
1779 qCDebug(lcQpaInputMethods) << "Warning: setComposingRegion: failed to retrieve text from composing region";
1780
1781 return JNI_TRUE;
1782 }
1783 }
1784
1785 m_composingText = text.mid(start - textOffset, end - start);
1786 m_composingTextStart = start;
1787
1788 return JNI_TRUE;
1789}
1790
1792{
1793 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1794 if (query.isNull())
1795 return JNI_FALSE;
1796
1797 BatchEditLock batchEditLock(this);
1798
1799 int blockPosition = getBlockPosition(query);
1800 int localCursorPos = start - blockPosition;
1801
1802 if (focusObjectIsComposing() && start == end && start >= m_composingTextStart
1803 && start <= m_composingTextStart + m_composingText.length()) {
1804 // not actually changing the selection; just moving the
1805 // preedit cursor
1806 int localOldPos = query->value(Qt::ImCursorPosition).toInt();
1807 int pos = localCursorPos - localOldPos;
1808 QList<QInputMethodEvent::Attribute> attributes;
1809 attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, pos, 1));
1810
1811 //but we have to tell Qt about the compose text all over again
1812
1813 // Show compose text underlined
1814 QTextCharFormat underlined;
1815 underlined.setFontUnderline(true);
1816 attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat,0, m_composingText.length(),
1817 QVariant(underlined)));
1818 m_composingCursor = start;
1819
1820 QInputMethodEvent event(m_composingText, attributes);
1821 QGuiApplication::sendEvent(m_focusObject, &event);
1822 } else {
1823 // actually changing the selection
1824 focusObjectStopComposing();
1825 QList<QInputMethodEvent::Attribute> attributes;
1826 attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection,
1827 localCursorPos,
1828 end - start));
1829 QInputMethodEvent event({}, attributes);
1830 QGuiApplication::sendEvent(m_focusObject, &event);
1831 }
1832 return JNI_TRUE;
1833}
1834
1836{
1837 BatchEditLock batchEditLock(this);
1838
1839 focusObjectStopComposing();
1840 m_handleMode = ShowCursor;
1841 sendShortcut(QKeySequence::SelectAll);
1842 return JNI_TRUE;
1843}
1844
1846{
1847 BatchEditLock batchEditLock(this);
1848
1849 // This is probably not what native EditText would do, but normally if there is selection, then
1850 // there will be no composing region
1851 finishComposingText();
1852
1853 m_handleMode = ShowCursor;
1854 sendShortcut(QKeySequence::Cut);
1855 return JNI_TRUE;
1856}
1857
1859{
1860 BatchEditLock batchEditLock(this);
1861
1862 focusObjectStopComposing();
1863 m_handleMode = ShowCursor;
1864 sendShortcut(QKeySequence::Copy);
1865 return JNI_TRUE;
1866}
1867
1869{
1870#warning TODO
1871 return JNI_FALSE;
1872}
1873
1875{
1876 BatchEditLock batchEditLock(this);
1877
1878 // TODO: This is not what native EditText does
1879 finishComposingText();
1880
1881 m_handleMode = ShowCursor;
1882 sendShortcut(QKeySequence::Paste);
1883 return JNI_TRUE;
1884}
1885
1886void QAndroidInputContext::sendShortcut(const QKeySequence &sequence)
1887{
1888 for (int i = 0; i < sequence.count(); ++i) {
1889 const QKeyCombination keys = sequence[i];
1890 Qt::Key key = Qt::Key(keys.toCombined() & ~Qt::KeyboardModifierMask);
1891 Qt::KeyboardModifiers mod = Qt::KeyboardModifiers(keys.toCombined() & Qt::KeyboardModifierMask);
1892
1893 QKeyEvent pressEvent(QEvent::KeyPress, key, mod);
1894 QKeyEvent releaseEvent(QEvent::KeyRelease, key, mod);
1895
1896 QGuiApplication::sendEvent(m_focusObject, &pressEvent);
1897 QGuiApplication::sendEvent(m_focusObject, &releaseEvent);
1898 }
1899}
1900
1901QSharedPointer<QInputMethodQueryEvent> QAndroidInputContext::focusObjectInputMethodQuery(Qt::InputMethodQueries queries) {
1902 if (!qGuiApp)
1903 return {};
1904
1905 QObject *focusObject = qGuiApp->focusObject();
1906 if (!focusObject)
1907 return {};
1908
1909 QInputMethodQueryEvent *ret = new QInputMethodQueryEvent(queries);
1910 QCoreApplication::sendEvent(focusObject, ret);
1911 return QSharedPointer<QInputMethodQueryEvent>(ret);
1912}
1913
1914void QAndroidInputContext::sendInputMethodEvent(QInputMethodEvent *event)
1915{
1916 if (!qGuiApp)
1917 return;
1918
1919 QObject *focusObject = qGuiApp->focusObject();
1920 if (!focusObject)
1921 return;
1922
1923 QCoreApplication::sendEvent(focusObject, event);
1924}
1925
1926QT_END_NAMESPACE
jboolean setSelection(jint start, jint end)
jint getCursorCapsMode(jint reqModes)
QString getSelectedText(jint flags)
bool isAnimating() const override
This function can be reimplemented to return true whenever input method is animating shown or hidden.
QString getTextAfterCursor(jint length, jint flags)
void reportFullscreenMode(jboolean enabled)
QRectF keyboardRect() const override
This function can be reimplemented to return virtual keyboard rectangle in currently active window co...
void reset() override
Method to be called when input method needs to be reset.
jboolean setComposingText(const QString &text, jint newCursorPosition)
void hideInputPanel() override
Request to hide input panel.
jboolean commitText(const QString &text, jint newCursorPosition)
jboolean setComposingRegion(jint start, jint end)
static QAndroidInputContext * androidInputContext()
void update(Qt::InputMethodQueries queries) override
Notification on editor updates.
QString getTextBeforeCursor(jint length, jint flags)
void sendShortcut(const QKeySequence &)
void invokeAction(QInputMethod::Action action, int cursorPosition) override
Called when the word currently being composed in the input item is tapped by the user.
jboolean deleteSurroundingText(jint leftLength, jint rightLength)
jboolean replaceText(jint start, jint end, const QString text, jint newCursorPosition)
void handleLocationChanged(int handleId, int x, int y)
void showInputPanel() override
Request to show input panel.
const ExtractedText & getExtractedText(jint hintMaxChars, jint hintMaxLines, jint flags)
bool isInputPanelVisible() const override
Returns input panel visibility status.
The QInputMethodEvent class provides parameters for input method events.
Definition qevent.h:629
\inmodule QtCore\reentrant
Definition qpoint.h:30
Combined button and popup list for selecting options.
void updateSelection(int selStart, int selEnd, int candidatesStart, int candidatesEnd)
void showSoftwareKeyboard(int left, int top, int width, int height, int inputHints, int enterKeyType)
bool isSoftwareKeyboardVisible()
QAndroidPlatformIntegration * androidPlatformIntegration()
static jfieldID m_startOffsetFieldID
static int getBlockPosition(const QSharedPointer< QInputMethodQueryEvent > &query)
static jboolean cut(JNIEnv *, jobject)
static jint getCursorCapsMode(JNIEnv *, jobject, jint reqModes)
static jfieldID m_partialEndOffsetFieldID
static jfieldID m_textFieldID
static char const *const QtExtractedTextClassName
static bool hasValidFocusObject()
static jboolean fullscreenMode(JNIEnv *, jobject)
static QRect screenInputItemRectangle()
static jboolean finishComposingText(JNIEnv *, jobject)
static jobject getExtractedText(JNIEnv *env, jobject, int hintMaxChars, int hintMaxLines, jint flags)
static jfieldID m_selectionStartFieldID
static JNINativeMethod methods[]
static QAndroidInputContext * m_androidInputContext
static jboolean copy(JNIEnv *, jobject)
static jstring getTextBeforeCursor(JNIEnv *env, jobject, jint length, jint flags)
static jfieldID m_selectionEndFieldID
static jboolean copyURL(JNIEnv *, jobject)
static char const *const QtNativeInputConnectionClassName
static int m_selectHandleWidth
static void runOnQtThread(const std::function< void()> &func)
static jboolean commitText(JNIEnv *env, jobject, jstring text, jint newCursorPosition)
static jboolean paste(JNIEnv *, jobject)
static jboolean replaceText(JNIEnv *env, jobject, jint start, jint end, jstring text, jint newCursorPosition)
static jboolean beginBatchEdit(JNIEnv *, jobject)
static jmethodID m_classConstructorMethodID
static jstring getSelectedText(JNIEnv *env, jobject, jint flags)
static jboolean setComposingRegion(JNIEnv *, jobject, jint start, jint end)
static jboolean deleteSurroundingText(JNIEnv *, jobject, jint leftLength, jint rightLength)
static jboolean setComposingText(JNIEnv *env, jobject, jstring text, jint newCursorPosition)
static jboolean updateCursorPosition(JNIEnv *, jobject)
static jstring getTextAfterCursor(JNIEnv *env, jobject, jint length, jint flags)
static int getAbsoluteCursorPosition(const QSharedPointer< QInputMethodQueryEvent > &query)
static void reportFullscreenMode(JNIEnv *, jobject, jboolean enabled)
static jclass m_extractedTextClass
static jboolean endBatchEdit(JNIEnv *, jobject)
static jfieldID m_partialStartOffsetFieldID
static jboolean setSelection(JNIEnv *, jobject, jint start, jint end)
static jboolean selectAll(JNIEnv *, jobject)
#define qGuiApp