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()) {
539 return;
540 }
541 }
543}
544
546{
547 focusObjectStopComposing();
548}
549
551{
552 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
553 if (!query.isNull() && m_batchEditNestingLevel == 0) {
554 const int cursorPos = getAbsoluteCursorPosition(query);
555 const int composeLength = m_composingText.length();
556
557 //Q_ASSERT(m_composingText.isEmpty() == (m_composingTextStart == -1));
558 if (m_composingText.isEmpty() != (m_composingTextStart == -1))
559 qWarning() << "Input method out of sync" << m_composingText << m_composingTextStart;
560
561 int realSelectionStart = cursorPos;
562 int realSelectionEnd = cursorPos;
563
564 int cpos = query->value(Qt::ImCursorPosition).toInt();
565 int anchor = query->value(Qt::ImAnchorPosition).toInt();
566 if (cpos != anchor) {
567 if (!m_composingText.isEmpty()) {
568 qWarning("Selecting text while preediting may give unpredictable results.");
569 focusObjectStopComposing();
570 }
571 int blockPos = getBlockPosition(query);
572 realSelectionStart = blockPos + cpos;
573 realSelectionEnd = blockPos + anchor;
574 }
575 // Qt's idea of the cursor position is the start of the preedit area, so we maintain our own preedit cursor pos
576 if (focusObjectIsComposing())
577 realSelectionStart = realSelectionEnd = m_composingCursor;
578
579 // Some keyboards misbahave when selStart > selEnd
580 if (realSelectionStart > realSelectionEnd)
581 std::swap(realSelectionStart, realSelectionEnd);
582
583 QtAndroidInput::updateSelection(realSelectionStart, realSelectionEnd,
584 m_composingTextStart, m_composingTextStart + composeLength); // pre-edit text
585 }
586}
587
588bool QAndroidInputContext::isImhNoTextHandlesSet()
589{
590 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
591 if (query.isNull())
592 return false;
593 return query->value(Qt::ImHints).toUInt() & Qt::ImhNoTextHandles;
594}
595
597{
598 if (m_fullScreenMode) {
599 QtAndroidInput::updateHandles(Hidden);
600 return;
601 }
602 static bool noHandles = qEnvironmentVariableIntValue("QT_QPA_NO_TEXT_HANDLES");
603 if (noHandles || !m_focusObject)
604 return;
605
606 if (isImhNoTextHandlesSet()) {
607 QtAndroidInput::updateHandles(Hidden);
608 return;
609 }
610
611 auto im = qGuiApp->inputMethod();
612
613 QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition | Qt::ImEnabled
614 | Qt::ImCurrentSelection | Qt::ImHints | Qt::ImSurroundingText
615 | Qt::ImReadOnly);
616 QCoreApplication::sendEvent(m_focusObject, &query);
617
618 int cpos = query.value(Qt::ImCursorPosition).toInt();
619 int anchor = query.value(Qt::ImAnchorPosition).toInt();
620 const QVariant readOnlyVariant = query.value(Qt::ImReadOnly);
621 bool readOnly = readOnlyVariant.toBool();
622 QPlatformWindow *qPlatformWindow = qGuiApp->focusWindow()->handle();
623
624 if (!readOnly && ((m_handleMode & 0xff) == Hidden)) {
625 QtAndroidInput::updateHandles(Hidden);
626 return;
627 }
628
629 if ( cpos == anchor && (!readOnlyVariant.isValid() || readOnly)) {
630 QtAndroidInput::updateHandles(Hidden);
631 return;
632 }
633
634 if (cpos == anchor || im->anchorRectangle().isNull()) {
635 auto curRect = cursorRectangle();
636 QPoint cursorPointGlobal = QPoint(curRect.x() + (curRect.width() / 2), curRect.y() + curRect.height());
637 QPoint cursorPoint(curRect.center().x(), curRect.bottom());
638 int x = curRect.x();
639 int y = curRect.y();
640
641 // Use x and y for the editMenuPoint from the cursorPointGlobal when the cursor is in the Dialog
642 if (cursorPointGlobal != cursorPoint) {
643 x = cursorPointGlobal.x();
644 y = cursorPointGlobal.y();
645 }
646
647 QPoint editMenuPoint(x, y);
648 m_handleMode &= ShowEditPopup;
649 m_handleMode |= ShowCursor;
650 uint32_t buttons = readOnly ? 0 : EditContext::PasteButton;
651 if (!query.value(Qt::ImSurroundingText).toString().isEmpty())
653 QtAndroidInput::updateHandles(m_handleMode, editMenuPoint, buttons, cursorPointGlobal);
654 m_hideCursorHandleTimer.start();
655
656 return;
657 }
658
659 m_handleMode = ShowSelection | ShowEditPopup ;
660 auto leftRect = cursorRectangle();
661 auto rightRect = anchorRectangle();
662 if (cpos > anchor)
663 std::swap(leftRect, rightRect);
664 //Move the left or right select handle to the center from the screen edge
665 //the select handle is close to or over the screen edge. Otherwise, the
666 //select handle might go out of the screen and it would be impossible to drag.
667 QPoint leftPoint(qPlatformWindow->mapToGlobal(leftRect.bottomLeft().toPoint()));
668 QPoint rightPoint(qPlatformWindow->mapToGlobal(rightRect.bottomRight().toPoint()));
669
671 if (platformIntegration) {
672 if (m_selectHandleWidth == 0)
674
675 int rightSideOfScreen = platformIntegration->screen()->availableGeometry().right();
676 if (leftPoint.x() < m_selectHandleWidth)
677 leftPoint.setX(m_selectHandleWidth);
678 leftPoint = qPlatformWindow->mapFromGlobal(leftPoint);
679
680 if (rightPoint.x() > rightSideOfScreen - m_selectHandleWidth)
681 rightPoint.setX(rightSideOfScreen - m_selectHandleWidth);
682 rightPoint = qPlatformWindow->mapFromGlobal(rightPoint);
683
684 QPoint editPoint(leftRect.united(rightRect).topLeft().toPoint());
685 uint32_t buttons = readOnly ? EditContext::CopyButton | EditContext::SelectAllButton
687
688 QtAndroidInput::updateHandles(m_handleMode, editPoint, buttons, leftPoint, rightPoint,
689 query.value(Qt::ImCurrentSelection).toString().isRightToLeft());
690 m_hideCursorHandleTimer.stop();
691 }
692}
693
694/*
695 Called from Java when a cursor/selection handle was dragged to a new position
696
697 handleId of 1 means the cursor handle, 2 means the left handle, 3 means the right handle
698 */
699void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y)
700{
701 if (m_batchEditNestingLevel != 0) {
702 qWarning() << "QAndroidInputContext::handleLocationChanged returned";
703 return;
704 }
705 QPoint point(x, y);
706
707 // The handle is down of the cursor, but we want the position in the middle.
708 QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition
709 | Qt::ImAbsolutePosition | Qt::ImCurrentSelection);
710 QCoreApplication::sendEvent(m_focusObject, &query);
711 int cpos = query.value(Qt::ImCursorPosition).toInt();
712 int anchor = query.value(Qt::ImAnchorPosition).toInt();
713 auto leftRect = cursorRectangle();
714 auto rightRect = anchorRectangle();
715 if (cpos > anchor)
716 std::swap(leftRect, rightRect);
717
718 // Do not allow dragging left handle below right handle, or right handle above left handle
719 if (handleId == 2 && point.y() > rightRect.center().y()) {
720 point.setY(rightRect.center().y());
721 } else if (handleId == 3 && point.y() < leftRect.center().y()) {
722 point.setY(leftRect.center().y());
723 }
724
725 bool ok;
726 auto object = m_focusObject->parent();
727 int dialogMoveX = 0;
728 while (object) {
729 if (QString::compare(object->metaObject()->className(),
730 "QDialog", Qt::CaseInsensitive) == 0) {
731 dialogMoveX += object->property("x").toInt();
732 }
733 object = object->parent();
734 };
735
736 auto position =
737 QPointF(QHighDpi::fromNativePixels(point, QGuiApplication::focusWindow()));
738 const QPointF fixedPosition = QPointF(position.x() - dialogMoveX, position.y());
739 const QInputMethod *im = QGuiApplication::inputMethod();
740 const QTransform mapToLocal = im->inputItemTransform().inverted();
741 const int handlePos = im->queryFocusObject(Qt::ImCursorPosition, mapToLocal.map(fixedPosition)).toInt(&ok);
742
743 if (!ok)
744 return;
745
746 int newCpos = cpos;
747 int newAnchor = anchor;
748 if (newAnchor > newCpos)
749 std::swap(newAnchor, newCpos);
750
751 if (handleId == 1) {
752 newCpos = handlePos;
753 newAnchor = handlePos;
754 } else if (handleId == 2) {
755 newAnchor = handlePos;
756 } else if (handleId == 3) {
757 newCpos = handlePos;
758 }
759
760 /*
761 Do not allow clearing selection by dragging selection handles and do not allow swapping
762 selection handles for consistency with Android's native text editing controls. Ensure that at
763 least one symbol remains selected.
764 */
765 if ((handleId == 2 || handleId == 3) && newCpos <= newAnchor) {
766 QTextBoundaryFinder finder(QTextBoundaryFinder::Grapheme,
767 query.value(Qt::ImCurrentSelection).toString());
768
769 const int oldSelectionStartPos = qMin(cpos, anchor);
770
771 if (handleId == 2) {
772 finder.toEnd();
773 finder.toPreviousBoundary();
774 newAnchor = finder.position() + oldSelectionStartPos;
775 } else {
776 finder.toStart();
777 finder.toNextBoundary();
778 newCpos = finder.position() + oldSelectionStartPos;
779 }
780 }
781
782 // Check if handle has been dragged far enough
783 if (!focusObjectIsComposing() && newCpos == cpos && newAnchor == anchor)
784 return;
785
786 /*
787 If the editor is currently in composing state, we have to compare newCpos with
788 m_composingCursor instead of cpos. And since there is nothing to compare with newAnchor, we
789 perform the check only when user drags the cursor handle.
790 */
791 if (focusObjectIsComposing() && handleId == 1) {
792 int absoluteCpos = query.value(Qt::ImAbsolutePosition).toInt(&ok);
793 if (!ok)
794 absoluteCpos = cpos;
795 const int blockPos = absoluteCpos - cpos;
796
797 if (blockPos + newCpos == m_composingCursor)
798 return;
799 }
800
801 BatchEditLock batchEditLock(this);
802
803 focusObjectStopComposing();
804
805 QList<QInputMethodEvent::Attribute> attributes;
806 attributes.append({ QInputMethodEvent::Selection, newAnchor, newCpos - newAnchor });
807 if (newCpos != newAnchor)
808 attributes.append({ QInputMethodEvent::Cursor, 0, 0 });
809
810 QInputMethodEvent event(QString(), attributes);
811 QGuiApplication::sendEvent(m_focusObject, &event);
812}
813
815{
816 if (m_focusObject && screenInputItemRectangle().contains(x, y)) {
817 // If the user touch the input rectangle, we can show the cursor handle
818 m_handleMode = ShowCursor;
819 // The VK will appear in a moment, stop the timer
820 m_hideCursorHandleTimer.stop();
821
822 if (focusObjectIsComposing()) {
823 const int curBlockPos = getBlockPosition(
824 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAbsolutePosition));
825 const int touchPosition = curBlockPos
826 + queryFocusObject(Qt::ImCursorPosition, QPointF(x, y)).toInt();
827 if (touchPosition != m_composingCursor)
828 focusObjectStopComposing();
829 }
830
831 // Check if cursor is visible in focused window before updating handles
832 QPlatformWindow *window = qGuiApp->focusWindow()->handle();
833 const QRectF curRect = cursorRectangle();
834 const QPoint cursorGlobalPoint = window->mapToGlobal(QPoint(curRect.x(), curRect.y()));
835 const QRect windowRect = QPlatformInputContext::inputItemClipRectangle().toRect();
836 const QRect windowGlobalRect = QRect(window->mapToGlobal(windowRect.topLeft()), windowRect.size());
837
838 if (windowGlobalRect.contains(cursorGlobalPoint.x(), cursorGlobalPoint.y()))
840 }
841}
842
844{
845 static bool noHandles = qEnvironmentVariableIntValue("QT_QPA_NO_TEXT_HANDLES");
846 if (noHandles)
847 return;
848
849 if (m_focusObject && screenInputItemRectangle().contains(x, y)) {
850 BatchEditLock batchEditLock(this);
851
852 focusObjectStopComposing();
853 const QPointF touchPoint(x, y);
854 setSelectionOnFocusObject(touchPoint, touchPoint);
855
856 QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition | Qt::ImTextBeforeCursor | Qt::ImTextAfterCursor);
857 QCoreApplication::sendEvent(m_focusObject, &query);
858 int cursor = query.value(Qt::ImCursorPosition).toInt();
859 int anchor = cursor;
860 QString before = query.value(Qt::ImTextBeforeCursor).toString();
861 QString after = query.value(Qt::ImTextAfterCursor).toString();
862 for (const auto &ch : after) {
863 if (!ch.isLetterOrNumber())
864 break;
865 ++anchor;
866 }
867
868 for (auto itch = before.rbegin(); itch != after.rend(); ++itch) {
869 if (!itch->isLetterOrNumber())
870 break;
871 --cursor;
872 }
873 if (cursor == anchor || cursor < 0 || cursor - anchor > 500) {
874 m_handleMode = ShowCursor | ShowEditPopup;
876 return;
877 }
878 QList<QInputMethodEvent::Attribute> imAttributes;
879 imAttributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, 0, 0, QVariant()));
880 imAttributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection, anchor, cursor - anchor, QVariant()));
881 QInputMethodEvent event(QString(), imAttributes);
882 QGuiApplication::sendEvent(m_focusObject, &event);
883
884 m_handleMode = ShowSelection | ShowEditPopup;
886 }
887}
888
890{
891 if (m_handleMode) {
892 // When the user enter text on the keyboard, we hide the cursor handle
893 m_handleMode = Hidden;
895 }
896}
897
899{
900 if (m_handleMode & ShowSelection) {
901 m_handleMode = Hidden;
903 } else {
904 m_hideCursorHandleTimer.start();
905 }
906}
907
908void QAndroidInputContext::update(Qt::InputMethodQueries queries)
909{
910 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(queries);
911 if (query.isNull())
912 return;
913#warning TODO extract the needed data from query
914}
915
916void QAndroidInputContext::invokeAction(QInputMethod::Action action, int cursorPosition)
917{
918#warning TODO Handle at least QInputMethod::ContextMenu action
919 Q_UNUSED(action);
920 Q_UNUSED(cursorPosition);
921 //### click should be passed to the IM, but in the meantime it's better to ignore it than to do something wrong
922 // if (action == QInputMethod::Click)
923 // commit();
924}
925
927{
928 return QtAndroidInput::softwareKeyboardRect();
929}
930
932{
933 return false;
934}
935
937{
938 if (QGuiApplication::applicationState() != Qt::ApplicationActive) {
939 connect(qGuiApp, SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(showInputPanelLater(Qt::ApplicationState)));
940 return;
941 }
942 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
943 if (query.isNull())
944 return;
945
946 if (!qGuiApp->focusWindow()->handle())
947 return; // not a real window, probably VR/XR
948
949 disconnect(m_updateCursorPosConnection);
950 m_updateCursorPosConnection = {};
951
952 if (qGuiApp->focusObject()->metaObject()->indexOfSignal("cursorPositionChanged(int,int)") >= 0) // QLineEdit breaks the pattern
953 m_updateCursorPosConnection = connect(qGuiApp->focusObject(), SIGNAL(cursorPositionChanged(int,int)), this, SLOT(updateCursorPosition()));
954 else if (qGuiApp->focusObject()->metaObject()->indexOfSignal("cursorPositionChanged()") >= 0)
955 m_updateCursorPosConnection = connect(qGuiApp->focusObject(), SIGNAL(cursorPositionChanged()), this, SLOT(updateCursorPosition()));
956
957 QRect rect = QPlatformInputContext::inputItemRectangle().toRect();
958 QtAndroidInput::showSoftwareKeyboard(rect.left(), rect.top(), rect.width(), rect.height(),
959 query->value(Qt::ImHints).toUInt(),
960 query->value(Qt::ImEnterKeyType).toUInt());
961}
962
963void QAndroidInputContext::showInputPanelLater(Qt::ApplicationState state)
964{
965 if (state != Qt::ApplicationActive)
966 return;
967 disconnect(qGuiApp, SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(showInputPanelLater(Qt::ApplicationState)));
969}
970
971void QAndroidInputContext::safeCall(const std::function<void()> &func, Qt::ConnectionType conType)
972{
973 if (qGuiApp->thread() == QThread::currentThread())
974 func();
975 else
976 QMetaObject::invokeMethod(this, "safeCall", conType, Q_ARG(std::function<void()>, func));
977}
978
983
988
990{
991 return m_composingText.length();
992}
993
995{
996 m_composingText.clear();
997 m_composingTextStart = -1;
998 m_composingCursor = -1;
999 m_extractedText.clear();
1000}
1001
1002
1004{
1005 return m_focusObject;
1006}
1007
1009{
1010 if (object != m_focusObject) {
1011 focusObjectStopComposing();
1012 m_focusObject = object;
1013 reset();
1014 }
1016}
1017
1019{
1020 ++m_batchEditNestingLevel;
1021 return JNI_TRUE;
1022}
1023
1025{
1026 if (--m_batchEditNestingLevel == 0) { //ending batch edit mode
1027 focusObjectStartComposing();
1029 }
1030 return JNI_TRUE;
1031}
1032
1033/*
1034 Android docs say: This behaves like calling setComposingText(text, newCursorPosition) then
1035 finishComposingText().
1036*/
1037jboolean QAndroidInputContext::commitText(const QString &text, jint newCursorPosition)
1038{
1039 BatchEditLock batchEditLock(this);
1040 return setComposingText(text, newCursorPosition) && finishComposingText();
1041}
1042
1043jboolean QAndroidInputContext::deleteSurroundingText(jint leftLength, jint rightLength)
1044{
1045 BatchEditLock batchEditLock(this);
1046
1047 focusObjectStopComposing();
1048
1049 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1050 if (query.isNull())
1051 return JNI_TRUE;
1052
1053 if (leftLength < 0) {
1054 rightLength += -leftLength;
1055 leftLength = 0;
1056 }
1057
1058 const int initialBlockPos = getBlockPosition(query);
1059 const int initialCursorPos = getAbsoluteCursorPosition(query);
1060 const int initialAnchorPos = initialBlockPos + query->value(Qt::ImAnchorPosition).toInt();
1061
1062 /*
1063 According to documentation, we should delete leftLength characters before current selection
1064 and rightLength characters after current selection (without affecting selection). But that is
1065 absolutely not what Android's native EditText does. It deletes leftLength characters before
1066 min(selection start, composing region start) and rightLength characters after max(selection
1067 end, composing region end). There are no known keyboards that depend on this behavior, but
1068 it is better to be consistent with EditText behavior, because there definitely should be no
1069 keyboards that depend on documented behavior.
1070 */
1071 const int leftEnd =
1072 m_composingText.isEmpty()
1073 ? qMin(initialCursorPos, initialAnchorPos)
1074 : qMin(qMin(initialCursorPos, initialAnchorPos), m_composingTextStart);
1075
1076 const int rightBegin =
1077 m_composingText.isEmpty()
1078 ? qMax(initialCursorPos, initialAnchorPos)
1079 : qMax(qMax(initialCursorPos, initialAnchorPos),
1080 m_composingTextStart + m_composingText.length());
1081
1082 int textBeforeCursorLen;
1083 int textAfterCursorLen;
1084
1085 QVariant textBeforeCursor = query->value(Qt::ImTextBeforeCursor);
1086 QVariant textAfterCursor = query->value(Qt::ImTextAfterCursor);
1087 if (textBeforeCursor.isValid() && textAfterCursor.isValid()) {
1088 textBeforeCursorLen = textBeforeCursor.toString().length();
1089 textAfterCursorLen = textAfterCursor.toString().length();
1090 } else {
1091 textBeforeCursorLen = initialCursorPos - initialBlockPos;
1092 textAfterCursorLen =
1093 query->value(Qt::ImSurroundingText).toString().length() - textBeforeCursorLen;
1094 }
1095
1096 leftLength = qMin(qMax(0, textBeforeCursorLen - (initialCursorPos - leftEnd)), leftLength);
1097 rightLength = qMin(qMax(0, textAfterCursorLen - (rightBegin - initialCursorPos)), rightLength);
1098
1099 if (leftLength == 0 && rightLength == 0)
1100 return JNI_TRUE;
1101
1102 if (leftEnd == rightBegin) {
1103 // We have no selection and no composing region; we can do everything using one event
1104 QInputMethodEvent event;
1105 event.setCommitString({}, -leftLength, leftLength + rightLength);
1106 QGuiApplication::sendEvent(m_focusObject, &event);
1107 } else {
1108 if (initialCursorPos != initialAnchorPos) {
1109 QInputMethodEvent event({}, {
1110 { QInputMethodEvent::Selection, initialCursorPos - initialBlockPos, 0 }
1111 });
1112
1113 QGuiApplication::sendEvent(m_focusObject, &event);
1114 }
1115
1116 int currentCursorPos = initialCursorPos;
1117
1118 if (rightLength > 0) {
1119 QInputMethodEvent event;
1120 event.setCommitString({}, rightBegin - currentCursorPos, rightLength);
1121 QGuiApplication::sendEvent(m_focusObject, &event);
1122
1123 currentCursorPos = rightBegin;
1124 }
1125
1126 if (leftLength > 0) {
1127 const int leftBegin = leftEnd - leftLength;
1128
1129 QInputMethodEvent event;
1130 event.setCommitString({}, leftBegin - currentCursorPos, leftLength);
1131 QGuiApplication::sendEvent(m_focusObject, &event);
1132
1133 currentCursorPos = leftBegin;
1134
1135 if (!m_composingText.isEmpty())
1136 m_composingTextStart -= leftLength;
1137 }
1138
1139 // Restore cursor position or selection
1140 if (currentCursorPos != initialCursorPos - leftLength
1141 || initialCursorPos != initialAnchorPos) {
1142 // If we have deleted a newline character, we are now in a new block
1143 const int currentBlockPos = getBlockPosition(
1144 focusObjectInputMethodQuery(Qt::ImAbsolutePosition | Qt::ImCursorPosition));
1145
1146 QInputMethodEvent event({}, {
1147 { QInputMethodEvent::Selection, initialCursorPos - leftLength - currentBlockPos,
1148 initialAnchorPos - initialCursorPos },
1149 { QInputMethodEvent::Cursor, 0, 0 }
1150 });
1151
1152 QGuiApplication::sendEvent(m_focusObject, &event);
1153 }
1154 }
1155
1156 return JNI_TRUE;
1157}
1158
1159// Android docs say the cursor must not move
1161{
1162 BatchEditLock batchEditLock(this);
1163
1164 if (!focusObjectStopComposing())
1165 return JNI_FALSE;
1166
1167 clear();
1168 return JNI_TRUE;
1169}
1170
1171/*
1172 Android docs say: This behaves like calling finishComposingText(), setSelection(start, end)
1173 and then commitText(text, newCursorPosition, textAttribute)
1174 https://developer.android.com/reference/android/view/inputmethod/InputConnection#replaceText(int,%20int,%20java.lang.CharSequence,%20int,%20android.view.inputmethod.TextAttribute)
1175*/
1176jboolean QAndroidInputContext::replaceText(jint start, jint end, const QString text, jint newCursorPosition)
1177{
1178 if (!finishComposingText())
1179 return JNI_FALSE;
1180 if (!setSelection(start, end))
1181 return JNI_FALSE;
1182
1183 return commitText(text, newCursorPosition);
1184}
1185
1187{
1188 m_fullScreenMode = enabled;
1189 BatchEditLock batchEditLock(this);
1190 if (!focusObjectStopComposing())
1191 return;
1192
1193 if (enabled)
1194 m_handleMode = Hidden;
1195
1197}
1198
1199// Called in calling thread's context
1201{
1202 return m_fullScreenMode;
1203}
1204
1205bool QAndroidInputContext::focusObjectIsComposing() const
1206{
1207 return m_composingCursor != -1;
1208}
1209
1210void QAndroidInputContext::focusObjectStartComposing()
1211{
1212 if (focusObjectIsComposing() || m_composingText.isEmpty())
1213 return;
1214
1215 // Composing strings containing newline characters are rare and may cause problems
1216 if (m_composingText.contains(u'\n'))
1217 return;
1218
1219 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1220 if (!query)
1221 return;
1222
1223 if (query->value(Qt::ImCursorPosition).toInt() != query->value(Qt::ImAnchorPosition).toInt())
1224 return;
1225
1226 const int absoluteCursorPos = getAbsoluteCursorPosition(query);
1227 if (absoluteCursorPos < m_composingTextStart
1228 || absoluteCursorPos > m_composingTextStart + m_composingText.length())
1229 return;
1230
1231 m_composingCursor = absoluteCursorPos;
1232
1233 QTextCharFormat underlined;
1234 underlined.setFontUnderline(true);
1235
1236 QInputMethodEvent event(m_composingText, {
1237 { QInputMethodEvent::Cursor, absoluteCursorPos - m_composingTextStart, 1 },
1238 { QInputMethodEvent::TextFormat, 0, int(m_composingText.length()), underlined }
1239 });
1240
1241 event.setCommitString({}, m_composingTextStart - absoluteCursorPos, m_composingText.length());
1242
1243 QGuiApplication::sendEvent(m_focusObject, &event);
1244}
1245
1246bool QAndroidInputContext::focusObjectStopComposing()
1247{
1248 if (!focusObjectIsComposing())
1249 return true; // not composing
1250
1251 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1252 if (query.isNull())
1253 return false;
1254
1255 const int blockPos = getBlockPosition(query);
1256 const int localCursorPos = m_composingCursor - blockPos;
1257
1258 m_composingCursor = -1;
1259
1260 {
1261 // commit the composing test
1262 QList<QInputMethodEvent::Attribute> attributes;
1263 QInputMethodEvent event(QString(), attributes);
1264 event.setCommitString(m_composingText);
1265 sendInputMethodEvent(&event);
1266 }
1267 {
1268 // Moving Qt's cursor to where the preedit cursor used to be
1269 QList<QInputMethodEvent::Attribute> attributes;
1270 attributes.append(
1271 QInputMethodEvent::Attribute(QInputMethodEvent::Selection, localCursorPos, 0));
1272 QInputMethodEvent event(QString(), attributes);
1273 sendInputMethodEvent(&event);
1274 }
1275
1276 return true;
1277}
1278
1280{
1281 jint res = 0;
1282 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1283 if (query.isNull())
1284 return res;
1285
1286 const uint qtInputMethodHints = query->value(Qt::ImHints).toUInt();
1287 const int localPos = query->value(Qt::ImCursorPosition).toInt();
1288
1289 bool atWordBoundary =
1290 localPos == 0
1291 && (!focusObjectIsComposing() || m_composingCursor == m_composingTextStart);
1292
1293 if (!atWordBoundary) {
1294 QString surroundingText = query->value(Qt::ImSurroundingText).toString();
1295 surroundingText.truncate(localPos);
1296 if (focusObjectIsComposing())
1297 surroundingText += QStringView{m_composingText}.left(m_composingCursor - m_composingTextStart);
1298 // Add a character to see if it is at the end of the sentence or not
1299 QTextBoundaryFinder finder(QTextBoundaryFinder::Sentence, surroundingText + u'A');
1300 finder.setPosition(surroundingText.length());
1301 if (finder.isAtBoundary())
1302 atWordBoundary = finder.isAtBoundary();
1303 }
1304 if (atWordBoundary && !(qtInputMethodHints & Qt::ImhLowercaseOnly) && !(qtInputMethodHints & Qt::ImhNoAutoUppercase))
1305 res |= CAP_MODE_SENTENCES;
1306
1307 if (qtInputMethodHints & Qt::ImhUppercaseOnly)
1308 res |= CAP_MODE_CHARACTERS;
1309
1310 return res;
1311}
1312
1313
1314
1315const QAndroidInputContext::ExtractedText &QAndroidInputContext::getExtractedText(jint /*hintMaxChars*/, jint /*hintMaxLines*/, jint /*flags*/)
1316{
1317 // Note to self: "if the GET_EXTRACTED_TEXT_MONITOR flag is set, you should be calling
1318 // updateExtractedText(View, int, ExtractedText) whenever you call
1319 // updateSelection(View, int, int, int, int)." QTBUG-37980
1320
1321 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(
1322 Qt::ImCursorPosition | Qt::ImAbsolutePosition | Qt::ImAnchorPosition);
1323 if (query.isNull())
1324 return m_extractedText;
1325
1326 const int cursorPos = getAbsoluteCursorPosition(query);
1327 const int blockPos = getBlockPosition(query);
1328
1329 // It is documented that we should try to return hintMaxChars
1330 // characters, but standard Android controls always return all text, and
1331 // there are input methods out there that (surprise) seem to depend on
1332 // what happens in reality rather than what's documented.
1333
1334 QVariant textBeforeCursor = QInputMethod::queryFocusObject(Qt::ImTextBeforeCursor, INT_MAX);
1335 QVariant textAfterCursor = QInputMethod::queryFocusObject(Qt::ImTextAfterCursor, INT_MAX);
1336 if (textBeforeCursor.isValid() && textAfterCursor.isValid()) {
1337 if (focusObjectIsComposing()) {
1338 m_extractedText.text =
1339 textBeforeCursor.toString() + m_composingText + textAfterCursor.toString();
1340 } else {
1341 m_extractedText.text = textBeforeCursor.toString() + textAfterCursor.toString();
1342 }
1343
1344 m_extractedText.startOffset = qMax(0, cursorPos - textBeforeCursor.toString().length());
1345 } else {
1346 m_extractedText.text = focusObjectInputMethodQuery(Qt::ImSurroundingText)
1347 ->value(Qt::ImSurroundingText).toString();
1348
1349 if (focusObjectIsComposing())
1350 m_extractedText.text.insert(cursorPos - blockPos, m_composingText);
1351
1352 m_extractedText.startOffset = blockPos;
1353 }
1354
1355 if (focusObjectIsComposing()) {
1356 m_extractedText.selectionStart = m_composingCursor - m_extractedText.startOffset;
1357 m_extractedText.selectionEnd = m_extractedText.selectionStart;
1358 } else {
1359 m_extractedText.selectionStart = cursorPos - m_extractedText.startOffset;
1360 m_extractedText.selectionEnd =
1361 blockPos + query->value(Qt::ImAnchorPosition).toInt() - m_extractedText.startOffset;
1362
1363 // Some keyboards misbehave when selectionStart > selectionEnd
1364 if (m_extractedText.selectionStart > m_extractedText.selectionEnd)
1365 std::swap(m_extractedText.selectionStart, m_extractedText.selectionEnd);
1366 }
1367
1368 return m_extractedText;
1369}
1370
1372{
1373 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1374 if (query.isNull())
1375 return QString();
1376
1377 return query->value(Qt::ImCurrentSelection).toString();
1378}
1379
1380QString QAndroidInputContext::getTextAfterCursor(jint length, jint /*flags*/)
1381{
1382 if (length <= 0)
1383 return QString();
1384
1385 QString text;
1386
1387 QVariant reportedTextAfter = QInputMethod::queryFocusObject(Qt::ImTextAfterCursor, length);
1388 if (reportedTextAfter.isValid()) {
1389 text = reportedTextAfter.toString();
1390 } else {
1391 // Compatibility code for old controls that do not implement the new API
1392 QSharedPointer<QInputMethodQueryEvent> query =
1393 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImSurroundingText);
1394 if (query) {
1395 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1396 text = query->value(Qt::ImSurroundingText).toString().mid(cursorPos);
1397 }
1398 }
1399
1400 if (focusObjectIsComposing()) {
1401 // Controls do not report preedit text, so we have to add it
1402 const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart;
1403 text = QStringView{m_composingText}.mid(cursorPosInsidePreedit) + text;
1404 } else {
1405 // We must not return selected text if there is any
1406 QSharedPointer<QInputMethodQueryEvent> query =
1407 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAnchorPosition);
1408 if (query) {
1409 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1410 const int anchorPos = query->value(Qt::ImAnchorPosition).toInt();
1411 if (anchorPos > cursorPos)
1412 text.remove(0, anchorPos - cursorPos);
1413 }
1414 }
1415
1416 text.truncate(length);
1417 return text;
1418}
1419
1420QString QAndroidInputContext::getTextBeforeCursor(jint length, jint /*flags*/)
1421{
1422 if (length <= 0)
1423 return QString();
1424
1425 QString text;
1426
1427 QVariant reportedTextBefore = QInputMethod::queryFocusObject(Qt::ImTextBeforeCursor, length);
1428 if (reportedTextBefore.isValid()) {
1429 text = reportedTextBefore.toString();
1430 } else {
1431 // Compatibility code for old controls that do not implement the new API
1432 QSharedPointer<QInputMethodQueryEvent> query =
1433 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImSurroundingText);
1434 if (query) {
1435 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1436 text = query->value(Qt::ImSurroundingText).toString().left(cursorPos);
1437 }
1438 }
1439
1440 if (focusObjectIsComposing()) {
1441 // Controls do not report preedit text, so we have to add it
1442 const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart;
1443 text += QStringView{m_composingText}.left(cursorPosInsidePreedit);
1444 } else {
1445 // We must not return selected text if there is any
1446 QSharedPointer<QInputMethodQueryEvent> query =
1447 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAnchorPosition);
1448 if (query) {
1449 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1450 const int anchorPos = query->value(Qt::ImAnchorPosition).toInt();
1451 if (anchorPos < cursorPos)
1452 text.chop(cursorPos - anchorPos);
1453 }
1454 }
1455
1456 if (text.length() > length)
1457 text = text.right(length);
1458 return text;
1459}
1460
1461/*
1462 Android docs say that this function should:
1463 - remove the current composing text, if there is any
1464 - otherwise remove currently selected text, if there is any
1465 - insert new text in place of old composing text or, if there was none, at current cursor position
1466 - mark the inserted text as composing
1467 - move cursor as specified by newCursorPosition: if > 0, it is relative to the end of inserted
1468 text - 1; if <= 0, it is relative to the start of inserted text
1469 */
1470
1471jboolean QAndroidInputContext::setComposingText(const QString &text, jint newCursorPosition)
1472{
1473 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1474 if (query.isNull())
1475 return JNI_FALSE;
1476
1477 BatchEditLock batchEditLock(this);
1478
1479 const int absoluteCursorPos = getAbsoluteCursorPosition(query);
1480 int absoluteAnchorPos = getBlockPosition(query) + query->value(Qt::ImAnchorPosition).toInt();
1481
1482 auto setCursorPosition = [=]() {
1483 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1484 QInputMethodEvent event({}, { { QInputMethodEvent::Selection, cursorPos, 0 } });
1485 QGuiApplication::sendEvent(m_focusObject, &event);
1486 };
1487
1488 // If we have composing region and selection (and therefore focusObjectIsComposing() == false),
1489 // we must clear selection so that we won't delete it when we will be replacing composing text
1490 if (!m_composingText.isEmpty() && absoluteCursorPos != absoluteAnchorPos) {
1491 setCursorPosition();
1492 absoluteAnchorPos = absoluteCursorPos;
1493 }
1494
1495 // The value of Qt::ImCursorPosition is not updated at the start
1496 // when the first character is added, so we must update it (QTBUG-85090)
1497 if (absoluteCursorPos == 0 && text.length() == 1 && getTextAfterCursor(1,1).length() >= 0) {
1498 setCursorPosition();
1499 }
1500
1501 // If we had no composing region, pretend that we had a zero-length composing region at current
1502 // cursor position to simplify code. Also account for that we must delete selected text if there
1503 // (still) is any.
1504 const int effectiveAbsoluteCursorPos = qMin(absoluteCursorPos, absoluteAnchorPos);
1505 if (m_composingTextStart == -1)
1506 m_composingTextStart = effectiveAbsoluteCursorPos;
1507
1508 const int oldComposingTextLen = m_composingText.length();
1509 m_composingText = text;
1510
1511 const int newAbsoluteCursorPos =
1512 newCursorPosition <= 0
1513 ? m_composingTextStart + newCursorPosition
1514 : m_composingTextStart + m_composingText.length() + newCursorPosition - 1;
1515
1516 const bool focusObjectWasComposing = focusObjectIsComposing();
1517
1518 // Same checks as in focusObjectStartComposing()
1519 if (!m_composingText.isEmpty() && !m_composingText.contains(u'\n')
1520 && newAbsoluteCursorPos >= m_composingTextStart
1521 && newAbsoluteCursorPos <= m_composingTextStart + m_composingText.length())
1522 m_composingCursor = newAbsoluteCursorPos;
1523 else
1524 m_composingCursor = -1;
1525
1526 if (focusObjectIsComposing()) {
1527 QTextCharFormat underlined;
1528 underlined.setFontUnderline(true);
1529
1530 QInputMethodEvent event(m_composingText, {
1531 { QInputMethodEvent::TextFormat, 0, int(m_composingText.length()), underlined },
1532 { QInputMethodEvent::Cursor, m_composingCursor - m_composingTextStart, 1 }
1533 });
1534
1535 if (oldComposingTextLen > 0 && !focusObjectWasComposing) {
1536 event.setCommitString({}, m_composingTextStart - effectiveAbsoluteCursorPos,
1537 oldComposingTextLen);
1538 }
1539 if (m_composingText.isEmpty())
1540 clear();
1541
1542 QGuiApplication::sendEvent(m_focusObject, &event);
1543 } else {
1544 QInputMethodEvent event({}, {});
1545
1546 if (focusObjectWasComposing) {
1547 event.setCommitString(m_composingText);
1548 } else {
1549 event.setCommitString(m_composingText,
1550 m_composingTextStart - effectiveAbsoluteCursorPos,
1551 oldComposingTextLen);
1552 }
1553 if (m_composingText.isEmpty())
1554 clear();
1555
1556 QGuiApplication::sendEvent(m_focusObject, &event);
1557 }
1558
1559 if (!focusObjectIsComposing() && newCursorPosition != 1) {
1560 // Move cursor using a separate event because if we have inserted or deleted a newline
1561 // character, then we are now inside an another block
1562
1563 const int newBlockPos = getBlockPosition(
1564 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAbsolutePosition));
1565
1566 QInputMethodEvent event({}, {
1567 { QInputMethodEvent::Selection, newAbsoluteCursorPos - newBlockPos, 0 }
1568 });
1569
1570 QGuiApplication::sendEvent(m_focusObject, &event);
1571 }
1572
1573 keyDown();
1574
1575 return JNI_TRUE;
1576}
1577
1578// Android docs say:
1579// * start may be after end, same meaning as if swapped
1580// * this function should not trigger updateSelection, but Android's native EditText does trigger it
1581// * if start == end then we should stop composing
1583{
1584 BatchEditLock batchEditLock(this);
1585
1586 // Qt will not include the current preedit text in the query results, and interprets all
1587 // parameters relative to the text excluding the preedit. The simplest solution is therefore to
1588 // tell Qt that we commit the text before we set the new region. This may cause a little flicker, but is
1589 // much more robust than trying to keep the two different world views in sync
1590
1591 finishComposingText();
1592
1593 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1594 if (query.isNull())
1595 return JNI_FALSE;
1596
1597 if (start == end)
1598 return JNI_TRUE;
1599 if (start > end)
1600 qSwap(start, end);
1601
1602 QString text = query->value(Qt::ImSurroundingText).toString();
1603 int textOffset = getBlockPosition(query);
1604
1605 if (start < textOffset || end > textOffset + text.length()) {
1606 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1607
1608 if (end - textOffset > text.length()) {
1609 const QString after = query->value(Qt::ImTextAfterCursor).toString();
1610 const int additionalSuffixLen = after.length() - (text.length() - cursorPos);
1611
1612 if (additionalSuffixLen > 0)
1613 text += QStringView{after}.right(additionalSuffixLen);
1614 }
1615
1616 if (start < textOffset) {
1617 QString before = query->value(Qt::ImTextBeforeCursor).toString();
1618 before.chop(cursorPos);
1619
1620 if (!before.isEmpty()) {
1621 text = before + text;
1622 textOffset -= before.length();
1623 }
1624 }
1625
1626 if (start < textOffset || end - textOffset > text.length()) {
1627 qCDebug(lcQpaInputMethods) << "Warning: setComposingRegion: failed to retrieve text from composing region";
1628
1629 return JNI_TRUE;
1630 }
1631 }
1632
1633 m_composingText = text.mid(start - textOffset, end - start);
1634 m_composingTextStart = start;
1635
1636 return JNI_TRUE;
1637}
1638
1640{
1641 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1642 if (query.isNull())
1643 return JNI_FALSE;
1644
1645 BatchEditLock batchEditLock(this);
1646
1647 int blockPosition = getBlockPosition(query);
1648 int localCursorPos = start - blockPosition;
1649
1650 if (focusObjectIsComposing() && start == end && start >= m_composingTextStart
1651 && start <= m_composingTextStart + m_composingText.length()) {
1652 // not actually changing the selection; just moving the
1653 // preedit cursor
1654 int localOldPos = query->value(Qt::ImCursorPosition).toInt();
1655 int pos = localCursorPos - localOldPos;
1656 QList<QInputMethodEvent::Attribute> attributes;
1657 attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, pos, 1));
1658
1659 //but we have to tell Qt about the compose text all over again
1660
1661 // Show compose text underlined
1662 QTextCharFormat underlined;
1663 underlined.setFontUnderline(true);
1664 attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat,0, m_composingText.length(),
1665 QVariant(underlined)));
1666 m_composingCursor = start;
1667
1668 QInputMethodEvent event(m_composingText, attributes);
1669 QGuiApplication::sendEvent(m_focusObject, &event);
1670 } else {
1671 // actually changing the selection
1672 focusObjectStopComposing();
1673 QList<QInputMethodEvent::Attribute> attributes;
1674 attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection,
1675 localCursorPos,
1676 end - start));
1677 QInputMethodEvent event({}, attributes);
1678 QGuiApplication::sendEvent(m_focusObject, &event);
1679 }
1680 return JNI_TRUE;
1681}
1682
1684{
1685 BatchEditLock batchEditLock(this);
1686
1687 focusObjectStopComposing();
1688 m_handleMode = ShowCursor;
1689 sendShortcut(QKeySequence::SelectAll);
1690 return JNI_TRUE;
1691}
1692
1694{
1695 BatchEditLock batchEditLock(this);
1696
1697 // This is probably not what native EditText would do, but normally if there is selection, then
1698 // there will be no composing region
1699 finishComposingText();
1700
1701 m_handleMode = ShowCursor;
1702 sendShortcut(QKeySequence::Cut);
1703 return JNI_TRUE;
1704}
1705
1707{
1708 BatchEditLock batchEditLock(this);
1709
1710 focusObjectStopComposing();
1711 m_handleMode = ShowCursor;
1712 sendShortcut(QKeySequence::Copy);
1713 return JNI_TRUE;
1714}
1715
1717{
1718#warning TODO
1719 return JNI_FALSE;
1720}
1721
1723{
1724 BatchEditLock batchEditLock(this);
1725
1726 // TODO: This is not what native EditText does
1727 finishComposingText();
1728
1729 m_handleMode = ShowCursor;
1730 sendShortcut(QKeySequence::Paste);
1731 return JNI_TRUE;
1732}
1733
1734void QAndroidInputContext::sendShortcut(const QKeySequence &sequence)
1735{
1736 for (int i = 0; i < sequence.count(); ++i) {
1737 const QKeyCombination keys = sequence[i];
1738 Qt::Key key = Qt::Key(keys.toCombined() & ~Qt::KeyboardModifierMask);
1739 Qt::KeyboardModifiers mod = Qt::KeyboardModifiers(keys.toCombined() & Qt::KeyboardModifierMask);
1740
1741 QKeyEvent pressEvent(QEvent::KeyPress, key, mod);
1742 QKeyEvent releaseEvent(QEvent::KeyRelease, key, mod);
1743
1744 QGuiApplication::sendEvent(m_focusObject, &pressEvent);
1745 QGuiApplication::sendEvent(m_focusObject, &releaseEvent);
1746 }
1747}
1748
1749QSharedPointer<QInputMethodQueryEvent> QAndroidInputContext::focusObjectInputMethodQuery(Qt::InputMethodQueries queries) {
1750 if (!qGuiApp)
1751 return {};
1752
1753 QObject *focusObject = qGuiApp->focusObject();
1754 if (!focusObject)
1755 return {};
1756
1757 QInputMethodQueryEvent *ret = new QInputMethodQueryEvent(queries);
1758 QCoreApplication::sendEvent(focusObject, ret);
1759 return QSharedPointer<QInputMethodQueryEvent>(ret);
1760}
1761
1762void QAndroidInputContext::sendInputMethodEvent(QInputMethodEvent *event)
1763{
1764 if (!qGuiApp)
1765 return;
1766
1767 QObject *focusObject = qGuiApp->focusObject();
1768 if (!focusObject)
1769 return;
1770
1771 QCoreApplication::sendEvent(focusObject, event);
1772}
1773
1774QT_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: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