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