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