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