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 len:" << str.size() << 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 << "len:" << str.size() << 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) << "textLen:" << extractedText.text.size() << "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 len:" << text.size();
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 << "len:" << text.size();
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 << "len:" << text.size();
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 len:" << str.size() << 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
394static QPlatformWindow *focusWindowHandle()
395{
396 QWindow *window = qGuiApp->focusWindow();
397 return window ? window->handle() : nullptr;
398}
399
401{
402 QPlatformWindow *window = focusWindowHandle();
403 if (!window)
404 return QRect();
405 const QRect windowRect = QPlatformInputContext::inputItemRectangle().toRect();
406 return QRect(window->mapToGlobal(windowRect.topLeft()), windowRect.size());
407}
408
411 , m_composingTextStart(-1)
412 , m_composingCursor(-1)
414 , m_batchEditNestingLevel(0)
415 , m_focusObject(0)
416 , m_fullScreenMode(false)
417{
418 QJniEnvironment env;
419 jclass clazz = env.findClass(QtNativeInputConnectionClassName);
420 if (Q_UNLIKELY(!clazz)) {
421 qCritical() << "Native registration unable to find class '"
423 << '\'';
424 return;
425 }
426
427 if (Q_UNLIKELY(env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0)) {
428 qCritical() << "RegisterNatives failed for '"
430 << '\'';
431 return;
432 }
433
434 clazz = env.findClass(QtExtractedTextClassName);
435 if (Q_UNLIKELY(!clazz)) {
436 qCritical() << "Native registration unable to find class '"
438 << '\'';
439 return;
440 }
441
442 m_extractedTextClass = static_cast<jclass>(env->NewGlobalRef(clazz));
443 m_classConstructorMethodID = env->GetMethodID(m_extractedTextClass, "<init>", "()V");
444 if (Q_UNLIKELY(!m_classConstructorMethodID)) {
445 qCritical("GetMethodID failed");
446 return;
447 }
448
449 m_partialEndOffsetFieldID = env->GetFieldID(m_extractedTextClass, "partialEndOffset", "I");
450 if (Q_UNLIKELY(!m_partialEndOffsetFieldID)) {
451 qCritical("Can't find field partialEndOffset");
452 return;
453 }
454
455 m_partialStartOffsetFieldID = env->GetFieldID(m_extractedTextClass, "partialStartOffset", "I");
456 if (Q_UNLIKELY(!m_partialStartOffsetFieldID)) {
457 qCritical("Can't find field partialStartOffset");
458 return;
459 }
460
461 m_selectionEndFieldID = env->GetFieldID(m_extractedTextClass, "selectionEnd", "I");
462 if (Q_UNLIKELY(!m_selectionEndFieldID)) {
463 qCritical("Can't find field selectionEnd");
464 return;
465 }
466
467 m_selectionStartFieldID = env->GetFieldID(m_extractedTextClass, "selectionStart", "I");
468 if (Q_UNLIKELY(!m_selectionStartFieldID)) {
469 qCritical("Can't find field selectionStart");
470 return;
471 }
472
473 m_startOffsetFieldID = env->GetFieldID(m_extractedTextClass, "startOffset", "I");
474 if (Q_UNLIKELY(!m_startOffsetFieldID)) {
475 qCritical("Can't find field startOffset");
476 return;
477 }
478
479 m_textFieldID = env->GetFieldID(m_extractedTextClass, "text", "Ljava/lang/String;");
480 if (Q_UNLIKELY(!m_textFieldID)) {
481 qCritical("Can't find field text");
482 return;
483 }
484 qRegisterMetaType<QInputMethodEvent *>("QInputMethodEvent*");
485 qRegisterMetaType<QInputMethodQueryEvent *>("QInputMethodQueryEvent*");
487
488 QObject::connect(QGuiApplication::inputMethod(), &QInputMethod::cursorRectangleChanged,
489 this, &QAndroidInputContext::updateSelectionHandles);
490 QObject::connect(QGuiApplication::inputMethod(), &QInputMethod::anchorRectangleChanged,
491 this, &QAndroidInputContext::updateSelectionHandles);
492 QObject::connect(QGuiApplication::inputMethod(), &QInputMethod::inputItemClipRectangleChanged, this, [this]{
493 auto im = qGuiApp->inputMethod();
494 if (!im->inputItemClipRectangle().contains(im->anchorRectangle()) ||
495 !im->inputItemClipRectangle().contains(im->cursorRectangle())) {
496 m_handleMode = Hidden;
497 updateSelectionHandles();
498 }
499 });
500 m_hideCursorHandleTimer.setInterval(4000);
501 m_hideCursorHandleTimer.setSingleShot(true);
502 m_hideCursorHandleTimer.setTimerType(Qt::VeryCoarseTimer);
503 connect(&m_hideCursorHandleTimer, &QTimer::timeout, this, [this]{
504 m_handleMode = Hidden;
506 });
507}
508
520
525
526// cursor position getter that also works with editors that have not been updated to the new API
527static inline int getAbsoluteCursorPosition(const QSharedPointer<QInputMethodQueryEvent> &query)
528{
529 QVariant absolutePos = query->value(Qt::ImAbsolutePosition);
530 return absolutePos.isValid() ? absolutePos.toInt() : query->value(Qt::ImCursorPosition).toInt();
531}
532
533// position of the start of the current block
534static inline int getBlockPosition(const QSharedPointer<QInputMethodQueryEvent> &query)
535{
536 QVariant absolutePos = query->value(Qt::ImAbsolutePosition);
537 return absolutePos.isValid() ? absolutePos.toInt() - query->value(Qt::ImCursorPosition).toInt() : 0;
538}
539
541{
542 focusObjectStopComposing();
543 clear();
544 m_batchEditNestingLevel = 0;
545 m_handleMode = Hidden;
546 if (qGuiApp->focusObject()) {
547 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(Qt::ImEnabled);
548 if (!query.isNull() && query->value(Qt::ImEnabled).toBool()) {
549 // reset() runs on the focus change that an accessibility
550 // setFocusAction triggers; resetSoftwareKeyboard()'s restartInput()
551 // re-prompts the IME on some keyboards, which would re-open the
552 // panel we are suppressing in showInputPanel(). Skip it for the
553 // accessibility-focus path so the two stay consistent.
554 if (!m_accessibilityFocusInProgress)
556 return;
557 }
558 }
560}
561
563{
564 focusObjectStopComposing();
565}
566
568{
569 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
570 if (!query.isNull() && m_batchEditNestingLevel == 0) {
571 const int cursorPos = getAbsoluteCursorPosition(query);
572 const int composeLength = m_composingText.length();
573
574 //Q_ASSERT(m_composingText.isEmpty() == (m_composingTextStart == -1));
575 if (m_composingText.isEmpty() != (m_composingTextStart == -1))
576 qWarning() << "Input method out of sync" << m_composingText << m_composingTextStart;
577
578 int realSelectionStart = cursorPos;
579 int realSelectionEnd = cursorPos;
580
581 int cpos = query->value(Qt::ImCursorPosition).toInt();
582 int anchor = query->value(Qt::ImAnchorPosition).toInt();
583 if (cpos != anchor) {
584 if (!m_composingText.isEmpty()) {
585 qWarning("Selecting text while preediting may give unpredictable results.");
586 focusObjectStopComposing();
587 }
588 int blockPos = getBlockPosition(query);
589 realSelectionStart = blockPos + cpos;
590 realSelectionEnd = blockPos + anchor;
591 }
592 // Qt's idea of the cursor position is the start of the preedit area, so we maintain our own preedit cursor pos
593 if (focusObjectIsComposing())
594 realSelectionStart = realSelectionEnd = m_composingCursor;
595
596 // Some keyboards misbahave when selStart > selEnd
597 if (realSelectionStart > realSelectionEnd)
598 std::swap(realSelectionStart, realSelectionEnd);
599
600 QtAndroidInput::updateSelection(realSelectionStart, realSelectionEnd,
601 m_composingTextStart, m_composingTextStart + composeLength); // pre-edit text
602 }
603}
604
605bool QAndroidInputContext::isImhNoEditMenuSet()
606{
607 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
608 if (query.isNull())
609 return false;
610 return query->value(Qt::ImHints).toUInt() & Qt::ImhNoEditMenu;
611}
612
613bool QAndroidInputContext::isImhNoTextHandlesSet()
614{
615 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
616 if (query.isNull())
617 return false;
618 return query->value(Qt::ImHints).toUInt() & Qt::ImhNoTextHandles;
619}
620
622{
623 if (m_fullScreenMode) {
624 QtAndroidInput::updateHandles(Hidden);
625 return;
626 }
627 static bool noHandles = qEnvironmentVariableIntValue("QT_QPA_NO_TEXT_HANDLES");
628 if (noHandles || !m_focusObject)
629 return;
630
631 QPlatformWindow *qPlatformWindow = focusWindowHandle();
632 if (!qPlatformWindow)
633 return;
634
635 if (isImhNoTextHandlesSet()) {
636 QtAndroidInput::updateHandles(Hidden);
637 return;
638 }
639
640 auto im = qGuiApp->inputMethod();
641
642 QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition | Qt::ImEnabled
643 | Qt::ImCurrentSelection | Qt::ImHints | Qt::ImSurroundingText
644 | Qt::ImReadOnly);
645 QCoreApplication::sendEvent(m_focusObject, &query);
646
647 int cpos = query.value(Qt::ImCursorPosition).toInt();
648 int anchor = query.value(Qt::ImAnchorPosition).toInt();
649 const QVariant readOnlyVariant = query.value(Qt::ImReadOnly);
650 bool readOnly = readOnlyVariant.toBool();
651
652 if (!readOnly && ((m_handleMode & 0xff) == Hidden)) {
653 QtAndroidInput::updateHandles(Hidden);
654 return;
655 }
656
657 if ( cpos == anchor && (!readOnlyVariant.isValid() || readOnly)) {
658 QtAndroidInput::updateHandles(Hidden);
659 return;
660 }
661
662 if (cpos == anchor || im->anchorRectangle().isNull()) {
663 auto curRect = cursorRectangle();
664 QPoint cursorPointGlobal = QPoint(curRect.x() + (curRect.width() / 2), curRect.y() + curRect.height());
665 QPoint cursorPoint(curRect.center().x(), curRect.bottom());
666 int x = curRect.x();
667 int y = curRect.y();
668
669 // Use x and y for the editMenuPoint from the cursorPointGlobal when the cursor is in the Dialog
670 if (cursorPointGlobal != cursorPoint) {
671 x = cursorPointGlobal.x();
672 y = cursorPointGlobal.y();
673 }
674
675 QPoint editMenuPoint(x, y);
676 m_handleMode &= ShowEditPopup;
677 m_handleMode |= ShowCursor;
678 uint32_t buttons = 0;
679 const bool withEditMenu = !isImhNoEditMenuSet();
680 if (withEditMenu) {
681 buttons = readOnly ? 0 : EditContext::PasteButton;
682 if (!query.value(Qt::ImSurroundingText).toString().isEmpty())
684 }
685 QtAndroidInput::updateHandles(m_handleMode, editMenuPoint, buttons, cursorPointGlobal);
686 m_hideCursorHandleTimer.start();
687
688 return;
689 }
690
691 m_handleMode = ShowSelection | ShowEditPopup ;
692 auto leftRect = cursorRectangle();
693 auto rightRect = anchorRectangle();
694 if (cpos > anchor)
695 std::swap(leftRect, rightRect);
696 //Move the left or right select handle to the center from the screen edge
697 //the select handle is close to or over the screen edge. Otherwise, the
698 //select handle might go out of the screen and it would be impossible to drag.
699 QPoint leftPoint(qPlatformWindow->mapToGlobal(leftRect.bottomLeft().toPoint()));
700 QPoint rightPoint(qPlatformWindow->mapToGlobal(rightRect.bottomRight().toPoint()));
701
703 if (platformIntegration) {
704 if (m_selectHandleWidth == 0)
706
707 int rightSideOfScreen = platformIntegration->screen()->availableGeometry().right();
708 if (leftPoint.x() < m_selectHandleWidth)
709 leftPoint.setX(m_selectHandleWidth);
710 leftPoint = qPlatformWindow->mapFromGlobal(leftPoint);
711
712 if (rightPoint.x() > rightSideOfScreen - m_selectHandleWidth)
713 rightPoint.setX(rightSideOfScreen - m_selectHandleWidth);
714 rightPoint = qPlatformWindow->mapFromGlobal(rightPoint);
715
716 QPoint editPoint(leftRect.united(rightRect).topLeft().toPoint());
717 uint32_t buttons = 0;
718 const bool withEditMenu = !isImhNoEditMenuSet();
719 if (withEditMenu) {
722 }
723
724 QtAndroidInput::updateHandles(m_handleMode, editPoint, buttons, leftPoint, rightPoint,
725 query.value(Qt::ImCurrentSelection).toString().isRightToLeft());
726 m_hideCursorHandleTimer.stop();
727 }
728}
729
730/*
731 Called from Java when a cursor/selection handle was dragged to a new position
732
733 handleId of 1 means the cursor handle, 2 means the left handle, 3 means the right handle
734 */
735void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y)
736{
737 if (m_batchEditNestingLevel != 0) {
738 qWarning() << "QAndroidInputContext::handleLocationChanged returned";
739 return;
740 }
741 QPoint point(x, y);
742
743 // The handle is down of the cursor, but we want the position in the middle.
744 QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition
745 | Qt::ImAbsolutePosition | Qt::ImCurrentSelection);
746 QCoreApplication::sendEvent(m_focusObject, &query);
747 int cpos = query.value(Qt::ImCursorPosition).toInt();
748 int anchor = query.value(Qt::ImAnchorPosition).toInt();
749 auto leftRect = cursorRectangle();
750 auto rightRect = anchorRectangle();
751 if (cpos > anchor)
752 std::swap(leftRect, rightRect);
753
754 // Do not allow dragging left handle below right handle, or right handle above left handle
755 if (handleId == 2 && point.y() > rightRect.center().y()) {
756 point.setY(rightRect.center().y());
757 } else if (handleId == 3 && point.y() < leftRect.center().y()) {
758 point.setY(leftRect.center().y());
759 }
760
761 bool ok;
762 auto object = m_focusObject->parent();
763 int dialogMoveX = 0;
764 while (object) {
765 if (QString::compare(object->metaObject()->className(),
766 "QDialog", Qt::CaseInsensitive) == 0) {
767 dialogMoveX += object->property("x").toInt();
768 }
769 object = object->parent();
770 };
771
772 auto position =
773 QPointF(QHighDpi::fromNativePixels(point, QGuiApplication::focusWindow()));
774 const QPointF fixedPosition = QPointF(position.x() - dialogMoveX, position.y());
775 const QInputMethod *im = QGuiApplication::inputMethod();
776 const QTransform mapToLocal = im->inputItemTransform().inverted();
777 const int handlePos = im->queryFocusObject(Qt::ImCursorPosition, mapToLocal.map(fixedPosition)).toInt(&ok);
778
779 if (!ok)
780 return;
781
782 int newCpos = cpos;
783 int newAnchor = anchor;
784 if (newAnchor > newCpos)
785 std::swap(newAnchor, newCpos);
786
787 if (handleId == 1) {
788 newCpos = handlePos;
789 newAnchor = handlePos;
790 } else if (handleId == 2) {
791 newAnchor = handlePos;
792 } else if (handleId == 3) {
793 newCpos = handlePos;
794 }
795
796 /*
797 Do not allow clearing selection by dragging selection handles and do not allow swapping
798 selection handles for consistency with Android's native text editing controls. Ensure that at
799 least one symbol remains selected.
800 */
801 if ((handleId == 2 || handleId == 3) && newCpos <= newAnchor) {
802 QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme,
803 query.value(Qt::ImCurrentSelection).toString());
804
805 const int oldSelectionStartPos = qMin(cpos, anchor);
806
807 if (handleId == 2) {
808 finder.toEnd();
809 finder.toPreviousBoundary();
810 newAnchor = finder.position() + oldSelectionStartPos;
811 } else {
812 finder.toStart();
813 finder.toNextBoundary();
814 newCpos = finder.position() + oldSelectionStartPos;
815 }
816 }
817
818 // Check if handle has been dragged far enough
819 if (!focusObjectIsComposing() && newCpos == cpos && newAnchor == anchor)
820 return;
821
822 /*
823 If the editor is currently in composing state, we have to compare newCpos with
824 m_composingCursor instead of cpos. And since there is nothing to compare with newAnchor, we
825 perform the check only when user drags the cursor handle.
826 */
827 if (focusObjectIsComposing() && handleId == 1) {
828 int absoluteCpos = query.value(Qt::ImAbsolutePosition).toInt(&ok);
829 if (!ok)
830 absoluteCpos = cpos;
831 const int blockPos = absoluteCpos - cpos;
832
833 if (blockPos + newCpos == m_composingCursor)
834 return;
835 }
836
837 BatchEditLock batchEditLock(this);
838
839 focusObjectStopComposing();
840
841 QList<QInputMethodEvent::Attribute> attributes;
842 attributes.append({ QInputMethodEvent::Selection, newAnchor, newCpos - newAnchor });
843 if (newCpos != newAnchor)
844 attributes.append({ QInputMethodEvent::Cursor, 0, 0 });
845
846 QInputMethodEvent event(QString(), attributes);
847 QGuiApplication::sendEvent(m_focusObject, &event);
848}
849
851{
852 QPlatformWindow *window = focusWindowHandle();
853 if (window && m_focusObject && screenInputItemRectangle().contains(x, y)) {
854 // If the user touch the input rectangle, we can show the cursor handle
855 m_handleMode = ShowCursor;
856 // The VK will appear in a moment, stop the timer
857 m_hideCursorHandleTimer.stop();
858
859 if (focusObjectIsComposing()) {
860 const int curBlockPos = getBlockPosition(
861 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAbsolutePosition));
862 const int touchPosition = curBlockPos
863 + queryFocusObject(Qt::ImCursorPosition, QPointF(x, y)).toInt();
864 if (touchPosition != m_composingCursor)
865 focusObjectStopComposing();
866 }
867
868 // Check if cursor is visible in focused window before updating handles
869 const QRectF curRect = cursorRectangle();
870 const QPoint cursorGlobalPoint = window->mapToGlobal(QPoint(curRect.x(), curRect.y()));
871 const QRect windowRect = QPlatformInputContext::inputItemClipRectangle().toRect();
872 const QRect windowGlobalRect = QRect(window->mapToGlobal(windowRect.topLeft()), windowRect.size());
873
874 if (windowGlobalRect.contains(cursorGlobalPoint.x(), cursorGlobalPoint.y()))
876 }
877}
878
880{
881 static bool noHandles = qEnvironmentVariableIntValue("QT_QPA_NO_TEXT_HANDLES");
882 if (noHandles)
883 return;
884
885 if (m_focusObject && screenInputItemRectangle().contains(x, y)) {
886 BatchEditLock batchEditLock(this);
887
888 focusObjectStopComposing();
889 const QPointF touchPoint(x, y);
890 setSelectionOnFocusObject(touchPoint, touchPoint);
891
892 QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition | Qt::ImTextBeforeCursor | Qt::ImTextAfterCursor);
893 QCoreApplication::sendEvent(m_focusObject, &query);
894 int cursor = query.value(Qt::ImCursorPosition).toInt();
895 int anchor = cursor;
896 QString before = query.value(Qt::ImTextBeforeCursor).toString();
897 QString after = query.value(Qt::ImTextAfterCursor).toString();
898 for (const auto &ch : after) {
899 if (!ch.isLetterOrNumber())
900 break;
901 ++anchor;
902 }
903
904 for (auto itch = before.rbegin(); itch != before.rend(); ++itch) {
905 if (!itch->isLetterOrNumber())
906 break;
907 --cursor;
908 }
909 if (cursor == anchor || cursor < 0 || cursor - anchor > 500) {
910 m_handleMode = ShowCursor | ShowEditPopup;
912 return;
913 }
914 QList<QInputMethodEvent::Attribute> imAttributes;
915 imAttributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, 0, 0, QVariant()));
916 imAttributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection, anchor, cursor - anchor, QVariant()));
917 QInputMethodEvent event(QString(), imAttributes);
918 QGuiApplication::sendEvent(m_focusObject, &event);
919
920 m_handleMode = ShowSelection | ShowEditPopup;
922 }
923}
924
926{
927 if (m_handleMode) {
928 // When the user enter text on the keyboard, we hide the cursor handle
929 m_handleMode = Hidden;
931 }
932}
933
935{
936 if (m_handleMode & ShowSelection) {
937 m_handleMode = Hidden;
939 } else {
940 m_hideCursorHandleTimer.start();
941 }
942}
943
944void QAndroidInputContext::update(Qt::InputMethodQueries queries)
945{
946 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(queries);
947 if (query.isNull())
948 return;
949#if QT_CONFIG(accessibility)
950 // Editors report every applied text change here, including edits that bypass
951 // the IME mutators (key-event backspace, hardware keys, programmatic changes).
952 // Announce them; a change arriving mid-batch is deferred to endBatchEdit() so
953 // each committed edit is announced exactly once.
954 if (queries & Qt::ImSurroundingText) {
955 if (m_batchEditNestingLevel == 0)
956 notifyTextChangedForAccessibility();
957 else if (QAccessible::isActive())
958 m_a11yTextEditPending = true;
959 }
960#endif
961#warning TODO extract the needed data from query
962}
963
964void QAndroidInputContext::invokeAction(QInputMethod::Action action, int cursorPosition)
965{
966#warning TODO Handle at least QInputMethod::ContextMenu action
967 Q_UNUSED(action);
968 Q_UNUSED(cursorPosition);
969 //### click should be passed to the IM, but in the meantime it's better to ignore it than to do something wrong
970 // if (action == QInputMethod::Click)
971 // commit();
972}
973
975{
976 return QtAndroidInput::softwareKeyboardRect();
977}
978
980{
981 return false;
982}
983
985{
986 if (QGuiApplication::applicationState() != Qt::ApplicationActive) {
987 connect(qGuiApp, SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(showInputPanelLater(Qt::ApplicationState)));
988 return;
989 }
990
991 // Don't open the keyboard for the input focus that an accessibility
992 // setFocusAction grants while a screen reader navigates fields; a
993 // deliberate activation (double-tap) still opens it normally.
994 if (m_accessibilityFocusInProgress)
995 return;
996
997 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
998 if (query.isNull())
999 return;
1000
1002 return; // not a real window, probably VR/XR
1003
1004 disconnect(m_updateCursorPosConnection);
1005 m_updateCursorPosConnection = {};
1006
1007 if (qGuiApp->focusObject()->metaObject()->indexOfSignal("cursorPositionChanged(int,int)") >= 0) // QLineEdit breaks the pattern
1008 m_updateCursorPosConnection = connect(qGuiApp->focusObject(), SIGNAL(cursorPositionChanged(int,int)), this, SLOT(updateCursorPosition()));
1009 else if (qGuiApp->focusObject()->metaObject()->indexOfSignal("cursorPositionChanged()") >= 0)
1010 m_updateCursorPosConnection = connect(qGuiApp->focusObject(), SIGNAL(cursorPositionChanged()), this, SLOT(updateCursorPosition()));
1011
1012 QRect rect = QPlatformInputContext::inputItemRectangle().toRect();
1013 QtAndroidInput::showSoftwareKeyboard(rect.left(), rect.top(), rect.width(), rect.height(),
1014 query->value(Qt::ImHints).toUInt(),
1015 query->value(Qt::ImEnterKeyType).toUInt());
1016}
1017
1018void QAndroidInputContext::showInputPanelLater(Qt::ApplicationState state)
1019{
1020 if (state != Qt::ApplicationActive)
1021 return;
1022 disconnect(qGuiApp, SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(showInputPanelLater(Qt::ApplicationState)));
1024}
1025
1026void QAndroidInputContext::safeCall(const std::function<void()> &func, Qt::ConnectionType conType)
1027{
1028 if (qGuiApp->thread() == QThread::currentThread())
1029 func();
1030 else
1031 QMetaObject::invokeMethod(this, "safeCall", conType, Q_ARG(std::function<void()>, func));
1032}
1033
1038
1043
1045{
1046 return m_composingText.length();
1047}
1048
1050{
1051 m_composingText.clear();
1052 m_composingTextStart = -1;
1053 m_composingCursor = -1;
1054 m_extractedText.clear();
1055}
1056
1057
1059{
1060 return m_focusObject;
1061}
1062
1063void QAndroidInputContext::setFocusObject(QObject *object)
1064{
1065 if (object != m_focusObject) {
1066 focusObjectStopComposing();
1067 m_focusObject = object;
1068 reset();
1069#if QT_CONFIG(accessibility)
1070 // (Re)capture the text baseline for the new focus object (when
1071 // accessibility is active; otherwise just invalidate and let the lazy
1072 // fallbacks capture) so both the IME (endBatchEdit) and non-IME
1073 // (update()) announcement paths diff against the field's pre-edit
1074 // content. Capturing fires nothing — the announcement paths are gated
1075 // separately. An unanswered query leaves the baseline invalid so the
1076 // baseline-adopt guard handles it instead of diffing against garbage.
1077 m_a11yTextEditPending = false;
1078 m_a11yLastText.clear();
1079 m_a11yBaselineValid = false;
1080 if (m_focusObject && QAccessible::isActive()) {
1081 QInputMethodQueryEvent query(Qt::ImSurroundingText);
1082 QCoreApplication::sendEvent(m_focusObject, &query);
1083 const QVariant surroundingText = query.value(Qt::ImSurroundingText);
1084 if (surroundingText.isValid()) {
1085 m_a11yLastText = surroundingText.toString();
1086 m_a11yBaselineValid = true;
1087 }
1088 }
1089#endif
1090 }
1092}
1093
1094#if QT_CONFIG(accessibility)
1095void QAndroidInputContext::markTextEditForAccessibility()
1096{
1097 if (!QAccessible::isActive() || !m_focusObject)
1098 return;
1099 m_a11yTextEditPending = true;
1100 // Fallback baseline capture — setFocusObject() captures eagerly, but only
1101 // when accessibility was active at focus time (and only if the query was
1102 // answered). If it wasn't, capture here before this edit is applied so the
1103 // diff in notifyTextChangedForAccessibility() is accurate.
1104 if (!m_a11yBaselineValid) {
1105 QInputMethodQueryEvent query(Qt::ImSurroundingText);
1106 QCoreApplication::sendEvent(m_focusObject, &query);
1107 m_a11yLastText = query.value(Qt::ImSurroundingText).toString();
1108 m_a11yBaselineValid = true;
1109 }
1110}
1111
1112void QAndroidInputContext::notifyTextChangedForAccessibility()
1113{
1114 if (!QAccessible::isActive() || !m_focusObject)
1115 return;
1116
1117 QInputMethodQueryEvent query(Qt::ImSurroundingText);
1118 QCoreApplication::sendEvent(m_focusObject, &query);
1119 const QString after = query.value(Qt::ImSurroundingText).toString();
1120 if (!m_a11yBaselineValid) {
1121 // No pre-edit baseline to diff against — adopt the current text and stay
1122 // silent rather than announcing a bogus whole-field change.
1123 m_a11yLastText = after;
1124 m_a11yBaselineValid = true;
1125 return;
1126 }
1127 const QString before = m_a11yLastText;
1128 if (after == before)
1129 return;
1130 m_a11yLastText = after;
1131
1132 // Character-level diff: the common prefix and suffix bound the changed span,
1133 // giving TalkBack {fromIndex, addedCount, removedCount} to echo just the
1134 // inserted/deleted characters.
1135 const int minLen = qMin(before.size(), after.size());
1136 int prefix = 0;
1137 while (prefix < minLen && before.at(prefix) == after.at(prefix))
1138 ++prefix;
1139 // Don't split a surrogate pair: fromIndex must be a code-point boundary, or
1140 // Android/TalkBack will mis-handle the (non-BMP, e.g. emoji) change.
1141 if (prefix > 0 && before.at(prefix - 1).isHighSurrogate())
1142 --prefix;
1143 int suffix = 0;
1144 while (suffix < minLen - prefix
1145 && before.at(before.size() - 1 - suffix) == after.at(after.size() - 1 - suffix))
1146 ++suffix;
1147 if (suffix > 0 && before.at(before.size() - suffix).isLowSurrogate())
1148 --suffix;
1149 const int removedCount = int(before.size()) - prefix - suffix;
1150 const int addedCount = int(after.size()) - prefix - suffix;
1151
1152 // Pass the input-focus object's accessible id; the Java side sources the
1153 // event from the accessibility-focused virtual view (which may differ).
1154 uint focusUid = 0;
1155 if (QAccessibleInterface *iface = QAccessible::queryAccessibleInterface(m_focusObject))
1156 focusUid = QAccessible::uniqueId(iface);
1157
1158 QtAndroid::notifyTextChanged(focusUid, after, before, prefix, addedCount, removedCount);
1159}
1160#endif
1161
1163{
1164 ++m_batchEditNestingLevel;
1165 return JNI_TRUE;
1166}
1167
1169{
1170 if (--m_batchEditNestingLevel == 0) { //ending batch edit mode
1171 focusObjectStartComposing();
1173#if QT_CONFIG(accessibility)
1174 // Announce the change to TalkBack once the edit is applied — but only if
1175 // this batch actually changed text (flag set by the IME mutators, or by
1176 // update() observing a mid-batch ImSurroundingText change). Focus-time
1177 // batches change nothing, set no flag, and so can't fire a
1178 // (label-clobbering) text-change event. Text changes outside a batch
1179 // (key events, programmatic edits) are announced from update() directly,
1180 // which also keeps the baseline synced.
1181 if (m_a11yTextEditPending) {
1182 m_a11yTextEditPending = false;
1183 notifyTextChangedForAccessibility();
1184 }
1185#endif
1186 }
1187 return JNI_TRUE;
1188}
1189
1190/*
1191 Android docs say: This behaves like calling setComposingText(text, newCursorPosition) then
1192 finishComposingText().
1193*/
1194jboolean QAndroidInputContext::commitText(const QString &text, jint newCursorPosition)
1195{
1196 BatchEditLock batchEditLock(this);
1197 return setComposingText(text, newCursorPosition) && finishComposingText();
1198}
1199
1200jboolean QAndroidInputContext::deleteSurroundingText(jint leftLength, jint rightLength)
1201{
1202 BatchEditLock batchEditLock(this);
1203#if QT_CONFIG(accessibility)
1204 markTextEditForAccessibility();
1205#endif
1206
1207 focusObjectStopComposing();
1208
1209 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1210 if (query.isNull())
1211 return JNI_TRUE;
1212
1213 if (leftLength < 0) {
1214 rightLength += -leftLength;
1215 leftLength = 0;
1216 }
1217
1218 const int initialBlockPos = getBlockPosition(query);
1219 const int initialCursorPos = getAbsoluteCursorPosition(query);
1220 const int initialAnchorPos = initialBlockPos + query->value(Qt::ImAnchorPosition).toInt();
1221
1222 /*
1223 According to documentation, we should delete leftLength characters before current selection
1224 and rightLength characters after current selection (without affecting selection). But that is
1225 absolutely not what Android's native EditText does. It deletes leftLength characters before
1226 min(selection start, composing region start) and rightLength characters after max(selection
1227 end, composing region end). There are no known keyboards that depend on this behavior, but
1228 it is better to be consistent with EditText behavior, because there definitely should be no
1229 keyboards that depend on documented behavior.
1230 */
1231 const int leftEnd =
1232 m_composingText.isEmpty()
1233 ? qMin(initialCursorPos, initialAnchorPos)
1234 : qMin(qMin(initialCursorPos, initialAnchorPos), m_composingTextStart);
1235
1236 const int rightBegin =
1237 m_composingText.isEmpty()
1238 ? qMax(initialCursorPos, initialAnchorPos)
1239 : qMax(qMax(initialCursorPos, initialAnchorPos),
1240 m_composingTextStart + m_composingText.length());
1241
1242 int textBeforeCursorLen;
1243 int textAfterCursorLen;
1244
1245 QVariant textBeforeCursor = query->value(Qt::ImTextBeforeCursor);
1246 QVariant textAfterCursor = query->value(Qt::ImTextAfterCursor);
1247 if (textBeforeCursor.isValid() && textAfterCursor.isValid()) {
1248 textBeforeCursorLen = textBeforeCursor.toString().length();
1249 textAfterCursorLen = textAfterCursor.toString().length();
1250 } else {
1251 textBeforeCursorLen = initialCursorPos - initialBlockPos;
1252 textAfterCursorLen =
1253 query->value(Qt::ImSurroundingText).toString().length() - textBeforeCursorLen;
1254 }
1255
1256 leftLength = qMin(qMax(0, textBeforeCursorLen - (initialCursorPos - leftEnd)), leftLength);
1257 rightLength = qMin(qMax(0, textAfterCursorLen - (rightBegin - initialCursorPos)), rightLength);
1258
1259 if (leftLength == 0 && rightLength == 0)
1260 return JNI_TRUE;
1261
1262 if (leftEnd == rightBegin) {
1263 // We have no selection and no composing region; we can do everything using one event
1264 QInputMethodEvent event;
1265 event.setCommitString({}, -leftLength, leftLength + rightLength);
1266 QGuiApplication::sendEvent(m_focusObject, &event);
1267 } else {
1268 if (initialCursorPos != initialAnchorPos) {
1269 QInputMethodEvent event({}, {
1270 { QInputMethodEvent::Selection, initialCursorPos - initialBlockPos, 0 }
1271 });
1272
1273 QGuiApplication::sendEvent(m_focusObject, &event);
1274 }
1275
1276 int currentCursorPos = initialCursorPos;
1277
1278 if (rightLength > 0) {
1279 QInputMethodEvent event;
1280 event.setCommitString({}, rightBegin - currentCursorPos, rightLength);
1281 QGuiApplication::sendEvent(m_focusObject, &event);
1282
1283 currentCursorPos = rightBegin;
1284 }
1285
1286 if (leftLength > 0) {
1287 const int leftBegin = leftEnd - leftLength;
1288
1289 QInputMethodEvent event;
1290 event.setCommitString({}, leftBegin - currentCursorPos, leftLength);
1291 QGuiApplication::sendEvent(m_focusObject, &event);
1292
1293 currentCursorPos = leftBegin;
1294
1295 if (!m_composingText.isEmpty())
1296 m_composingTextStart -= leftLength;
1297 }
1298
1299 // Restore cursor position or selection
1300 if (currentCursorPos != initialCursorPos - leftLength
1301 || initialCursorPos != initialAnchorPos) {
1302 // If we have deleted a newline character, we are now in a new block
1303 const int currentBlockPos = getBlockPosition(
1304 focusObjectInputMethodQuery(Qt::ImAbsolutePosition | Qt::ImCursorPosition));
1305
1306 QInputMethodEvent event({}, {
1307 { QInputMethodEvent::Selection, initialCursorPos - leftLength - currentBlockPos,
1308 initialAnchorPos - initialCursorPos },
1309 { QInputMethodEvent::Cursor, 0, 0 }
1310 });
1311
1312 QGuiApplication::sendEvent(m_focusObject, &event);
1313 }
1314 }
1315
1316 return JNI_TRUE;
1317}
1318
1319// Android docs say the cursor must not move
1321{
1322 BatchEditLock batchEditLock(this);
1323
1324 if (!focusObjectStopComposing())
1325 return JNI_FALSE;
1326
1327 clear();
1328 return JNI_TRUE;
1329}
1330
1331/*
1332 Android docs say: This behaves like calling finishComposingText(), setSelection(start, end)
1333 and then commitText(text, newCursorPosition, textAttribute)
1334 https://developer.android.com/reference/android/view/inputmethod/InputConnection#replaceText(int,%20int,%20java.lang.CharSequence,%20int,%20android.view.inputmethod.TextAttribute)
1335*/
1336jboolean QAndroidInputContext::replaceText(jint start, jint end, const QString text, jint newCursorPosition)
1337{
1338 if (!finishComposingText())
1339 return JNI_FALSE;
1340 if (!setSelection(start, end))
1341 return JNI_FALSE;
1342
1343 return commitText(text, newCursorPosition);
1344}
1345
1347{
1348 m_fullScreenMode = enabled;
1349 BatchEditLock batchEditLock(this);
1350 if (!focusObjectStopComposing())
1351 return;
1352
1353 if (enabled)
1354 m_handleMode = Hidden;
1355
1357}
1358
1359// Called in calling thread's context
1361{
1362 return m_fullScreenMode;
1363}
1364
1365bool QAndroidInputContext::focusObjectIsComposing() const
1366{
1367 return m_composingCursor != -1;
1368}
1369
1370void QAndroidInputContext::focusObjectStartComposing()
1371{
1372 if (focusObjectIsComposing() || m_composingText.isEmpty())
1373 return;
1374
1375 // Composing strings containing newline characters are rare and may cause problems
1376 if (m_composingText.contains(u'\n'))
1377 return;
1378
1379 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1380 if (!query)
1381 return;
1382
1383 if (query->value(Qt::ImCursorPosition).toInt() != query->value(Qt::ImAnchorPosition).toInt())
1384 return;
1385
1386 const int absoluteCursorPos = getAbsoluteCursorPosition(query);
1387 if (absoluteCursorPos < m_composingTextStart
1388 || absoluteCursorPos > m_composingTextStart + m_composingText.length())
1389 return;
1390
1391 m_composingCursor = absoluteCursorPos;
1392
1393 QTextCharFormat underlined;
1394 underlined.setFontUnderline(true);
1395
1396 QInputMethodEvent event(m_composingText, {
1397 { QInputMethodEvent::Cursor, absoluteCursorPos - m_composingTextStart, 1 },
1398 { QInputMethodEvent::TextFormat, 0, int(m_composingText.length()), underlined }
1399 });
1400
1401 event.setCommitString({}, m_composingTextStart - absoluteCursorPos, m_composingText.length());
1402
1403 QGuiApplication::sendEvent(m_focusObject, &event);
1404}
1405
1406bool QAndroidInputContext::focusObjectStopComposing()
1407{
1408 if (!focusObjectIsComposing())
1409 return true; // not composing
1410
1411 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1412 if (query.isNull())
1413 return false;
1414
1415 const int blockPos = getBlockPosition(query);
1416 const int localCursorPos = m_composingCursor - blockPos;
1417
1418 m_composingCursor = -1;
1419
1420 {
1421 // commit the composing test
1422 QList<QInputMethodEvent::Attribute> attributes;
1423 QInputMethodEvent event(QString(), attributes);
1424 event.setCommitString(m_composingText);
1425 sendInputMethodEvent(&event);
1426 }
1427 {
1428 // Moving Qt's cursor to where the preedit cursor used to be
1429 QList<QInputMethodEvent::Attribute> attributes;
1430 attributes.append(
1431 QInputMethodEvent::Attribute(QInputMethodEvent::Selection, localCursorPos, 0));
1432 QInputMethodEvent event(QString(), attributes);
1433 sendInputMethodEvent(&event);
1434 }
1435
1436 return true;
1437}
1438
1440{
1441 jint res = 0;
1442 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1443 if (query.isNull())
1444 return res;
1445
1446 const uint qtInputMethodHints = query->value(Qt::ImHints).toUInt();
1447 const int localPos = query->value(Qt::ImCursorPosition).toInt();
1448
1449 bool atWordBoundary =
1450 localPos == 0
1451 && (!focusObjectIsComposing() || m_composingCursor == m_composingTextStart);
1452
1453 if (!atWordBoundary) {
1454 QString surroundingText = query->value(Qt::ImSurroundingText).toString();
1455 surroundingText.truncate(localPos);
1456 if (focusObjectIsComposing())
1457 surroundingText += QStringView{m_composingText}.left(m_composingCursor - m_composingTextStart);
1458 // Add a character to see if it is at the end of the sentence or not
1459 QTextBoundaryFinder finder(QTextBoundaryFinder::Sentence, surroundingText + u'A');
1460 finder.setPosition(surroundingText.length());
1461 if (finder.isAtBoundary())
1462 atWordBoundary = finder.isAtBoundary();
1463 }
1464 if (atWordBoundary && !(qtInputMethodHints & Qt::ImhLowercaseOnly) && !(qtInputMethodHints & Qt::ImhNoAutoUppercase))
1465 res |= CAP_MODE_SENTENCES;
1466
1467 if (qtInputMethodHints & Qt::ImhUppercaseOnly)
1468 res |= CAP_MODE_CHARACTERS;
1469
1470 return res;
1471}
1472
1473
1474
1475const QAndroidInputContext::ExtractedText &QAndroidInputContext::getExtractedText(jint /*hintMaxChars*/, jint /*hintMaxLines*/, jint /*flags*/)
1476{
1477 // Note to self: "if the GET_EXTRACTED_TEXT_MONITOR flag is set, you should be calling
1478 // updateExtractedText(View, int, ExtractedText) whenever you call
1479 // updateSelection(View, int, int, int, int)." QTBUG-37980
1480
1481 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(
1482 Qt::ImCursorPosition | Qt::ImAbsolutePosition | Qt::ImAnchorPosition);
1483 if (query.isNull())
1484 return m_extractedText;
1485
1486 const int cursorPos = getAbsoluteCursorPosition(query);
1487 const int blockPos = getBlockPosition(query);
1488
1489 // It is documented that we should try to return hintMaxChars
1490 // characters, but standard Android controls always return all text, and
1491 // there are input methods out there that (surprise) seem to depend on
1492 // what happens in reality rather than what's documented.
1493
1494 QVariant textBeforeCursor = QInputMethod::queryFocusObject(Qt::ImTextBeforeCursor, INT_MAX);
1495 QVariant textAfterCursor = QInputMethod::queryFocusObject(Qt::ImTextAfterCursor, INT_MAX);
1496 if (textBeforeCursor.isValid() && textAfterCursor.isValid()) {
1497 if (focusObjectIsComposing()) {
1498 m_extractedText.text =
1499 textBeforeCursor.toString() + m_composingText + textAfterCursor.toString();
1500 } else {
1501 m_extractedText.text = textBeforeCursor.toString() + textAfterCursor.toString();
1502 }
1503
1504 m_extractedText.startOffset = qMax(0, cursorPos - textBeforeCursor.toString().length());
1505 } else {
1506 m_extractedText.text = focusObjectInputMethodQuery(Qt::ImSurroundingText)
1507 ->value(Qt::ImSurroundingText).toString();
1508
1509 if (focusObjectIsComposing())
1510 m_extractedText.text.insert(cursorPos - blockPos, m_composingText);
1511
1512 m_extractedText.startOffset = blockPos;
1513 }
1514
1515 if (focusObjectIsComposing()) {
1516 m_extractedText.selectionStart = m_composingCursor - m_extractedText.startOffset;
1517 m_extractedText.selectionEnd = m_extractedText.selectionStart;
1518 } else {
1519 m_extractedText.selectionStart = cursorPos - m_extractedText.startOffset;
1520 m_extractedText.selectionEnd =
1521 blockPos + query->value(Qt::ImAnchorPosition).toInt() - m_extractedText.startOffset;
1522
1523 // Some keyboards misbehave when selectionStart > selectionEnd
1524 if (m_extractedText.selectionStart > m_extractedText.selectionEnd)
1525 std::swap(m_extractedText.selectionStart, m_extractedText.selectionEnd);
1526 }
1527
1528 return m_extractedText;
1529}
1530
1532{
1533 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1534 if (query.isNull())
1535 return QString();
1536
1537 return query->value(Qt::ImCurrentSelection).toString();
1538}
1539
1540QString QAndroidInputContext::getTextAfterCursor(jint length, jint /*flags*/)
1541{
1542 if (length <= 0)
1543 return QString();
1544
1545 QString text;
1546
1547 QVariant reportedTextAfter = QInputMethod::queryFocusObject(Qt::ImTextAfterCursor, length);
1548 if (reportedTextAfter.isValid()) {
1549 text = reportedTextAfter.toString();
1550 } else {
1551 // Compatibility code for old controls that do not implement the new API
1552 QSharedPointer<QInputMethodQueryEvent> query =
1553 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImSurroundingText);
1554 if (query) {
1555 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1556 text = query->value(Qt::ImSurroundingText).toString().mid(cursorPos);
1557 }
1558 }
1559
1560 if (focusObjectIsComposing()) {
1561 // Controls do not report preedit text, so we have to add it
1562 const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart;
1563 text = QStringView{m_composingText}.mid(cursorPosInsidePreedit) + text;
1564 } else {
1565 // We must not return selected text if there is any
1566 QSharedPointer<QInputMethodQueryEvent> query =
1567 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAnchorPosition);
1568 if (query) {
1569 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1570 const int anchorPos = query->value(Qt::ImAnchorPosition).toInt();
1571 if (anchorPos > cursorPos)
1572 text.remove(0, anchorPos - cursorPos);
1573 }
1574 }
1575
1576 text.truncate(length);
1577 return text;
1578}
1579
1580QString QAndroidInputContext::getTextBeforeCursor(jint length, jint /*flags*/)
1581{
1582 if (length <= 0)
1583 return QString();
1584
1585 QString text;
1586
1587 QVariant reportedTextBefore = QInputMethod::queryFocusObject(Qt::ImTextBeforeCursor, length);
1588 if (reportedTextBefore.isValid()) {
1589 text = reportedTextBefore.toString();
1590 } else {
1591 // Compatibility code for old controls that do not implement the new API
1592 QSharedPointer<QInputMethodQueryEvent> query =
1593 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImSurroundingText);
1594 if (query) {
1595 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1596 text = query->value(Qt::ImSurroundingText).toString().left(cursorPos);
1597 }
1598 }
1599
1600 if (focusObjectIsComposing()) {
1601 // Controls do not report preedit text, so we have to add it
1602 const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart;
1603 text += QStringView{m_composingText}.left(cursorPosInsidePreedit);
1604 } else {
1605 // We must not return selected text if there is any
1606 QSharedPointer<QInputMethodQueryEvent> query =
1607 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAnchorPosition);
1608 if (query) {
1609 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1610 const int anchorPos = query->value(Qt::ImAnchorPosition).toInt();
1611 if (anchorPos < cursorPos)
1612 text.chop(cursorPos - anchorPos);
1613 }
1614 }
1615
1616 if (text.length() > length)
1617 text = text.right(length);
1618 return text;
1619}
1620
1621/*
1622 Android docs say that this function should:
1623 - remove the current composing text, if there is any
1624 - otherwise remove currently selected text, if there is any
1625 - insert new text in place of old composing text or, if there was none, at current cursor position
1626 - mark the inserted text as composing
1627 - move cursor as specified by newCursorPosition: if > 0, it is relative to the end of inserted
1628 text - 1; if <= 0, it is relative to the start of inserted text
1629 */
1630
1631jboolean QAndroidInputContext::setComposingText(const QString &text, jint newCursorPosition)
1632{
1633 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1634 if (query.isNull())
1635 return JNI_FALSE;
1636
1637 BatchEditLock batchEditLock(this);
1638#if QT_CONFIG(accessibility)
1639 markTextEditForAccessibility();
1640#endif
1641
1642 const int absoluteCursorPos = getAbsoluteCursorPosition(query);
1643 int absoluteAnchorPos = getBlockPosition(query) + query->value(Qt::ImAnchorPosition).toInt();
1644
1645 auto setCursorPosition = [=]() {
1646 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1647 QInputMethodEvent event({}, { { QInputMethodEvent::Selection, cursorPos, 0 } });
1648 QGuiApplication::sendEvent(m_focusObject, &event);
1649 };
1650
1651 // If we have composing region and selection (and therefore focusObjectIsComposing() == false),
1652 // we must clear selection so that we won't delete it when we will be replacing composing text
1653 if (!m_composingText.isEmpty() && absoluteCursorPos != absoluteAnchorPos) {
1654 setCursorPosition();
1655 absoluteAnchorPos = absoluteCursorPos;
1656 }
1657
1658 // The value of Qt::ImCursorPosition is not updated at the start
1659 // when the first character is added, so we must update it (QTBUG-85090)
1660 if (absoluteCursorPos == 0 && text.length() == 1 && getTextAfterCursor(1,1).length() >= 0) {
1661 setCursorPosition();
1662 }
1663
1664 // If we had no composing region, pretend that we had a zero-length composing region at current
1665 // cursor position to simplify code. Also account for that we must delete selected text if there
1666 // (still) is any.
1667 const int effectiveAbsoluteCursorPos = qMin(absoluteCursorPos, absoluteAnchorPos);
1668 if (m_composingTextStart == -1)
1669 m_composingTextStart = effectiveAbsoluteCursorPos;
1670
1671 const int oldComposingTextLen = m_composingText.length();
1672 m_composingText = text;
1673
1674 const int newAbsoluteCursorPos =
1675 newCursorPosition <= 0
1676 ? m_composingTextStart + newCursorPosition
1677 : m_composingTextStart + m_composingText.length() + newCursorPosition - 1;
1678
1679 const bool focusObjectWasComposing = focusObjectIsComposing();
1680
1681 // Same checks as in focusObjectStartComposing()
1682 if (!m_composingText.isEmpty() && !m_composingText.contains(u'\n')
1683 && newAbsoluteCursorPos >= m_composingTextStart
1684 && newAbsoluteCursorPos <= m_composingTextStart + m_composingText.length())
1685 m_composingCursor = newAbsoluteCursorPos;
1686 else
1687 m_composingCursor = -1;
1688
1689 if (focusObjectIsComposing()) {
1690 QTextCharFormat underlined;
1691 underlined.setFontUnderline(true);
1692
1693 QInputMethodEvent event(m_composingText, {
1694 { QInputMethodEvent::TextFormat, 0, int(m_composingText.length()), underlined },
1695 { QInputMethodEvent::Cursor, m_composingCursor - m_composingTextStart, 1 }
1696 });
1697
1698 if (oldComposingTextLen > 0 && !focusObjectWasComposing) {
1699 event.setCommitString({}, m_composingTextStart - effectiveAbsoluteCursorPos,
1700 oldComposingTextLen);
1701 }
1702 if (m_composingText.isEmpty())
1703 clear();
1704
1705 QGuiApplication::sendEvent(m_focusObject, &event);
1706 } else {
1707 QInputMethodEvent event({}, {});
1708
1709 if (focusObjectWasComposing) {
1710 event.setCommitString(m_composingText);
1711 } else {
1712 event.setCommitString(m_composingText,
1713 m_composingTextStart - effectiveAbsoluteCursorPos,
1714 oldComposingTextLen);
1715 }
1716 if (m_composingText.isEmpty())
1717 clear();
1718
1719 QGuiApplication::sendEvent(m_focusObject, &event);
1720 }
1721
1722 if (!focusObjectIsComposing() && newCursorPosition != 1) {
1723 // Move cursor using a separate event because if we have inserted or deleted a newline
1724 // character, then we are now inside an another block
1725
1726 const int newBlockPos = getBlockPosition(
1727 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAbsolutePosition));
1728
1729 QInputMethodEvent event({}, {
1730 { QInputMethodEvent::Selection, newAbsoluteCursorPos - newBlockPos, 0 }
1731 });
1732
1733 QGuiApplication::sendEvent(m_focusObject, &event);
1734 }
1735
1736 keyDown();
1737
1738 return JNI_TRUE;
1739}
1740
1741// Android docs say:
1742// * start may be after end, same meaning as if swapped
1743// * this function should not trigger updateSelection, but Android's native EditText does trigger it
1744// * if start == end then we should stop composing
1746{
1747 BatchEditLock batchEditLock(this);
1748
1749 // Qt will not include the current preedit text in the query results, and interprets all
1750 // parameters relative to the text excluding the preedit. The simplest solution is therefore to
1751 // tell Qt that we commit the text before we set the new region. This may cause a little flicker, but is
1752 // much more robust than trying to keep the two different world views in sync
1753
1754 finishComposingText();
1755
1756 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1757 if (query.isNull())
1758 return JNI_FALSE;
1759
1760 if (start == end)
1761 return JNI_TRUE;
1762 if (start > end)
1763 qSwap(start, end);
1764
1765 QString text = query->value(Qt::ImSurroundingText).toString();
1766 int textOffset = getBlockPosition(query);
1767
1768 if (start < textOffset || end > textOffset + text.length()) {
1769 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1770
1771 if (end - textOffset > text.length()) {
1772 const QString after = query->value(Qt::ImTextAfterCursor).toString();
1773 const int additionalSuffixLen = after.length() - (text.length() - cursorPos);
1774
1775 if (additionalSuffixLen > 0)
1776 text += QStringView{after}.right(additionalSuffixLen);
1777 }
1778
1779 if (start < textOffset) {
1780 QString before = query->value(Qt::ImTextBeforeCursor).toString();
1781 before.chop(cursorPos);
1782
1783 if (!before.isEmpty()) {
1784 text = before + text;
1785 textOffset -= before.length();
1786 }
1787 }
1788
1789 if (start < textOffset || end - textOffset > text.length()) {
1790 qCDebug(lcQpaInputMethods) << "Warning: setComposingRegion: failed to retrieve text from composing region";
1791
1792 return JNI_TRUE;
1793 }
1794 }
1795
1796 m_composingText = text.mid(start - textOffset, end - start);
1797 m_composingTextStart = start;
1798
1799 return JNI_TRUE;
1800}
1801
1803{
1804 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1805 if (query.isNull())
1806 return JNI_FALSE;
1807
1808 BatchEditLock batchEditLock(this);
1809
1810 int blockPosition = getBlockPosition(query);
1811 int localCursorPos = start - blockPosition;
1812
1813 if (focusObjectIsComposing() && start == end && start >= m_composingTextStart
1814 && start <= m_composingTextStart + m_composingText.length()) {
1815 // not actually changing the selection; just moving the
1816 // preedit cursor
1817 int localOldPos = query->value(Qt::ImCursorPosition).toInt();
1818 int pos = localCursorPos - localOldPos;
1819 QList<QInputMethodEvent::Attribute> attributes;
1820 attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, pos, 1));
1821
1822 //but we have to tell Qt about the compose text all over again
1823
1824 // Show compose text underlined
1825 QTextCharFormat underlined;
1826 underlined.setFontUnderline(true);
1827 attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat,0, m_composingText.length(),
1828 QVariant(underlined)));
1829 m_composingCursor = start;
1830
1831 QInputMethodEvent event(m_composingText, attributes);
1832 QGuiApplication::sendEvent(m_focusObject, &event);
1833 } else {
1834 // actually changing the selection
1835 focusObjectStopComposing();
1836 QList<QInputMethodEvent::Attribute> attributes;
1837 attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection,
1838 localCursorPos,
1839 end - start));
1840 QInputMethodEvent event({}, attributes);
1841 QGuiApplication::sendEvent(m_focusObject, &event);
1842 }
1843 return JNI_TRUE;
1844}
1845
1847{
1848 BatchEditLock batchEditLock(this);
1849
1850 focusObjectStopComposing();
1851 m_handleMode = ShowCursor;
1852 sendShortcut(QKeySequence::SelectAll);
1853 return JNI_TRUE;
1854}
1855
1857{
1858 BatchEditLock batchEditLock(this);
1859
1860 // This is probably not what native EditText would do, but normally if there is selection, then
1861 // there will be no composing region
1862 finishComposingText();
1863
1864 m_handleMode = ShowCursor;
1865 sendShortcut(QKeySequence::Cut);
1866 return JNI_TRUE;
1867}
1868
1870{
1871 BatchEditLock batchEditLock(this);
1872
1873 focusObjectStopComposing();
1874 m_handleMode = ShowCursor;
1875 sendShortcut(QKeySequence::Copy);
1876 return JNI_TRUE;
1877}
1878
1880{
1881#warning TODO
1882 return JNI_FALSE;
1883}
1884
1886{
1887 BatchEditLock batchEditLock(this);
1888
1889 // TODO: This is not what native EditText does
1890 finishComposingText();
1891
1892 m_handleMode = ShowCursor;
1893 sendShortcut(QKeySequence::Paste);
1894 return JNI_TRUE;
1895}
1896
1897void QAndroidInputContext::sendShortcut(const QKeySequence &sequence)
1898{
1899 for (int i = 0; i < sequence.count(); ++i) {
1900 const QKeyCombination keys = sequence[i];
1901 Qt::Key key = Qt::Key(keys.toCombined() & ~Qt::KeyboardModifierMask);
1902 Qt::KeyboardModifiers mod = Qt::KeyboardModifiers(keys.toCombined() & Qt::KeyboardModifierMask);
1903
1904 QKeyEvent pressEvent(QEvent::KeyPress, key, mod);
1905 QKeyEvent releaseEvent(QEvent::KeyRelease, key, mod);
1906
1907 QGuiApplication::sendEvent(m_focusObject, &pressEvent);
1908 QGuiApplication::sendEvent(m_focusObject, &releaseEvent);
1909 }
1910}
1911
1912QSharedPointer<QInputMethodQueryEvent> QAndroidInputContext::focusObjectInputMethodQuery(Qt::InputMethodQueries queries) {
1913 if (!qGuiApp)
1914 return {};
1915
1916 QObject *focusObject = qGuiApp->focusObject();
1917 if (!focusObject)
1918 return {};
1919
1920 QInputMethodQueryEvent *ret = new QInputMethodQueryEvent(queries);
1921 QCoreApplication::sendEvent(focusObject, ret);
1922 return QSharedPointer<QInputMethodQueryEvent>(ret);
1923}
1924
1925void QAndroidInputContext::sendInputMethodEvent(QInputMethodEvent *event)
1926{
1927 if (!qGuiApp)
1928 return;
1929
1930 QObject *focusObject = qGuiApp->focusObject();
1931 if (!focusObject)
1932 return;
1933
1934 QCoreApplication::sendEvent(focusObject, event);
1935}
1936
1937QT_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 QPlatformWindow * focusWindowHandle()
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