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