Qt 6.x
The Qt SDK
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
9#include "androidjniinput.h"
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
30namespace {
31
33{
34public:
35
37 : m_context(context)
38 {
39 m_context->beginBatchEdit();
40 }
41
43 {
44 m_context->endBatchEdit();
45 }
46
47 BatchEditLock(const BatchEditLock &) = delete;
49
50private:
51
52 QAndroidInputContext *m_context;
53};
54
55} // namespace anonymous
56
58static char const *const QtNativeInputConnectionClassName = "org/qtproject/qt/android/QtNativeInputConnection";
59static char const *const QtExtractedTextClassName = "org/qtproject/qt/android/QtExtractedText";
60static int m_selectHandleWidth = 0;
61static jclass m_extractedTextClass = 0;
62static jmethodID m_classConstructorMethodID = 0;
63static jfieldID m_partialEndOffsetFieldID = 0;
64static jfieldID m_partialStartOffsetFieldID = 0;
65static jfieldID m_selectionEndFieldID = 0;
66static jfieldID m_selectionStartFieldID = 0;
67static jfieldID m_startOffsetFieldID = 0;
68static jfieldID m_textFieldID = 0;
69
70static void runOnQtThread(const std::function<void()> &func)
71{
73 if (!protector.acquire())
74 return;
76}
77
78static jboolean beginBatchEdit(JNIEnv */*env*/, jobject /*thiz*/)
79{
81 return JNI_FALSE;
82
83 qCDebug(lcQpaInputMethods) << "@@@ BEGINBATCH";
84 jboolean res = JNI_FALSE;
86 return res;
87}
88
89static jboolean endBatchEdit(JNIEnv */*env*/, jobject /*thiz*/)
90{
92 return JNI_FALSE;
93
94 qCDebug(lcQpaInputMethods) << "@@@ ENDBATCH";
95
96 jboolean res = JNI_FALSE;
98 return res;
99}
100
101
102static jboolean commitText(JNIEnv *env, jobject /*thiz*/, jstring text, jint newCursorPosition)
103{
105 return JNI_FALSE;
106
107 jboolean isCopy;
108 const jchar *jstr = env->GetStringChars(text, &isCopy);
109 QString str(reinterpret_cast<const QChar *>(jstr), env->GetStringLength(text));
110 env->ReleaseStringChars(text, jstr);
111
112 qCDebug(lcQpaInputMethods) << "@@@ COMMIT" << str << newCursorPosition;
113 jboolean res = JNI_FALSE;
114 runOnQtThread([&]{res = m_androidInputContext->commitText(str, newCursorPosition);});
115 return res;
116}
117
118static jboolean deleteSurroundingText(JNIEnv */*env*/, jobject /*thiz*/, jint leftLength, jint rightLength)
119{
121 return JNI_FALSE;
122
123 qCDebug(lcQpaInputMethods) << "@@@ DELETE" << leftLength << rightLength;
124 jboolean res = JNI_FALSE;
125 runOnQtThread([&]{res = m_androidInputContext->deleteSurroundingText(leftLength, rightLength);});
126 return res;
127}
128
129static jboolean finishComposingText(JNIEnv */*env*/, jobject /*thiz*/)
130{
132 return JNI_FALSE;
133
134 qCDebug(lcQpaInputMethods) << "@@@ FINISH";
135 jboolean res = JNI_FALSE;
137 return res;
138}
139
140static jint getCursorCapsMode(JNIEnv */*env*/, jobject /*thiz*/, jint reqModes)
141{
143 return 0;
144
145 jint res = 0;
147 return res;
148}
149
150static jobject getExtractedText(JNIEnv *env, jobject /*thiz*/, int hintMaxChars, int hintMaxLines, jint flags)
151{
153 return 0;
154
156 runOnQtThread([&]{extractedText = m_androidInputContext->getExtractedText(hintMaxChars, hintMaxLines, flags);});
157
158 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;
159
160 jobject object = env->NewObject(m_extractedTextClass, m_classConstructorMethodID);
161 env->SetIntField(object, m_partialStartOffsetFieldID, extractedText.partialStartOffset);
162 env->SetIntField(object, m_partialEndOffsetFieldID, extractedText.partialEndOffset);
163 env->SetIntField(object, m_selectionStartFieldID, extractedText.selectionStart);
164 env->SetIntField(object, m_selectionEndFieldID, extractedText.selectionEnd);
165 env->SetIntField(object, m_startOffsetFieldID, extractedText.startOffset);
166 env->SetObjectField(object,
168 env->NewString(reinterpret_cast<const jchar *>(extractedText.text.constData()),
169 jsize(extractedText.text.length())));
170
171 return object;
172}
173
174static jstring getSelectedText(JNIEnv *env, jobject /*thiz*/, jint flags)
175{
177 return 0;
178
181 qCDebug(lcQpaInputMethods) << "@@@ GETSEL" << text;
182 if (text.isEmpty())
183 return 0;
184 return env->NewString(reinterpret_cast<const jchar *>(text.constData()), jsize(text.length()));
185}
186
187static jstring getTextAfterCursor(JNIEnv *env, jobject /*thiz*/, jint length, jint flags)
188{
190 return 0;
191
194 qCDebug(lcQpaInputMethods) << "@@@ GETA" << length << text;
195 return env->NewString(reinterpret_cast<const jchar *>(text.constData()), jsize(text.length()));
196}
197
198static jstring getTextBeforeCursor(JNIEnv *env, jobject /*thiz*/, jint length, jint flags)
199{
201 return 0;
202
205 qCDebug(lcQpaInputMethods) << "@@@ GETB" << length << text;
206 return env->NewString(reinterpret_cast<const jchar *>(text.constData()), jsize(text.length()));
207}
208
209static jboolean setComposingText(JNIEnv *env, jobject /*thiz*/, jstring text, jint newCursorPosition)
210{
212 return JNI_FALSE;
213
214 jboolean isCopy;
215 const jchar *jstr = env->GetStringChars(text, &isCopy);
216 QString str(reinterpret_cast<const QChar *>(jstr), env->GetStringLength(text));
217 env->ReleaseStringChars(text, jstr);
218
219 qCDebug(lcQpaInputMethods) << "@@@ SET" << str << newCursorPosition;
220 jboolean res = JNI_FALSE;
221 runOnQtThread([&]{res = m_androidInputContext->setComposingText(str, newCursorPosition);});
222 return res;
223}
224
225static jboolean setComposingRegion(JNIEnv */*env*/, jobject /*thiz*/, jint start, jint end)
226{
228 return JNI_FALSE;
229
230 qCDebug(lcQpaInputMethods) << "@@@ SETR" << start << end;
231 jboolean res = JNI_FALSE;
233 return res;
234}
235
236
237static jboolean setSelection(JNIEnv */*env*/, jobject /*thiz*/, jint start, jint end)
238{
240 return JNI_FALSE;
241
242 qCDebug(lcQpaInputMethods) << "@@@ SETSEL" << start << end;
243 jboolean res = JNI_FALSE;
245 return res;
246
247}
248
249static jboolean selectAll(JNIEnv */*env*/, jobject /*thiz*/)
250{
252 return JNI_FALSE;
253
254 qCDebug(lcQpaInputMethods) << "@@@ SELALL";
255 jboolean res = JNI_FALSE;
257 return res;
258}
259
260static jboolean cut(JNIEnv */*env*/, jobject /*thiz*/)
261{
263 return JNI_FALSE;
264
265 qCDebug(lcQpaInputMethods) << "@@@";
266 jboolean res = JNI_FALSE;
268 return res;
269}
270
271static jboolean copy(JNIEnv */*env*/, jobject /*thiz*/)
272{
274 return JNI_FALSE;
275
276 qCDebug(lcQpaInputMethods) << "@@@";
277 jboolean res = JNI_FALSE;
279 return res;
280}
281
282static jboolean copyURL(JNIEnv */*env*/, jobject /*thiz*/)
283{
285 return JNI_FALSE;
286
287 qCDebug(lcQpaInputMethods) << "@@@";
288 jboolean res = JNI_FALSE;
290 return res;
291}
292
293static jboolean paste(JNIEnv */*env*/, jobject /*thiz*/)
294{
296 return JNI_FALSE;
297
298 qCDebug(lcQpaInputMethods) << "@@@ PASTE";
299 jboolean res = JNI_FALSE;
301 return res;
302}
303
304static jboolean updateCursorPosition(JNIEnv */*env*/, jobject /*thiz*/)
305{
307 return JNI_FALSE;
308
309 qCDebug(lcQpaInputMethods) << "@@@ UPDATECURSORPOS";
310
312 return true;
313}
314
315
316static JNINativeMethod methods[] = {
317 {"beginBatchEdit", "()Z", (void *)beginBatchEdit},
318 {"endBatchEdit", "()Z", (void *)endBatchEdit},
319 {"commitText", "(Ljava/lang/String;I)Z", (void *)commitText},
320 {"deleteSurroundingText", "(II)Z", (void *)deleteSurroundingText},
321 {"finishComposingText", "()Z", (void *)finishComposingText},
322 {"getCursorCapsMode", "(I)I", (void *)getCursorCapsMode},
323 {"getExtractedText", "(III)Lorg/qtproject/qt/android/QtExtractedText;", (void *)getExtractedText},
324 {"getSelectedText", "(I)Ljava/lang/String;", (void *)getSelectedText},
325 {"getTextAfterCursor", "(II)Ljava/lang/String;", (void *)getTextAfterCursor},
326 {"getTextBeforeCursor", "(II)Ljava/lang/String;", (void *)getTextBeforeCursor},
327 {"setComposingText", "(Ljava/lang/String;I)Z", (void *)setComposingText},
328 {"setComposingRegion", "(II)Z", (void *)setComposingRegion},
329 {"setSelection", "(II)Z", (void *)setSelection},
330 {"selectAll", "()Z", (void *)selectAll},
331 {"cut", "()Z", (void *)cut},
332 {"copy", "()Z", (void *)copy},
333 {"copyURL", "()Z", (void *)copyURL},
334 {"paste", "()Z", (void *)paste},
335 {"updateCursorPosition", "()Z", (void *)updateCursorPosition}
336};
337
339{
341 QPlatformWindow *window = qGuiApp->focusWindow()->handle();
342 return QRect(window->mapToGlobal(windowRect.topLeft()), windowRect.size());
343}
344
347 , m_composingTextStart(-1)
348 , m_composingCursor(-1)
349 , m_handleMode(Hidden)
350 , m_batchEditNestingLevel(0)
351 , m_focusObject(0)
352{
353 QJniEnvironment env;
354 jclass clazz = env.findClass(QtNativeInputConnectionClassName);
355 if (Q_UNLIKELY(!clazz)) {
356 qCritical() << "Native registration unable to find class '"
358 << '\'';
359 return;
360 }
361
362 if (Q_UNLIKELY(env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0)) {
363 qCritical() << "RegisterNatives failed for '"
365 << '\'';
366 return;
367 }
368
369 clazz = env.findClass(QtExtractedTextClassName);
370 if (Q_UNLIKELY(!clazz)) {
371 qCritical() << "Native registration unable to find class '"
373 << '\'';
374 return;
375 }
376
377 m_extractedTextClass = static_cast<jclass>(env->NewGlobalRef(clazz));
378 m_classConstructorMethodID = env->GetMethodID(m_extractedTextClass, "<init>", "()V");
380 qCritical("GetMethodID failed");
381 return;
382 }
383
384 m_partialEndOffsetFieldID = env->GetFieldID(m_extractedTextClass, "partialEndOffset", "I");
386 qCritical("Can't find field partialEndOffset");
387 return;
388 }
389
390 m_partialStartOffsetFieldID = env->GetFieldID(m_extractedTextClass, "partialStartOffset", "I");
392 qCritical("Can't find field partialStartOffset");
393 return;
394 }
395
396 m_selectionEndFieldID = env->GetFieldID(m_extractedTextClass, "selectionEnd", "I");
398 qCritical("Can't find field selectionEnd");
399 return;
400 }
401
402 m_selectionStartFieldID = env->GetFieldID(m_extractedTextClass, "selectionStart", "I");
404 qCritical("Can't find field selectionStart");
405 return;
406 }
407
408 m_startOffsetFieldID = env->GetFieldID(m_extractedTextClass, "startOffset", "I");
410 qCritical("Can't find field startOffset");
411 return;
412 }
413
414 m_textFieldID = env->GetFieldID(m_extractedTextClass, "text", "Ljava/lang/String;");
416 qCritical("Can't find field text");
417 return;
418 }
419 qRegisterMetaType<QInputMethodEvent *>("QInputMethodEvent*");
420 qRegisterMetaType<QInputMethodQueryEvent *>("QInputMethodQueryEvent*");
422
428 auto im = qGuiApp->inputMethod();
429 if (!im->inputItemClipRectangle().contains(im->anchorRectangle()) ||
430 !im->inputItemClipRectangle().contains(im->cursorRectangle())) {
431 m_handleMode = Hidden;
432 updateSelectionHandles();
433 }
434 });
435 m_hideCursorHandleTimer.setInterval(4000);
436 m_hideCursorHandleTimer.setSingleShot(true);
437 m_hideCursorHandleTimer.setTimerType(Qt::VeryCoarseTimer);
438 connect(&m_hideCursorHandleTimer, &QTimer::timeout, this, [this]{
439 m_handleMode = Hidden;
441 });
442}
443
445{
453 m_textFieldID = 0;
454}
455
457{
459}
460
461// cursor position getter that also works with editors that have not been updated to the new API
463{
465 return absolutePos.isValid() ? absolutePos.toInt() : query->value(Qt::ImCursorPosition).toInt();
466}
467
468// position of the start of the current block
470{
472 return absolutePos.isValid() ? absolutePos.toInt() - query->value(Qt::ImCursorPosition).toInt() : 0;
473}
474
476{
477 focusObjectStopComposing();
478 clear();
479 m_batchEditNestingLevel = 0;
480 m_handleMode = Hidden;
481 if (qGuiApp->focusObject()) {
482 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(Qt::ImEnabled);
483 if (!query.isNull() && query->value(Qt::ImEnabled).toBool()) {
485 return;
486 }
487 }
489}
490
492{
493 focusObjectStopComposing();
494}
495
497{
498 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
499 if (!query.isNull() && m_batchEditNestingLevel == 0) {
500 const int cursorPos = getAbsoluteCursorPosition(query);
501 const int composeLength = m_composingText.length();
502
503 //Q_ASSERT(m_composingText.isEmpty() == (m_composingTextStart == -1));
504 if (m_composingText.isEmpty() != (m_composingTextStart == -1))
505 qWarning() << "Input method out of sync" << m_composingText << m_composingTextStart;
506
507 int realSelectionStart = cursorPos;
508 int realSelectionEnd = cursorPos;
509
510 int cpos = query->value(Qt::ImCursorPosition).toInt();
511 int anchor = query->value(Qt::ImAnchorPosition).toInt();
512 if (cpos != anchor) {
513 if (!m_composingText.isEmpty()) {
514 qWarning("Selecting text while preediting may give unpredictable results.");
515 focusObjectStopComposing();
516 }
517 int blockPos = getBlockPosition(query);
518 realSelectionStart = blockPos + cpos;
519 realSelectionEnd = blockPos + anchor;
520 }
521 // Qt's idea of the cursor position is the start of the preedit area, so we maintain our own preedit cursor pos
522 if (focusObjectIsComposing())
523 realSelectionStart = realSelectionEnd = m_composingCursor;
524
525 // Some keyboards misbahave when selStart > selEnd
526 if (realSelectionStart > realSelectionEnd)
527 std::swap(realSelectionStart, realSelectionEnd);
528
529 QtAndroidInput::updateSelection(realSelectionStart, realSelectionEnd,
530 m_composingTextStart, m_composingTextStart + composeLength); // pre-edit text
531 }
532}
533
535{
536 static bool noHandles = qEnvironmentVariableIntValue("QT_QPA_NO_TEXT_HANDLES");
537 if (noHandles || !m_focusObject)
538 return;
539
540 auto im = qGuiApp->inputMethod();
541
545 QCoreApplication::sendEvent(m_focusObject, &query);
546
547 int cpos = query.value(Qt::ImCursorPosition).toInt();
548 int anchor = query.value(Qt::ImAnchorPosition).toInt();
549 const QVariant readOnlyVariant = query.value(Qt::ImReadOnly);
550 bool readOnly = readOnlyVariant.toBool();
551 QPlatformWindow *qPlatformWindow = qGuiApp->focusWindow()->handle();
552
553 if ( cpos == anchor && (!readOnlyVariant.isValid() || readOnly)) {
555 return;
556 }
557
558 if (cpos == anchor || im->anchorRectangle().isNull()) {
559 auto curRect = cursorRectangle();
560 QPoint cursorPointGlobal = qPlatformWindow->mapToGlobal(
561 QPoint(curRect.x() + (curRect.width() / 2), curRect.y() + curRect.height()));
562 QPoint cursorPoint(curRect.center().x(), curRect.bottom());
563 int x = curRect.x();
564 int y = curRect.y();
565
566 // Use x and y for the editMenuPoint from the cursorPointGlobal when the cursor is in the Dialog
567 if (cursorPointGlobal != cursorPoint) {
568 x = cursorPointGlobal.x();
569 y = cursorPointGlobal.y();
570 }
571
572 QPoint editMenuPoint(x, y);
573 m_handleMode &= ShowEditPopup;
574 m_handleMode |= ShowCursor;
575 uint32_t buttons = readOnly ? 0 : EditContext::PasteButton;
576 if (!query.value(Qt::ImSurroundingText).toString().isEmpty())
578 QtAndroidInput::updateHandles(m_handleMode, editMenuPoint, buttons, cursorPointGlobal);
579 // The VK is hidden, reset the timer
580 if (m_hideCursorHandleTimer.isActive())
581 m_hideCursorHandleTimer.start();
582 return;
583 }
584
585 m_handleMode = ShowSelection | ShowEditPopup ;
586 auto leftRect = cursorRectangle();
587 auto rightRect = anchorRectangle();
588 if (cpos > anchor)
589 std::swap(leftRect, rightRect);
590 //Move the left or right select handle to the center from the screen edge
591 //the select handle is close to or over the screen edge. Otherwise, the
592 //select handle might go out of the screen and it would be impossible to drag.
593 QPoint leftPoint(qPlatformWindow->mapToGlobal(leftRect.bottomLeft().toPoint()));
594 QPoint rightPoint(qPlatformWindow->mapToGlobal(rightRect.bottomRight().toPoint()));
595
597 if (platformIntegration) {
598 if (m_selectHandleWidth == 0)
600
601 int rightSideOfScreen = platformIntegration->screen()->availableGeometry().right();
602 if (leftPoint.x() < m_selectHandleWidth)
603 leftPoint.setX(m_selectHandleWidth);
604
605 if (rightPoint.x() > rightSideOfScreen - m_selectHandleWidth)
606 rightPoint.setX(rightSideOfScreen - m_selectHandleWidth);
607
608 QPoint editPoint(qPlatformWindow->mapToGlobal(leftRect.united(rightRect).topLeft().toPoint()));
609 uint32_t buttons = readOnly ? EditContext::CopyButton | EditContext::SelectAllButton
611
612 QtAndroidInput::updateHandles(m_handleMode, editPoint, buttons, leftPoint, rightPoint,
613 query.value(Qt::ImCurrentSelection).toString().isRightToLeft());
614 m_hideCursorHandleTimer.stop();
615 }
616}
617
618/*
619 Called from Java when a cursor/selection handle was dragged to a new position
620
621 handleId of 1 means the cursor handle, 2 means the left handle, 3 means the right handle
622 */
624{
625 if (m_batchEditNestingLevel != 0) {
626 qWarning() << "QAndroidInputContext::handleLocationChanged returned";
627 return;
628 }
629 QPoint point(x, y);
630
631 // The handle is down of the cursor, but we want the position in the middle.
634 QCoreApplication::sendEvent(m_focusObject, &query);
635 int cpos = query.value(Qt::ImCursorPosition).toInt();
636 int anchor = query.value(Qt::ImAnchorPosition).toInt();
637 auto leftRect = cursorRectangle();
638 auto rightRect = anchorRectangle();
639 if (cpos > anchor)
640 std::swap(leftRect, rightRect);
641
642 // Do not allow dragging left handle below right handle, or right handle above left handle
643 if (handleId == 2 && point.y() > rightRect.center().y()) {
644 point.setY(rightRect.center().y());
645 } else if (handleId == 3 && point.y() < leftRect.center().y()) {
646 point.setY(leftRect.center().y());
647 }
648
649 bool ok;
650 auto object = m_focusObject->parent();
651 int dialogMoveX = 0;
652 while (object) {
653 if (QString::compare(object->metaObject()->className(),
654 "QDialog", Qt::CaseInsensitive) == 0) {
655 dialogMoveX += object->property("x").toInt();
656 }
657 object = object->parent();
658 };
659
660 auto position =
662 const QPointF fixedPosition = QPointF(position.x() - dialogMoveX, position.y());
664 const QTransform mapToLocal = im->inputItemTransform().inverted();
665 const int handlePos = im->queryFocusObject(Qt::ImCursorPosition, mapToLocal.map(fixedPosition)).toInt(&ok);
666
667 if (!ok)
668 return;
669
670 int newCpos = cpos;
671 int newAnchor = anchor;
672 if (newAnchor > newCpos)
673 std::swap(newAnchor, newCpos);
674
675 if (handleId == 1) {
676 newCpos = handlePos;
677 newAnchor = handlePos;
678 } else if (handleId == 2) {
679 newAnchor = handlePos;
680 } else if (handleId == 3) {
681 newCpos = handlePos;
682 }
683
684 /*
685 Do not allow clearing selection by dragging selection handles and do not allow swapping
686 selection handles for consistency with Android's native text editing controls. Ensure that at
687 least one symbol remains selected.
688 */
689 if ((handleId == 2 || handleId == 3) && newCpos <= newAnchor) {
691 query.value(Qt::ImCurrentSelection).toString());
692
693 const int oldSelectionStartPos = qMin(cpos, anchor);
694
695 if (handleId == 2) {
696 finder.toEnd();
697 finder.toPreviousBoundary();
698 newAnchor = finder.position() + oldSelectionStartPos;
699 } else {
700 finder.toStart();
701 finder.toNextBoundary();
702 newCpos = finder.position() + oldSelectionStartPos;
703 }
704 }
705
706 // Check if handle has been dragged far enough
707 if (!focusObjectIsComposing() && newCpos == cpos && newAnchor == anchor)
708 return;
709
710 /*
711 If the editor is currently in composing state, we have to compare newCpos with
712 m_composingCursor instead of cpos. And since there is nothing to compare with newAnchor, we
713 perform the check only when user drags the cursor handle.
714 */
715 if (focusObjectIsComposing() && handleId == 1) {
716 int absoluteCpos = query.value(Qt::ImAbsolutePosition).toInt(&ok);
717 if (!ok)
718 absoluteCpos = cpos;
719 const int blockPos = absoluteCpos - cpos;
720
721 if (blockPos + newCpos == m_composingCursor)
722 return;
723 }
724
725 BatchEditLock batchEditLock(this);
726
727 focusObjectStopComposing();
728
730 attributes.append({ QInputMethodEvent::Selection, newAnchor, newCpos - newAnchor });
731 if (newCpos != newAnchor)
732 attributes.append({ QInputMethodEvent::Cursor, 0, 0 });
733
734 QInputMethodEvent event(QString(), attributes);
735 QGuiApplication::sendEvent(m_focusObject, &event);
736}
737
739{
740 if (m_focusObject && screenInputItemRectangle().contains(x, y)) {
741 // If the user touch the input rectangle, we can show the cursor handle
742 m_handleMode = ShowCursor;
743 // The VK will appear in a moment, stop the timer
744 m_hideCursorHandleTimer.stop();
745
746 if (focusObjectIsComposing()) {
747 const int curBlockPos = getBlockPosition(
748 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAbsolutePosition));
749 const int touchPosition = curBlockPos
751 if (touchPosition != m_composingCursor)
752 focusObjectStopComposing();
753 }
754
756 }
757}
758
760{
761 static bool noHandles = qEnvironmentVariableIntValue("QT_QPA_NO_TEXT_HANDLES");
762 if (noHandles)
763 return;
764
765 if (m_focusObject && screenInputItemRectangle().contains(x, y)) {
766 BatchEditLock batchEditLock(this);
767
768 focusObjectStopComposing();
769 const QPointF touchPoint(x, y);
770 setSelectionOnFocusObject(touchPoint, touchPoint);
771
773 QCoreApplication::sendEvent(m_focusObject, &query);
774 int cursor = query.value(Qt::ImCursorPosition).toInt();
775 int anchor = cursor;
776 QString before = query.value(Qt::ImTextBeforeCursor).toString();
777 QString after = query.value(Qt::ImTextAfterCursor).toString();
778 for (const auto &ch : after) {
779 if (!ch.isLetterOrNumber())
780 break;
781 ++anchor;
782 }
783
784 for (auto itch = before.rbegin(); itch != after.rend(); ++itch) {
785 if (!itch->isLetterOrNumber())
786 break;
787 --cursor;
788 }
789 if (cursor == anchor || cursor < 0 || cursor - anchor > 500) {
790 m_handleMode = ShowCursor | ShowEditPopup;
792 return;
793 }
797 QInputMethodEvent event(QString(), imAttributes);
798 QGuiApplication::sendEvent(m_focusObject, &event);
799
800 m_handleMode = ShowSelection | ShowEditPopup;
802 }
803}
804
806{
807 if (m_handleMode) {
808 // When the user enter text on the keyboard, we hide the cursor handle
809 m_handleMode = Hidden;
811 }
812}
813
815{
816 if (m_handleMode & ShowSelection) {
817 m_handleMode = Hidden;
819 } else {
820 m_hideCursorHandleTimer.start();
821 }
822}
823
824void QAndroidInputContext::update(Qt::InputMethodQueries queries)
825{
826 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(queries);
827 if (query.isNull())
828 return;
829#warning TODO extract the needed data from query
830}
831
833{
834#warning TODO Handle at least QInputMethod::ContextMenu action
835 Q_UNUSED(action);
836 Q_UNUSED(cursorPosition);
837 //### click should be passed to the IM, but in the meantime it's better to ignore it than to do something wrong
838 // if (action == QInputMethod::Click)
839 // commit();
840}
841
843{
845}
846
848{
849 return false;
850}
851
853{
855 connect(qGuiApp, SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(showInputPanelLater(Qt::ApplicationState)));
856 return;
857 }
858 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
859 if (query.isNull())
860 return;
861
862 disconnect(m_updateCursorPosConnection);
863 m_updateCursorPosConnection = {};
864
865 if (qGuiApp->focusObject()->metaObject()->indexOfSignal("cursorPositionChanged(int,int)") >= 0) // QLineEdit breaks the pattern
866 m_updateCursorPosConnection = connect(qGuiApp->focusObject(), SIGNAL(cursorPositionChanged(int,int)), this, SLOT(updateCursorPosition()));
867 else if (qGuiApp->focusObject()->metaObject()->indexOfSignal("cursorPositionChanged()") >= 0)
868 m_updateCursorPosConnection = connect(qGuiApp->focusObject(), SIGNAL(cursorPositionChanged()), this, SLOT(updateCursorPosition()));
869
871 QtAndroidInput::showSoftwareKeyboard(rect.left(), rect.top(), rect.width(), rect.height(),
872 query->value(Qt::ImHints).toUInt(),
873 query->value(Qt::ImEnterKeyType).toUInt());
874}
875
876void QAndroidInputContext::showInputPanelLater(Qt::ApplicationState state)
877{
879 return;
880 disconnect(qGuiApp, SIGNAL(applicationStateChanged(Qt::ApplicationState)), this, SLOT(showInputPanelLater(Qt::ApplicationState)));
882}
883
884void QAndroidInputContext::safeCall(const std::function<void()> &func, Qt::ConnectionType conType)
885{
886 if (qGuiApp->thread() == QThread::currentThread())
887 func();
888 else
889 QMetaObject::invokeMethod(this, "safeCall", conType, Q_ARG(std::function<void()>, func));
890}
891
893{
895}
896
898{
900}
901
903{
904 return m_composingText.length();
905}
906
908{
909 m_composingText.clear();
910 m_composingTextStart = -1;
911 m_composingCursor = -1;
912 m_extractedText.clear();
913}
914
915
917{
918 if (object != m_focusObject) {
919 focusObjectStopComposing();
920 m_focusObject = object;
921 reset();
922 }
924}
925
927{
928 ++m_batchEditNestingLevel;
929 return JNI_TRUE;
930}
931
933{
934 if (--m_batchEditNestingLevel == 0) { //ending batch edit mode
935 focusObjectStartComposing();
937 }
938 return JNI_TRUE;
939}
940
941/*
942 Android docs say: This behaves like calling setComposingText(text, newCursorPosition) then
943 finishComposingText().
944*/
945jboolean QAndroidInputContext::commitText(const QString &text, jint newCursorPosition)
946{
947 BatchEditLock batchEditLock(this);
948 return setComposingText(text, newCursorPosition) && finishComposingText();
949}
950
951jboolean QAndroidInputContext::deleteSurroundingText(jint leftLength, jint rightLength)
952{
953 BatchEditLock batchEditLock(this);
954
955 focusObjectStopComposing();
956
957 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
958 if (query.isNull())
959 return JNI_TRUE;
960
961 if (leftLength < 0) {
962 rightLength += -leftLength;
963 leftLength = 0;
964 }
965
966 const int initialBlockPos = getBlockPosition(query);
967 const int initialCursorPos = getAbsoluteCursorPosition(query);
968 const int initialAnchorPos = initialBlockPos + query->value(Qt::ImAnchorPosition).toInt();
969
970 /*
971 According to documentation, we should delete leftLength characters before current selection
972 and rightLength characters after current selection (without affecting selection). But that is
973 absolutely not what Android's native EditText does. It deletes leftLength characters before
974 min(selection start, composing region start) and rightLength characters after max(selection
975 end, composing region end). There are no known keyboards that depend on this behavior, but
976 it is better to be consistent with EditText behavior, because there definitely should be no
977 keyboards that depend on documented behavior.
978 */
979 const int leftEnd =
980 m_composingText.isEmpty()
981 ? qMin(initialCursorPos, initialAnchorPos)
982 : qMin(qMin(initialCursorPos, initialAnchorPos), m_composingTextStart);
983
984 const int rightBegin =
985 m_composingText.isEmpty()
986 ? qMax(initialCursorPos, initialAnchorPos)
987 : qMax(qMax(initialCursorPos, initialAnchorPos),
988 m_composingTextStart + m_composingText.length());
989
990 int textBeforeCursorLen;
991 int textAfterCursorLen;
992
993 QVariant textBeforeCursor = query->value(Qt::ImTextBeforeCursor);
994 QVariant textAfterCursor = query->value(Qt::ImTextAfterCursor);
995 if (textBeforeCursor.isValid() && textAfterCursor.isValid()) {
996 textBeforeCursorLen = textBeforeCursor.toString().length();
997 textAfterCursorLen = textAfterCursor.toString().length();
998 } else {
999 textBeforeCursorLen = initialCursorPos - initialBlockPos;
1000 textAfterCursorLen =
1001 query->value(Qt::ImSurroundingText).toString().length() - textBeforeCursorLen;
1002 }
1003
1004 leftLength = qMin(qMax(0, textBeforeCursorLen - (initialCursorPos - leftEnd)), leftLength);
1005 rightLength = qMin(qMax(0, textAfterCursorLen - (rightBegin - initialCursorPos)), rightLength);
1006
1007 if (leftLength == 0 && rightLength == 0)
1008 return JNI_TRUE;
1009
1010 if (leftEnd == rightBegin) {
1011 // We have no selection and no composing region; we can do everything using one event
1013 event.setCommitString({}, -leftLength, leftLength + rightLength);
1014 QGuiApplication::sendEvent(m_focusObject, &event);
1015 } else {
1016 if (initialCursorPos != initialAnchorPos) {
1018 { QInputMethodEvent::Selection, initialCursorPos - initialBlockPos, 0 }
1019 });
1020
1021 QGuiApplication::sendEvent(m_focusObject, &event);
1022 }
1023
1024 int currentCursorPos = initialCursorPos;
1025
1026 if (rightLength > 0) {
1028 event.setCommitString({}, rightBegin - currentCursorPos, rightLength);
1029 QGuiApplication::sendEvent(m_focusObject, &event);
1030
1031 currentCursorPos = rightBegin;
1032 }
1033
1034 if (leftLength > 0) {
1035 const int leftBegin = leftEnd - leftLength;
1036
1038 event.setCommitString({}, leftBegin - currentCursorPos, leftLength);
1039 QGuiApplication::sendEvent(m_focusObject, &event);
1040
1041 currentCursorPos = leftBegin;
1042
1043 if (!m_composingText.isEmpty())
1044 m_composingTextStart -= leftLength;
1045 }
1046
1047 // Restore cursor position or selection
1048 if (currentCursorPos != initialCursorPos - leftLength
1049 || initialCursorPos != initialAnchorPos) {
1050 // If we have deleted a newline character, we are now in a new block
1051 const int currentBlockPos = getBlockPosition(
1052 focusObjectInputMethodQuery(Qt::ImAbsolutePosition | Qt::ImCursorPosition));
1053
1055 { QInputMethodEvent::Selection, initialCursorPos - leftLength - currentBlockPos,
1056 initialAnchorPos - initialCursorPos },
1058 });
1059
1060 QGuiApplication::sendEvent(m_focusObject, &event);
1061 }
1062 }
1063
1064 return JNI_TRUE;
1065}
1066
1067// Android docs say the cursor must not move
1069{
1070 BatchEditLock batchEditLock(this);
1071
1072 if (!focusObjectStopComposing())
1073 return JNI_FALSE;
1074
1075 clear();
1076 return JNI_TRUE;
1077}
1078
1079bool QAndroidInputContext::focusObjectIsComposing() const
1080{
1081 return m_composingCursor != -1;
1082}
1083
1084void QAndroidInputContext::focusObjectStartComposing()
1085{
1086 if (focusObjectIsComposing() || m_composingText.isEmpty())
1087 return;
1088
1089 // Composing strings containing newline characters are rare and may cause problems
1090 if (m_composingText.contains(u'\n'))
1091 return;
1092
1093 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1094 if (!query)
1095 return;
1096
1097 if (query->value(Qt::ImCursorPosition).toInt() != query->value(Qt::ImAnchorPosition).toInt())
1098 return;
1099
1100 const int absoluteCursorPos = getAbsoluteCursorPosition(query);
1101 if (absoluteCursorPos < m_composingTextStart
1102 || absoluteCursorPos > m_composingTextStart + m_composingText.length())
1103 return;
1104
1105 m_composingCursor = absoluteCursorPos;
1106
1107 QTextCharFormat underlined;
1108 underlined.setFontUnderline(true);
1109
1110 QInputMethodEvent event(m_composingText, {
1111 { QInputMethodEvent::Cursor, absoluteCursorPos - m_composingTextStart, 1 },
1112 { QInputMethodEvent::TextFormat, 0, int(m_composingText.length()), underlined }
1113 });
1114
1115 event.setCommitString({}, m_composingTextStart - absoluteCursorPos, m_composingText.length());
1116
1117 QGuiApplication::sendEvent(m_focusObject, &event);
1118}
1119
1120bool QAndroidInputContext::focusObjectStopComposing()
1121{
1122 if (!focusObjectIsComposing())
1123 return true; // not composing
1124
1125 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1126 if (query.isNull())
1127 return false;
1128
1129 const int blockPos = getBlockPosition(query);
1130 const int localCursorPos = m_composingCursor - blockPos;
1131
1132 m_composingCursor = -1;
1133
1134 {
1135 // commit the composing test
1137 QInputMethodEvent event(QString(), attributes);
1138 event.setCommitString(m_composingText);
1139 sendInputMethodEvent(&event);
1140 }
1141 {
1142 // Moving Qt's cursor to where the preedit cursor used to be
1144 attributes.append(
1146 QInputMethodEvent event(QString(), attributes);
1147 sendInputMethodEvent(&event);
1148 }
1149
1150 return true;
1151}
1152
1154{
1155 jint res = 0;
1156 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1157 if (query.isNull())
1158 return res;
1159
1160 const uint qtInputMethodHints = query->value(Qt::ImHints).toUInt();
1161 const int localPos = query->value(Qt::ImCursorPosition).toInt();
1162
1163 bool atWordBoundary =
1164 localPos == 0
1165 && (!focusObjectIsComposing() || m_composingCursor == m_composingTextStart);
1166
1167 if (!atWordBoundary) {
1168 QString surroundingText = query->value(Qt::ImSurroundingText).toString();
1169 surroundingText.truncate(localPos);
1170 if (focusObjectIsComposing())
1171 surroundingText += QStringView{m_composingText}.left(m_composingCursor - m_composingTextStart);
1172 // Add a character to see if it is at the end of the sentence or not
1173 QTextBoundaryFinder finder(QTextBoundaryFinder::Sentence, surroundingText + u'A');
1174 finder.setPosition(surroundingText.length());
1175 if (finder.isAtBoundary())
1176 atWordBoundary = finder.isAtBoundary();
1177 }
1178 if (atWordBoundary && !(qtInputMethodHints & Qt::ImhLowercaseOnly) && !(qtInputMethodHints & Qt::ImhNoAutoUppercase))
1179 res |= CAP_MODE_SENTENCES;
1180
1181 if (qtInputMethodHints & Qt::ImhUppercaseOnly)
1182 res |= CAP_MODE_CHARACTERS;
1183
1184 return res;
1185}
1186
1187
1188
1189const QAndroidInputContext::ExtractedText &QAndroidInputContext::getExtractedText(jint /*hintMaxChars*/, jint /*hintMaxLines*/, jint /*flags*/)
1190{
1191 // Note to self: "if the GET_EXTRACTED_TEXT_MONITOR flag is set, you should be calling
1192 // updateExtractedText(View, int, ExtractedText) whenever you call
1193 // updateSelection(View, int, int, int, int)." QTBUG-37980
1194
1195 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(
1197 if (query.isNull())
1198 return m_extractedText;
1199
1200 const int cursorPos = getAbsoluteCursorPosition(query);
1201 const int blockPos = getBlockPosition(query);
1202
1203 // It is documented that we should try to return hintMaxChars
1204 // characters, but standard Android controls always return all text, and
1205 // there are input methods out there that (surprise) seem to depend on
1206 // what happens in reality rather than what's documented.
1207
1210 if (textBeforeCursor.isValid() && textAfterCursor.isValid()) {
1211 if (focusObjectIsComposing()) {
1212 m_extractedText.text =
1213 textBeforeCursor.toString() + m_composingText + textAfterCursor.toString();
1214 } else {
1215 m_extractedText.text = textBeforeCursor.toString() + textAfterCursor.toString();
1216 }
1217
1218 m_extractedText.startOffset = qMax(0, cursorPos - textBeforeCursor.toString().length());
1219 } else {
1220 m_extractedText.text = focusObjectInputMethodQuery(Qt::ImSurroundingText)
1221 ->value(Qt::ImSurroundingText).toString();
1222
1223 if (focusObjectIsComposing())
1224 m_extractedText.text.insert(cursorPos - blockPos, m_composingText);
1225
1226 m_extractedText.startOffset = blockPos;
1227 }
1228
1229 if (focusObjectIsComposing()) {
1230 m_extractedText.selectionStart = m_composingCursor - m_extractedText.startOffset;
1231 m_extractedText.selectionEnd = m_extractedText.selectionStart;
1232 } else {
1233 m_extractedText.selectionStart = cursorPos - m_extractedText.startOffset;
1234 m_extractedText.selectionEnd =
1235 blockPos + query->value(Qt::ImAnchorPosition).toInt() - m_extractedText.startOffset;
1236
1237 // Some keyboards misbehave when selectionStart > selectionEnd
1238 if (m_extractedText.selectionStart > m_extractedText.selectionEnd)
1239 std::swap(m_extractedText.selectionStart, m_extractedText.selectionEnd);
1240 }
1241
1242 return m_extractedText;
1243}
1244
1246{
1247 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1248 if (query.isNull())
1249 return QString();
1250
1251 return query->value(Qt::ImCurrentSelection).toString();
1252}
1253
1255{
1256 if (length <= 0)
1257 return QString();
1258
1259 QString text;
1260
1262 if (reportedTextAfter.isValid()) {
1263 text = reportedTextAfter.toString();
1264 } else {
1265 // Compatibility code for old controls that do not implement the new API
1267 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImSurroundingText);
1268 if (query) {
1269 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1270 text = query->value(Qt::ImSurroundingText).toString().mid(cursorPos);
1271 }
1272 }
1273
1274 if (focusObjectIsComposing()) {
1275 // Controls do not report preedit text, so we have to add it
1276 const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart;
1277 text = QStringView{m_composingText}.mid(cursorPosInsidePreedit) + text;
1278 } else {
1279 // We must not return selected text if there is any
1281 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAnchorPosition);
1282 if (query) {
1283 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1284 const int anchorPos = query->value(Qt::ImAnchorPosition).toInt();
1285 if (anchorPos > cursorPos)
1286 text.remove(0, anchorPos - cursorPos);
1287 }
1288 }
1289
1291 return text;
1292}
1293
1295{
1296 if (length <= 0)
1297 return QString();
1298
1299 QString text;
1300
1302 if (reportedTextBefore.isValid()) {
1303 text = reportedTextBefore.toString();
1304 } else {
1305 // Compatibility code for old controls that do not implement the new API
1307 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImSurroundingText);
1308 if (query) {
1309 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1310 text = query->value(Qt::ImSurroundingText).toString().left(cursorPos);
1311 }
1312 }
1313
1314 if (focusObjectIsComposing()) {
1315 // Controls do not report preedit text, so we have to add it
1316 const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart;
1317 text += QStringView{m_composingText}.left(cursorPosInsidePreedit);
1318 } else {
1319 // We must not return selected text if there is any
1321 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAnchorPosition);
1322 if (query) {
1323 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1324 const int anchorPos = query->value(Qt::ImAnchorPosition).toInt();
1325 if (anchorPos < cursorPos)
1326 text.chop(cursorPos - anchorPos);
1327 }
1328 }
1329
1330 if (text.length() > length)
1331 text = text.right(length);
1332 return text;
1333}
1334
1335/*
1336 Android docs say that this function should:
1337 - remove the current composing text, if there is any
1338 - otherwise remove currently selected text, if there is any
1339 - insert new text in place of old composing text or, if there was none, at current cursor position
1340 - mark the inserted text as composing
1341 - move cursor as specified by newCursorPosition: if > 0, it is relative to the end of inserted
1342 text - 1; if <= 0, it is relative to the start of inserted text
1343 */
1344
1345jboolean QAndroidInputContext::setComposingText(const QString &text, jint newCursorPosition)
1346{
1347 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1348 if (query.isNull())
1349 return JNI_FALSE;
1350
1351 BatchEditLock batchEditLock(this);
1352
1353 const int absoluteCursorPos = getAbsoluteCursorPosition(query);
1354 int absoluteAnchorPos = getBlockPosition(query) + query->value(Qt::ImAnchorPosition).toInt();
1355
1356 auto setCursorPosition = [=]() {
1357 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1358 QInputMethodEvent event({}, { { QInputMethodEvent::Selection, cursorPos, 0 } });
1359 QGuiApplication::sendEvent(m_focusObject, &event);
1360 };
1361
1362 // If we have composing region and selection (and therefore focusObjectIsComposing() == false),
1363 // we must clear selection so that we won't delete it when we will be replacing composing text
1364 if (!m_composingText.isEmpty() && absoluteCursorPos != absoluteAnchorPos) {
1365 setCursorPosition();
1366 absoluteAnchorPos = absoluteCursorPos;
1367 }
1368
1369 // The value of Qt::ImCursorPosition is not updated at the start
1370 // when the first character is added, so we must update it (QTBUG-85090)
1371 if (absoluteCursorPos == 0 && text.length() == 1 && getTextAfterCursor(1,1).length() >= 0) {
1372 setCursorPosition();
1373 }
1374
1375 // If we had no composing region, pretend that we had a zero-length composing region at current
1376 // cursor position to simplify code. Also account for that we must delete selected text if there
1377 // (still) is any.
1378 const int effectiveAbsoluteCursorPos = qMin(absoluteCursorPos, absoluteAnchorPos);
1379 if (m_composingTextStart == -1)
1380 m_composingTextStart = effectiveAbsoluteCursorPos;
1381
1382 const int oldComposingTextLen = m_composingText.length();
1383 m_composingText = text;
1384
1385 const int newAbsoluteCursorPos =
1386 newCursorPosition <= 0
1387 ? m_composingTextStart + newCursorPosition
1388 : m_composingTextStart + m_composingText.length() + newCursorPosition - 1;
1389
1390 const bool focusObjectWasComposing = focusObjectIsComposing();
1391
1392 // Same checks as in focusObjectStartComposing()
1393 if (!m_composingText.isEmpty() && !m_composingText.contains(u'\n')
1394 && newAbsoluteCursorPos >= m_composingTextStart
1395 && newAbsoluteCursorPos <= m_composingTextStart + m_composingText.length())
1396 m_composingCursor = newAbsoluteCursorPos;
1397 else
1398 m_composingCursor = -1;
1399
1400 if (focusObjectIsComposing()) {
1401 QTextCharFormat underlined;
1402 underlined.setFontUnderline(true);
1403
1404 QInputMethodEvent event(m_composingText, {
1405 { QInputMethodEvent::TextFormat, 0, int(m_composingText.length()), underlined },
1406 { QInputMethodEvent::Cursor, m_composingCursor - m_composingTextStart, 1 }
1407 });
1408
1409 if (oldComposingTextLen > 0 && !focusObjectWasComposing) {
1410 event.setCommitString({}, m_composingTextStart - effectiveAbsoluteCursorPos,
1411 oldComposingTextLen);
1412 }
1413 if (m_composingText.isEmpty())
1414 clear();
1415
1416 QGuiApplication::sendEvent(m_focusObject, &event);
1417 } else {
1418 QInputMethodEvent event({}, {});
1419
1420 if (focusObjectWasComposing) {
1421 event.setCommitString(m_composingText);
1422 } else {
1423 event.setCommitString(m_composingText,
1424 m_composingTextStart - effectiveAbsoluteCursorPos,
1425 oldComposingTextLen);
1426 }
1427 if (m_composingText.isEmpty())
1428 clear();
1429
1430 QGuiApplication::sendEvent(m_focusObject, &event);
1431 }
1432
1433 if (!focusObjectIsComposing() && newCursorPosition != 1) {
1434 // Move cursor using a separate event because if we have inserted or deleted a newline
1435 // character, then we are now inside an another block
1436
1437 const int newBlockPos = getBlockPosition(
1438 focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAbsolutePosition));
1439
1441 { QInputMethodEvent::Selection, newAbsoluteCursorPos - newBlockPos, 0 }
1442 });
1443
1444 QGuiApplication::sendEvent(m_focusObject, &event);
1445 }
1446
1447 keyDown();
1448
1449 return JNI_TRUE;
1450}
1451
1452// Android docs say:
1453// * start may be after end, same meaning as if swapped
1454// * this function should not trigger updateSelection, but Android's native EditText does trigger it
1455// * if start == end then we should stop composing
1457{
1458 BatchEditLock batchEditLock(this);
1459
1460 // Qt will not include the current preedit text in the query results, and interprets all
1461 // parameters relative to the text excluding the preedit. The simplest solution is therefore to
1462 // tell Qt that we commit the text before we set the new region. This may cause a little flicker, but is
1463 // much more robust than trying to keep the two different world views in sync
1464
1466
1467 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1468 if (query.isNull())
1469 return JNI_FALSE;
1470
1471 if (start == end)
1472 return JNI_TRUE;
1473 if (start > end)
1474 qSwap(start, end);
1475
1476 QString text = query->value(Qt::ImSurroundingText).toString();
1477 int textOffset = getBlockPosition(query);
1478
1479 if (start < textOffset || end > textOffset + text.length()) {
1480 const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
1481
1482 if (end - textOffset > text.length()) {
1483 const QString after = query->value(Qt::ImTextAfterCursor).toString();
1484 const int additionalSuffixLen = after.length() - (text.length() - cursorPos);
1485
1486 if (additionalSuffixLen > 0)
1487 text += QStringView{after}.right(additionalSuffixLen);
1488 }
1489
1490 if (start < textOffset) {
1491 QString before = query->value(Qt::ImTextBeforeCursor).toString();
1492 before.chop(cursorPos);
1493
1494 if (!before.isEmpty()) {
1495 text = before + text;
1496 textOffset -= before.length();
1497 }
1498 }
1499
1500 if (start < textOffset || end - textOffset > text.length()) {
1501 qCDebug(lcQpaInputMethods) << "Warning: setComposingRegion: failed to retrieve text from composing region";
1502
1503 return JNI_TRUE;
1504 }
1505 }
1506
1507 m_composingText = text.mid(start - textOffset, end - start);
1508 m_composingTextStart = start;
1509
1510 return JNI_TRUE;
1511}
1512
1514{
1515 QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
1516 if (query.isNull())
1517 return JNI_FALSE;
1518
1519 BatchEditLock batchEditLock(this);
1520
1521 int blockPosition = getBlockPosition(query);
1522 int localCursorPos = start - blockPosition;
1523
1524 if (focusObjectIsComposing() && start == end && start >= m_composingTextStart
1525 && start <= m_composingTextStart + m_composingText.length()) {
1526 // not actually changing the selection; just moving the
1527 // preedit cursor
1528 int localOldPos = query->value(Qt::ImCursorPosition).toInt();
1529 int pos = localCursorPos - localOldPos;
1532
1533 //but we have to tell Qt about the compose text all over again
1534
1535 // Show compose text underlined
1536 QTextCharFormat underlined;
1537 underlined.setFontUnderline(true);
1539 QVariant(underlined)));
1540 m_composingCursor = start;
1541
1542 QInputMethodEvent event(m_composingText, attributes);
1543 QGuiApplication::sendEvent(m_focusObject, &event);
1544 } else {
1545 // actually changing the selection
1546 focusObjectStopComposing();
1549 localCursorPos,
1550 end - start));
1551 QInputMethodEvent event({}, attributes);
1552 QGuiApplication::sendEvent(m_focusObject, &event);
1553 }
1554 return JNI_TRUE;
1555}
1556
1558{
1559 BatchEditLock batchEditLock(this);
1560
1561 focusObjectStopComposing();
1562 m_handleMode = ShowCursor;
1564 return JNI_TRUE;
1565}
1566
1568{
1569 BatchEditLock batchEditLock(this);
1570
1571 // This is probably not what native EditText would do, but normally if there is selection, then
1572 // there will be no composing region
1574
1575 m_handleMode = ShowCursor;
1577 return JNI_TRUE;
1578}
1579
1581{
1582 BatchEditLock batchEditLock(this);
1583
1584 focusObjectStopComposing();
1585 m_handleMode = ShowCursor;
1587 return JNI_TRUE;
1588}
1589
1591{
1592#warning TODO
1593 return JNI_FALSE;
1594}
1595
1597{
1598 BatchEditLock batchEditLock(this);
1599
1600 // TODO: This is not what native EditText does
1602
1603 m_handleMode = ShowCursor;
1605 return JNI_TRUE;
1606}
1607
1609{
1610 for (int i = 0; i < sequence.count(); ++i) {
1611 const QKeyCombination keys = sequence[i];
1612 Qt::Key key = Qt::Key(keys.toCombined() & ~Qt::KeyboardModifierMask);
1613 Qt::KeyboardModifiers mod = Qt::KeyboardModifiers(keys.toCombined() & Qt::KeyboardModifierMask);
1614
1615 QKeyEvent pressEvent(QEvent::KeyPress, key, mod);
1616 QKeyEvent releaseEvent(QEvent::KeyRelease, key, mod);
1617
1618 QGuiApplication::sendEvent(m_focusObject, &pressEvent);
1619 QGuiApplication::sendEvent(m_focusObject, &releaseEvent);
1620 }
1621}
1622
1623QSharedPointer<QInputMethodQueryEvent> QAndroidInputContext::focusObjectInputMethodQuery(Qt::InputMethodQueries queries) {
1624 if (!qGuiApp)
1625 return {};
1626
1627 QObject *focusObject = qGuiApp->focusObject();
1628 if (!focusObject)
1629 return {};
1630
1632 QCoreApplication::sendEvent(focusObject, ret);
1634}
1635
1636void QAndroidInputContext::sendInputMethodEvent(QInputMethodEvent *event)
1637{
1638 if (!qGuiApp)
1639 return;
1640
1641 QObject *focusObject = qGuiApp->focusObject();
1642 if (!focusObject)
1643 return;
1644
1645 QCoreApplication::sendEvent(focusObject, event);
1646}
1647
static JNINativeMethod methods[]
jboolean setSelection(jint start, jint end)
void safeCall(const std::function< void()> &func, Qt::ConnectionType conType=Qt::BlockingQueuedConnection)
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)
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)
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.
QRect availableGeometry() const override
Reimplement in subclass to return the pixel geometry of the available space This normally is the desk...
\inmodule QtCore
Definition qchar.h:48
static bool sendEvent(QObject *receiver, QEvent *event)
Sends event event directly to receiver receiver, using the notify() function.
@ KeyRelease
Definition qcoreevent.h:65
@ KeyPress
Definition qcoreevent.h:64
static Qt::ApplicationState applicationState()
static QWindow * focusWindow()
Returns the QWindow that receives events tied to focus, such as key events.
static QInputMethod * inputMethod()
returns the input method.
The QInputMethodEvent class provides parameters for input method events.
Definition qevent.h:624
The QInputMethodQueryEvent class provides an event sent by the input context to input objects.
Definition qevent.h:678
The QInputMethod class provides access to the active text input method.
Action
Indicates the kind of action performed by the user.
void anchorRectangleChanged()
static QVariant queryFocusObject(Qt::InputMethodQuery query, const QVariant &argument)
Send query to the current focus object with parameters argument and return the result.
void inputItemClipRectangleChanged()
QTransform inputItemTransform() const
Returns the transformation from input item coordinates to the window coordinates.
void cursorRectangleChanged()
\inmodule QtCore
The QKeyEvent class describes a key event.
Definition qevent.h:423
The QKeySequence class encapsulates a key sequence as used by shortcuts.
int count() const
Returns the number of keys in the key sequence.
Definition qlist.h:74
void append(parameter_type t)
Definition qlist.h:441
\inmodule QtCore
Definition qobject.h:90
QObject * parent() const
Returns a pointer to the parent object.
Definition qobject.h:311
static QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *member, Qt::ConnectionType=Qt::AutoConnection)
\threadsafe
Definition qobject.cpp:2823
QVariant property(const char *name) const
Returns the value of the object's name property.
Definition qobject.cpp:4187
The QPlatformInputContext class abstracts the input method dependent data and composing state.
static QRectF anchorRectangle()
QPlatformInputContext::anchorRectangle.
static void setSelectionOnFocusObject(const QPointF &anchorPos, const QPointF &cursorPos)
QPlatformInputContext::setSelectionOnFocusObject.
static QVariant queryFocusObject(Qt::InputMethodQuery query, QPointF position)
QPlatformInputContext::queryFocusObject.
static QRectF cursorRectangle()
QPlatformInputContext::cursorRectangle.
static QRectF inputItemRectangle()
QPlatformInputContext::inputItemRectangle.
The QPlatformWindow class provides an abstraction for top-level windows.
virtual QPoint mapToGlobal(const QPoint &pos) const
Translates the window coordinate pos to global screen coordinates using native methods.
\inmodule QtCore\reentrant
Definition qpoint.h:214
\inmodule QtCore\reentrant
Definition qpoint.h:23
constexpr int x() const noexcept
Returns the x coordinate of this point.
Definition qpoint.h:127
constexpr void setY(int y) noexcept
Sets the y coordinate of this point to the given y coordinate.
Definition qpoint.h:142
constexpr int y() const noexcept
Returns the y coordinate of this point.
Definition qpoint.h:132
constexpr void setX(int x) noexcept
Sets the x coordinate of this point to the given x coordinate.
Definition qpoint.h:137
\inmodule QtCore\reentrant
Definition qrect.h:483
constexpr QRect toRect() const noexcept
Returns a QRect based on the values of this rectangle.
Definition qrect.h:845
\inmodule QtCore\reentrant
Definition qrect.h:30
constexpr QPoint topLeft() const noexcept
Returns the position of the rectangle's top-left corner.
Definition qrect.h:220
constexpr QSize size() const noexcept
Returns the size of the rectangle.
Definition qrect.h:241
constexpr int right() const noexcept
Returns the x-coordinate of the rectangle's right edge.
Definition qrect.h:178
\inmodule QtCore
\inmodule QtCore
Definition qstringview.h:76
\macro QT_RESTRICTED_CAST_FROM_ASCII
Definition qstring.h:127
QString right(qsizetype n) const
Returns a substring that contains the n rightmost characters of the string.
Definition qstring.cpp:5180
void chop(qsizetype n)
Removes n characters from the end of the string.
Definition qstring.cpp:6180
static QString fromLatin1(QByteArrayView ba)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qstring.cpp:5710
void truncate(qsizetype pos)
Truncates the string at the given position index.
Definition qstring.cpp:6159
void clear()
Clears the contents of the string and makes it null.
Definition qstring.h:1107
const QChar * constData() const
Returns a pointer to the data stored in the QString.
Definition qstring.h:1101
reverse_iterator rend()
Definition qstring.h:854
QString mid(qsizetype position, qsizetype n=-1) const
Returns a string that contains n characters of this string, starting at the specified position index.
Definition qstring.cpp:5204
bool isEmpty() const
Returns true if the string has no characters; otherwise returns false.
Definition qstring.h:1083
int compare(const QString &s, Qt::CaseSensitivity cs=Qt::CaseSensitive) const noexcept
Definition qstring.cpp:6498
QString & insert(qsizetype i, QChar c)
Definition qstring.cpp:3110
bool contains(QChar c, Qt::CaseSensitivity cs=Qt::CaseSensitive) const
Definition qstring.h:1217
static QString number(int, int base=10)
This is an overloaded member function, provided for convenience. It differs from the above function o...
Definition qstring.cpp:7822
QString left(qsizetype n) const
Returns a substring that contains the n leftmost characters of the string.
Definition qstring.cpp:5161
QString & remove(qsizetype i, qsizetype len)
Removes n characters from the string, starting at the given position index, and returns a reference t...
Definition qstring.cpp:3435
qsizetype length() const
Returns the number of characters in this string.
Definition qstring.h:187
reverse_iterator rbegin()
Definition qstring.h:853
BatchEditLock(const BatchEditLock &)=delete
BatchEditLock(QAndroidInputContext *context)
BatchEditLock & operator=(const BatchEditLock &)=delete
void setPosition(qsizetype position)
Sets the current position of the QTextBoundaryFinder to position.
qsizetype toNextBoundary()
Moves the QTextBoundaryFinder to the next boundary position and returns that position.
qsizetype toPreviousBoundary()
Moves the QTextBoundaryFinder to the previous boundary position and returns that position.
void toStart()
Moves the finder to the start of the string.
void toEnd()
Moves the finder to the end of the string.
bool isAtBoundary() const
Returns true if the object's position() is currently at a valid text boundary.
qsizetype position() const
Returns the current position of the QTextBoundaryFinder.
void setFontUnderline(bool underline)
If underline is true, sets the text format's font to be underlined; otherwise it is displayed non-und...
static QThread * currentThread()
Definition qthread.cpp:966
void setSingleShot(bool singleShot)
Definition qtimer.cpp:580
void start(int msec)
Starts or restarts the timer with a timeout interval of msec milliseconds.
Definition qtimer.cpp:208
void setInterval(int msec)
Definition qtimer.cpp:607
bool isActive() const
Returns true if the timer is running (pending); otherwise returns false.
Definition qtimer.cpp:156
void stop()
Stops the timer.
Definition qtimer.cpp:226
void timeout(QPrivateSignal)
This signal is emitted when the timer times out.
void setTimerType(Qt::TimerType atype)
Definition qtimer.cpp:661
The QTransform class specifies 2D transformations of a coordinate system.
Definition qtransform.h:20
QPoint map(const QPoint &p) const
This is an overloaded member function, provided for convenience. It differs from the above function o...
QTransform inverted(bool *invertible=nullptr) const
Returns an inverted copy of this matrix.
\inmodule QtCore
Definition qvariant.h:64
T value() const &
Definition qvariant.h:511
bool isValid() const
Returns true if the storage type of this variant is not QMetaType::UnknownType; otherwise returns fal...
Definition qvariant.h:707
int toInt(bool *ok=nullptr) const
Returns the variant as an int if the variant has userType() \l QMetaType::Int, \l QMetaType::Bool,...
QString toString() const
Returns the variant as a QString if the variant has a userType() including, but not limited to:
bool toBool() const
Returns the variant as a bool if the variant has userType() Bool.
QString str
[2]
QString text
QCursor cursor
qSwap(pi, e)
rect
[4]
else opt state
[0]
T fromNativePixels(const T &value, const C *context)
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()
void updateHandles(int mode, QPoint editMenuPos, uint32_t editButtons, QPoint cursor, QPoint anchor, bool rtl)
QRect softwareKeyboardRect()
QAndroidPlatformIntegration * androidPlatformIntegration()
@ ImTextBeforeCursor
@ ImSurroundingText
@ ImCursorPosition
@ ImEnterKeyType
@ ImCurrentSelection
@ ImAbsolutePosition
@ ImReadOnly
@ ImAnchorPosition
@ ImHints
@ ImEnabled
@ ImTextAfterCursor
@ VeryCoarseTimer
@ ImhUppercaseOnly
@ ImhLowercaseOnly
@ ImhNoAutoUppercase
@ KeyboardModifierMask
@ CaseInsensitive
ApplicationState
Definition qnamespace.h:261
@ ApplicationActive
Definition qnamespace.h:265
ConnectionType
@ BlockingQueuedConnection
static void * context
static int getBlockPosition(const QSharedPointer< QInputMethodQueryEvent > &query)
static char const *const QtExtractedTextClassName
static jboolean cut(JNIEnv *, jobject)
static jint getCursorCapsMode(JNIEnv *, jobject, jint reqModes)
static QAndroidInputContext * m_androidInputContext
static JNINativeMethod methods[]
static QRect screenInputItemRectangle()
static jboolean finishComposingText(JNIEnv *, jobject)
static jobject getExtractedText(JNIEnv *env, jobject, int hintMaxChars, int hintMaxLines, jint flags)
static char const *const QtNativeInputConnectionClassName
static jfieldID m_partialStartOffsetFieldID
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 jmethodID m_classConstructorMethodID
static jfieldID m_startOffsetFieldID
static void runOnQtThread(const std::function< void()> &func)
static jboolean commitText(JNIEnv *env, jobject, jstring text, jint newCursorPosition)
static jfieldID m_partialEndOffsetFieldID
static jboolean paste(JNIEnv *, jobject)
static jboolean beginBatchEdit(JNIEnv *, jobject)
static jstring getSelectedText(JNIEnv *env, jobject, jint flags)
static jboolean setComposingRegion(JNIEnv *, jobject, jint start, jint end)
static jfieldID m_textFieldID
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 int m_selectHandleWidth
static jstring getTextAfterCursor(JNIEnv *env, jobject, jint length, jint flags)
static int getAbsoluteCursorPosition(const QSharedPointer< QInputMethodQueryEvent > &query)
static jboolean endBatchEdit(JNIEnv *, jobject)
static jclass m_extractedTextClass
static jfieldID m_selectionStartFieldID
static jboolean setSelection(JNIEnv *, jobject, jint start, jint end)
static jboolean selectAll(JNIEnv *, jobject)
#define Q_UNLIKELY(x)
#define qGuiApp
#define qCritical
Definition qlogging.h:163
#define qWarning
Definition qlogging.h:162
#define qCDebug(category,...)
return ret
constexpr const T & qMin(const T &a, const T &b)
Definition qminmax.h:40
constexpr const T & qMax(const T &a, const T &b)
Definition qminmax.h:42
#define SLOT(a)
Definition qobjectdefs.h:51
#define Q_ARG(Type, data)
Definition qobjectdefs.h:62
#define SIGNAL(a)
Definition qobjectdefs.h:52
static bool contains(const QJsonArray &haystack, unsigned needle)
Definition qopengl.cpp:116
GLint GLint GLint GLint GLint x
[0]
GLuint64 key
GLuint GLuint end
GLenum GLuint GLenum GLsizei length
GLuint object
[3]
GLbitfield flags
GLuint start
GLint y
struct _cl_event * event
GLenum query
GLenum func
Definition qopenglext.h:663
GLuint res
static qreal position(const QQuickItem *item, QQuickAnchors::Anchor anchorLine)
Q_CORE_EXPORT int qEnvironmentVariableIntValue(const char *varName, bool *ok=nullptr) noexcept
#define Q_UNUSED(x)
unsigned int uint
Definition qtypes.h:29
QStringList keys
myObject disconnect()
[26]
aWidget window() -> setWindowTitle("New Window Title")
[2]
static bool invokeMethod(QObject *obj, const char *member, Qt::ConnectionType, QGenericReturnArgument ret, QGenericArgument val0=QGenericArgument(nullptr), QGenericArgument val1=QGenericArgument(), QGenericArgument val2=QGenericArgument(), QGenericArgument val3=QGenericArgument(), QGenericArgument val4=QGenericArgument(), QGenericArgument val5=QGenericArgument(), QGenericArgument val6=QGenericArgument(), QGenericArgument val7=QGenericArgument(), QGenericArgument val8=QGenericArgument(), QGenericArgument val9=QGenericArgument())
\threadsafe This is an overloaded member function, provided for convenience. It differs from the abov...