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