summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVova Mshanetskiy <vovams163@gmail.com>2019-05-31 16:25:14 +0300
committerVova Mshanetskiy <vovams163@gmail.com>2019-06-05 18:21:35 +0300
commit1ade5ea41ab80e49e92fe46e0c44f76ee2e5e7fb (patch)
tree5f8c9203875d6138bfd9df5ecb67af7332b8bac1
parente5f2be256fb3bb07a98693cb90a4d7e8bc63c508 (diff)
QAndroidInputContext: Improve compatibility with virtual keyboards
This commit improves QAndroidInputContext's conformance to Android's InputConnection interface and/or consistency of it's behavior with Android's native EditText control. * Composing region is now completely independent from cursor and selection, as required by InputConnection documentation. Also, Qt will now never clear composing region (i.e. call finishComposingText()) without receiving a command to do so from the keyboard. This is important for the following reasons: - Some keyboards misbehave if we change composing region without receiving a command from them. Notably, Samsung Keyboard does (QTBUG-68822). - Due to asynchronous nature of interaction between QAndroidInputContext and the keyboard, when user drags cursor handle quickly, the keyboard may call setComposingRegion() to mark a word, which is no longer under the cursor. This was causing text corruption (QTBUG-43156, QTBUG-59958). Also SwiftKey makes such calls when user presses Enter key (QTBUG-57819). - For similar reasons selecting a word with a double-tap could cause text corruption. The keyboard may call setComposingRegion() in response to the first tap after the second tap has been processed and the word has already been already selected. This is achieved by keeping track of start and end of composing region independently from the editor. Whenever possible (i.e. when there is no selection and the cursor is inside composing region), the composing text is represented as preedit text inside editor. And whenever that is imposible, the editor is told to commit, but QAndroidInputContext keeps information about composing region internally to be able to correctly interract with the keyboard. * deleteSurroundingText() has been re-written to work correctly when there are selection and/or composing region. Some keyboards (e.g Ginger Keyboard) do call deleteSurroundingText() when there is non-empty composing region. * All operations are now performed inside a batch edit (i.e. QAndroidInputContext now calls beginBatchEdit() and endBatchEdit() on itself) to ensure that an intermediate state is never reported to the keyboard, whenever an operation requires more than one QInputMethodEvent. BatchEditLock helper class was added to call begin/endBatchEdit() in RAII style. m_blockUpdateSelection has been removed because m_batchEditNestingLevel is now used instead of it. * Selection start and end positions are now reported to the keyboard so that start <= end. Some keyboards can not handle start > end. * getTextBefore/AfterCursor() now exclude selected text from their return values. While Android docs say "text before/after cursor", what they really mean is "text before/after selection" because "the cursor and the selection are one and the same thing". Some keyboards (e.g. Gboard) were behaving incorrectly when selected text was being returned. * getExtractedText() now tries to obtain and return the whole text from the editor. This is to fix compatibility with some buggy keyboards (e.g. Samsung Keyboard, Minuum) that ignore startOffset field and assume that selectionStart and selectionEnd are absolute values. Then they issue commands with wrong indexes in some cases. Fixes: QTBUG-43156 Fixes: QTBUG-59958 Fixes: QTBUG-57819 Fixes: QTBUG-68822 Change-Id: I7e71f3bcfbb2c32248d653a4197293db03579a79 Reviewed-by: BogDan Vatra <bogdan@kdab.com>
-rw-r--r--src/plugins/platforms/android/qandroidinputcontext.cpp626
-rw-r--r--src/plugins/platforms/android/qandroidinputcontext.h6
2 files changed, 430 insertions, 202 deletions
diff --git a/src/plugins/platforms/android/qandroidinputcontext.cpp b/src/plugins/platforms/android/qandroidinputcontext.cpp
index 4d9af18053..fa07af8c46 100644
--- a/src/plugins/platforms/android/qandroidinputcontext.cpp
+++ b/src/plugins/platforms/android/qandroidinputcontext.cpp
@@ -64,29 +64,33 @@
QT_BEGIN_NAMESPACE
-template <typename T>
-class ScopedValueChangeBack
+namespace {
+
+class BatchEditLock
{
public:
- ScopedValueChangeBack(T &variable, T newValue)
- : m_oldValue(variable),
- m_variable(variable)
- {
- m_variable = newValue;
- }
- inline void setOldValue()
+
+ explicit BatchEditLock(QAndroidInputContext *context)
+ : m_context(context)
{
- m_variable = m_oldValue;
+ m_context->beginBatchEdit();
}
- ~ScopedValueChangeBack()
+
+ ~BatchEditLock()
{
- setOldValue();
+ m_context->endBatchEdit();
}
+
+ BatchEditLock(const BatchEditLock &) = delete;
+ BatchEditLock &operator=(const BatchEditLock &) = delete;
+
private:
- T m_oldValue;
- T &m_variable;
+
+ QAndroidInputContext *m_context;
};
+} // namespace anonymous
+
static QAndroidInputContext *m_androidInputContext = 0;
static char const *const QtNativeInputConnectionClassName = "org/qtproject/qt5/android/QtNativeInputConnection";
static char const *const QtExtractedTextClassName = "org/qtproject/qt5/android/QtExtractedText";
@@ -423,8 +427,12 @@ static QRect inputItemRectangle()
}
QAndroidInputContext::QAndroidInputContext()
- : QPlatformInputContext(), m_composingTextStart(-1), m_blockUpdateSelection(false),
- m_handleMode(Hidden), m_batchEditNestingLevel(0), m_focusObject(0)
+ : QPlatformInputContext()
+ , m_composingTextStart(-1)
+ , m_composingCursor(-1)
+ , m_handleMode(Hidden)
+ , m_batchEditNestingLevel(0)
+ , m_focusObject(0)
{
jclass clazz = QJNIEnvironmentPrivate::findClass(QtNativeInputConnectionClassName);
if (Q_UNLIKELY(!clazz)) {
@@ -565,13 +573,13 @@ void QAndroidInputContext::reset()
void QAndroidInputContext::commit()
{
- finishComposingText();
+ focusObjectStopComposing();
}
void QAndroidInputContext::updateCursorPosition()
{
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
- if (!query.isNull() && !m_blockUpdateSelection && !m_batchEditNestingLevel) {
+ if (!query.isNull() && m_batchEditNestingLevel == 0) {
const int cursorPos = getAbsoluteCursorPosition(query);
const int composeLength = m_composingText.length();
@@ -579,24 +587,29 @@ void QAndroidInputContext::updateCursorPosition()
if (m_composingText.isEmpty() != (m_composingTextStart == -1))
qWarning() << "Input method out of sync" << m_composingText << m_composingTextStart;
- int realCursorPosition = cursorPos;
- int realAnchorPosition = cursorPos;
+ int realSelectionStart = cursorPos;
+ int realSelectionEnd = cursorPos;
int cpos = query->value(Qt::ImCursorPosition).toInt();
int anchor = query->value(Qt::ImAnchorPosition).toInt();
if (cpos != anchor) {
if (!m_composingText.isEmpty()) {
qWarning("Selecting text while preediting may give unpredictable results.");
- finishComposingText();
+ focusObjectStopComposing();
}
int blockPos = getBlockPosition(query);
- realCursorPosition = blockPos + cpos;
- realAnchorPosition = blockPos + anchor;
+ realSelectionStart = blockPos + cpos;
+ realSelectionEnd = blockPos + anchor;
}
// Qt's idea of the cursor position is the start of the preedit area, so we maintain our own preedit cursor pos
- if (!m_composingText.isEmpty())
- realCursorPosition = realAnchorPosition = m_composingCursor;
- QtAndroidInput::updateSelection(realCursorPosition, realAnchorPosition,
+ if (focusObjectIsComposing())
+ realSelectionStart = realSelectionEnd = m_composingCursor;
+
+ // Some keyboards misbahave when selStart > selEnd
+ if (realSelectionStart > realSelectionEnd)
+ std::swap(realSelectionStart, realSelectionEnd);
+
+ QtAndroidInput::updateSelection(realSelectionStart, realSelectionEnd,
m_composingTextStart, m_composingTextStart + composeLength); // pre-edit text
}
}
@@ -666,7 +679,7 @@ void QAndroidInputContext::updateSelectionHandles()
*/
void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y)
{
- if (m_batchEditNestingLevel.load() || m_blockUpdateSelection) {
+ if (m_batchEditNestingLevel != 0) {
qWarning() << "QAndroidInputContext::handleLocationChanged returned";
return;
}
@@ -741,15 +754,15 @@ void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y)
}
// Check if handle has been dragged far enough
- if (m_composingText.isEmpty() && newCpos == cpos && newAnchor == anchor)
+ if (!focusObjectIsComposing() && newCpos == cpos && newAnchor == anchor)
return;
/*
- If there is composing text, we have to compare newCpos with m_composingCursor instead of cpos.
- And since there is nothing to compare with newAnchor, we perform the check only when user
- drags the cursor handle.
+ If the editor is currently in composing state, we have to compare newCpos with
+ m_composingCursor instead of cpos. And since there is nothing to compare with newAnchor, we
+ perform the check only when user drags the cursor handle.
*/
- if (!m_composingText.isEmpty() && handleId == 1) {
+ if (focusObjectIsComposing() && handleId == 1) {
int absoluteCpos = query.value(Qt::ImAbsolutePosition).toInt(&ok);
if (!ok)
absoluteCpos = cpos;
@@ -759,7 +772,9 @@ void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y)
return;
}
- finishComposingText();
+ BatchEditLock batchEditLock(this);
+
+ focusObjectStopComposing();
QList<QInputMethodEvent::Attribute> attributes;
attributes.append({ QInputMethodEvent::Selection, newAnchor, newCpos - newAnchor });
@@ -777,7 +792,7 @@ void QAndroidInputContext::touchDown(int x, int y)
m_handleMode = ShowCursor;
// The VK will appear in a moment, stop the timer
m_hideCursorHandleTimer.stop();
- finishComposingText();
+ focusObjectStopComposing();
updateSelectionHandles();
}
}
@@ -789,13 +804,19 @@ void QAndroidInputContext::longPress(int x, int y)
return;
if (m_focusObject && inputItemRectangle().contains(x, y)) {
- finishComposingText();
+ BatchEditLock batchEditLock(this);
+
+ focusObjectStopComposing();
// Release left button, otherwise the following events will cancel the menu popup
QtAndroidInput::releaseMouse(x, y);
- handleLocationChanged(1, x, y);
- ScopedValueChangeBack<bool> svcb(m_blockUpdateSelection, true);
+ const double pixelDensity =
+ QGuiApplication::focusWindow()
+ ? QHighDpiScaling::factor(QGuiApplication::focusWindow())
+ : QHighDpiScaling::factor(QtAndroid::androidPlatformIntegration()->screen());
+ const QPointF touchPoint(x / pixelDensity, y / pixelDensity);
+ setSelectionOnFocusObject(touchPoint, touchPoint);
QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition | Qt::ImTextBeforeCursor | Qt::ImTextAfterCursor);
QCoreApplication::sendEvent(m_focusObject, &query);
@@ -934,6 +955,7 @@ void QAndroidInputContext::clear()
{
m_composingText.clear();
m_composingTextStart = -1;
+ m_composingCursor = -1;
m_extractedText.clear();
}
@@ -941,9 +963,8 @@ void QAndroidInputContext::clear()
void QAndroidInputContext::setFocusObject(QObject *object)
{
if (object != m_focusObject) {
+ focusObjectStopComposing();
m_focusObject = object;
- if (!m_composingText.isEmpty())
- finishComposingText();
reset();
}
QPlatformInputContext::setFocusObject(object);
@@ -958,78 +979,135 @@ jboolean QAndroidInputContext::beginBatchEdit()
jboolean QAndroidInputContext::endBatchEdit()
{
- if (--m_batchEditNestingLevel == 0 && !m_blockUpdateSelection) //ending batch edit mode
+ if (--m_batchEditNestingLevel == 0) { //ending batch edit mode
+ focusObjectStartComposing();
updateCursorPosition();
+ }
return JNI_TRUE;
}
/*
- Android docs say: If composing, replace compose text with \a text.
- Otherwise insert \a text at current cursor position.
-
- The cursor should then be moved to newCursorPosition. If > 0, this is
- relative to the end of the text - 1; if <= 0, this is relative to the start
- of the text. updateSelection() needs to be called.
+ Android docs say: This behaves like calling setComposingText(text, newCursorPosition) then
+ finishComposingText().
*/
jboolean QAndroidInputContext::commitText(const QString &text, jint newCursorPosition)
{
- ScopedValueChangeBack<bool> svcb(m_blockUpdateSelection, true);
- QInputMethodEvent event;
- event.setCommitString(text);
- sendInputMethodEvent(&event);
- clear();
-
- // Qt has now put the cursor at the end of the text, corresponding to newCursorPosition == 1
- if (newCursorPosition != 1) {
- QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
- if (!query.isNull()) {
- QList<QInputMethodEvent::Attribute> attributes;
- const int localPos = query->value(Qt::ImCursorPosition).toInt();
- const int newLocalPos = newCursorPosition > 0
- ? localPos + newCursorPosition - 1
- : localPos - text.length() + newCursorPosition;
- //move the cursor
- attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection,
- newLocalPos, 0));
- }
- }
- svcb.setOldValue();
- updateCursorPosition();
- return JNI_TRUE;
+ BatchEditLock batchEditLock(this);
+ return setComposingText(text, newCursorPosition) && finishComposingText();
}
jboolean QAndroidInputContext::deleteSurroundingText(jint leftLength, jint rightLength)
{
+ BatchEditLock batchEditLock(this);
+
+ focusObjectStopComposing();
+
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
if (query.isNull())
return JNI_TRUE;
- m_composingText.clear();
- m_composingTextStart = -1;
-
if (leftLength < 0) {
rightLength += -leftLength;
leftLength = 0;
}
+ const int initialBlockPos = getBlockPosition(query);
+ const int initialCursorPos = getAbsoluteCursorPosition(query);
+ const int initialAnchorPos = initialBlockPos + query->value(Qt::ImAnchorPosition).toInt();
+
+ /*
+ According to documentation, we should delete leftLength characters before current selection
+ and rightLength characters after current selection (without affecting selection). But that is
+ absolutely not what Android's native EditText does. It deletes leftLength characters before
+ min(selection start, composing region start) and rightLength characters after max(selection
+ end, composing region end). There are no known keyboards that depend on this behavior, but
+ it is better to be consistent with EditText behavior, because there definetly should be no
+ keyboards that depend on documented behavior.
+ */
+ const int leftEnd =
+ m_composingText.isEmpty()
+ ? qMin(initialCursorPos, initialAnchorPos)
+ : qMin(qMin(initialCursorPos, initialAnchorPos), m_composingTextStart);
+
+ const int rightBegin =
+ m_composingText.isEmpty()
+ ? qMax(initialCursorPos, initialAnchorPos)
+ : qMax(qMax(initialCursorPos, initialAnchorPos),
+ m_composingTextStart + m_composingText.length());
+
+ int textBeforeCursorLen;
+ int textAfterCursorLen;
+
QVariant textBeforeCursor = query->value(Qt::ImTextBeforeCursor);
QVariant textAfterCursor = query->value(Qt::ImTextAfterCursor);
if (textBeforeCursor.isValid() && textAfterCursor.isValid()) {
- leftLength = qMin(leftLength, textBeforeCursor.toString().length());
- rightLength = qMin(rightLength, textAfterCursor.toString().length());
+ textBeforeCursorLen = textBeforeCursor.toString().length();
+ textAfterCursorLen = textAfterCursor.toString().length();
} else {
- int cursorPos = query->value(Qt::ImCursorPosition).toInt();
- leftLength = qMin(leftLength, cursorPos);
- rightLength = qMin(rightLength, query->value(Qt::ImSurroundingText).toString().length() - cursorPos);
+ textBeforeCursorLen = initialCursorPos - initialBlockPos;
+ textAfterCursorLen =
+ query->value(Qt::ImSurroundingText).toString().length() - textBeforeCursorLen;
}
+ leftLength = qMin(qMax(0, textBeforeCursorLen - (initialCursorPos - leftEnd)), leftLength);
+ rightLength = qMin(qMax(0, textAfterCursorLen - (rightBegin - initialCursorPos)), rightLength);
+
if (leftLength == 0 && rightLength == 0)
return JNI_TRUE;
- QInputMethodEvent event;
- event.setCommitString(QString(), -leftLength, leftLength+rightLength);
- sendInputMethodEvent(&event);
- clear();
+ if (leftEnd == rightBegin) {
+ // We have no selection and no composing region; we can do everything using one event
+ QInputMethodEvent event;
+ event.setCommitString({}, -leftLength, leftLength + rightLength);
+ QGuiApplication::sendEvent(m_focusObject, &event);
+ } else {
+ if (initialCursorPos != initialAnchorPos) {
+ QInputMethodEvent event({}, {
+ { QInputMethodEvent::Selection, initialCursorPos - initialBlockPos, 0 }
+ });
+
+ QGuiApplication::sendEvent(m_focusObject, &event);
+ }
+
+ int currentCursorPos = initialCursorPos;
+
+ if (rightLength > 0) {
+ QInputMethodEvent event;
+ event.setCommitString({}, rightBegin - currentCursorPos, rightLength);
+ QGuiApplication::sendEvent(m_focusObject, &event);
+
+ currentCursorPos = rightBegin;
+ }
+
+ if (leftLength > 0) {
+ const int leftBegin = leftEnd - leftLength;
+
+ QInputMethodEvent event;
+ event.setCommitString({}, leftBegin - currentCursorPos, leftLength);
+ QGuiApplication::sendEvent(m_focusObject, &event);
+
+ currentCursorPos = leftBegin;
+
+ if (!m_composingText.isEmpty())
+ m_composingTextStart -= leftLength;
+ }
+
+ // Restore cursor position or selection
+ if (currentCursorPos != initialCursorPos - leftLength
+ || initialCursorPos != initialAnchorPos) {
+ // If we have deleted a newline character, we are now in a new block
+ const int currentBlockPos = getBlockPosition(
+ focusObjectInputMethodQuery(Qt::ImAbsolutePosition | Qt::ImCursorPosition));
+
+ QInputMethodEvent event({}, {
+ { QInputMethodEvent::Selection, initialCursorPos - leftLength - currentBlockPos,
+ initialAnchorPos - initialCursorPos },
+ { QInputMethodEvent::Cursor, 0, 0 }
+ });
+
+ QGuiApplication::sendEvent(m_focusObject, &event);
+ }
+ }
return JNI_TRUE;
}
@@ -1037,16 +1115,70 @@ jboolean QAndroidInputContext::deleteSurroundingText(jint leftLength, jint right
// Android docs say the cursor must not move
jboolean QAndroidInputContext::finishComposingText()
{
- if (m_composingText.isEmpty())
- return JNI_TRUE; // not composing
+ BatchEditLock batchEditLock(this);
+
+ if (!focusObjectStopComposing())
+ return JNI_FALSE;
+
+ clear();
+ return JNI_TRUE;
+}
+
+bool QAndroidInputContext::focusObjectIsComposing() const
+{
+ return m_composingCursor != -1;
+}
+
+void QAndroidInputContext::focusObjectStartComposing()
+{
+ if (focusObjectIsComposing() || m_composingText.isEmpty())
+ return;
+
+ // Composing strings containing newline characters are rare and may cause problems
+ if (m_composingText.contains(QLatin1Char('\n')))
+ return;
+
+ QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
+ if (!query)
+ return;
+
+ if (query->value(Qt::ImCursorPosition).toInt() != query->value(Qt::ImAnchorPosition).toInt())
+ return;
+
+ const int absoluteCursorPos = getAbsoluteCursorPosition(query);
+ if (absoluteCursorPos < m_composingTextStart
+ || absoluteCursorPos > m_composingTextStart + m_composingText.length())
+ return;
+
+ m_composingCursor = absoluteCursorPos;
+
+ QTextCharFormat underlined;
+ underlined.setFontUnderline(true);
+
+ QInputMethodEvent event(m_composingText, {
+ { QInputMethodEvent::Cursor, absoluteCursorPos - m_composingTextStart, 1 },
+ { QInputMethodEvent::TextFormat, 0, m_composingText.length(), underlined }
+ });
+
+ event.setCommitString({}, m_composingTextStart - absoluteCursorPos, m_composingText.length());
+
+ QGuiApplication::sendEvent(m_focusObject, &event);
+}
+
+bool QAndroidInputContext::focusObjectStopComposing()
+{
+ if (!focusObjectIsComposing())
+ return true; // not composing
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
if (query.isNull())
- return JNI_FALSE;
+ return false;
const int blockPos = getBlockPosition(query);
const int localCursorPos = m_composingCursor - blockPos;
+ m_composingCursor = -1;
+
// Moving Qt's cursor to where the preedit cursor used to be
QList<QInputMethodEvent::Attribute> attributes;
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection, localCursorPos, 0));
@@ -1054,9 +1186,8 @@ jboolean QAndroidInputContext::finishComposingText()
QInputMethodEvent event(QString(), attributes);
event.setCommitString(m_composingText);
sendInputMethodEvent(&event);
- clear();
- return JNI_TRUE;
+ return true;
}
jint QAndroidInputContext::getCursorCapsMode(jint /*reqModes*/)
@@ -1096,52 +1227,51 @@ const QAndroidInputContext::ExtractedText &QAndroidInputContext::getExtractedTex
// updateExtractedText(View, int, ExtractedText) whenever you call
// updateSelection(View, int, int, int, int)." QTBUG-37980
- QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
+ QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(
+ Qt::ImCursorPosition | Qt::ImAbsolutePosition | Qt::ImAnchorPosition);
if (query.isNull())
return m_extractedText;
- int localPos = query->value(Qt::ImCursorPosition).toInt(); //position before pre-edit text relative to the current block
- int blockPos = getBlockPosition(query);
- QString blockText = query->value(Qt::ImSurroundingText).toString();
- int composeLength = m_composingText.length();
-
- if (composeLength > 0) {
- //Qt doesn't give us the preedit text, so we have to insert it at the correct position
- int localComposePos = m_composingTextStart - blockPos;
- blockText = blockText.leftRef(localComposePos) + m_composingText + blockText.midRef(localComposePos);
- }
-
- int cpos = localPos + composeLength; //actual cursor pos relative to the current block
-
- int localOffset = 0; // start of extracted text relative to the current block
- if (blockPos > 0) {
- QString prevBlockEnding = query->value(Qt::ImTextBeforeCursor).toString();
- prevBlockEnding.chop(localPos);
- if (prevBlockEnding.endsWith(QLatin1Char('\n'))) {
- localOffset = -qMin(20, prevBlockEnding.length());
- blockText = prevBlockEnding.right(-localOffset) + blockText;
- }
- }
+ const int cursorPos = getAbsoluteCursorPosition(query);
+ const int blockPos = getBlockPosition(query);
// It is documented that we should try to return hintMaxChars
- // characters, but that's not what the standard Android controls do, and
+ // characters, but standard Android controls always return all text, and
// there are input methods out there that (surprise) seem to depend on
// what happens in reality rather than what's documented.
- m_extractedText.text = blockText;
- m_extractedText.startOffset = blockPos + localOffset;
+ QVariant textBeforeCursor = QInputMethod::queryFocusObject(Qt::ImTextBeforeCursor, INT_MAX);
+ QVariant textAfterCursor = QInputMethod::queryFocusObject(Qt::ImTextAfterCursor, INT_MAX);
+ if (textBeforeCursor.isValid() && textAfterCursor.isValid()) {
+ if (focusObjectIsComposing()) {
+ m_extractedText.text =
+ textBeforeCursor.toString() + m_composingText + textAfterCursor.toString();
+ } else {
+ m_extractedText.text = textBeforeCursor.toString() + textAfterCursor.toString();
+ }
- const QString &selection = query->value(Qt::ImCurrentSelection).toString();
- const int selLen = selection.length();
- if (selLen) {
- m_extractedText.selectionStart = query->value(Qt::ImAnchorPosition).toInt() - localOffset;
- m_extractedText.selectionEnd = m_extractedText.selectionStart + selLen;
- } else if (composeLength > 0) {
+ m_extractedText.startOffset = qMax(0, cursorPos - textBeforeCursor.toString().length());
+ } else {
+ m_extractedText.text = focusObjectInputMethodQuery(Qt::ImSurroundingText)
+ ->value(Qt::ImSurroundingText).toString();
+
+ if (focusObjectIsComposing())
+ m_extractedText.text.insert(cursorPos - blockPos, m_composingText);
+
+ m_extractedText.startOffset = blockPos;
+ }
+
+ if (focusObjectIsComposing()) {
m_extractedText.selectionStart = m_composingCursor - m_extractedText.startOffset;
- m_extractedText.selectionEnd = m_composingCursor - m_extractedText.startOffset;
- } else {
- m_extractedText.selectionStart = cpos - localOffset;
- m_extractedText.selectionEnd = cpos - localOffset;
+ m_extractedText.selectionEnd = m_extractedText.selectionStart;
+ } else {
+ m_extractedText.selectionStart = cursorPos - m_extractedText.startOffset;
+ m_extractedText.selectionEnd =
+ blockPos + query->value(Qt::ImAnchorPosition).toInt() - m_extractedText.startOffset;
+
+ // Some keyboards misbehave when selectionStart > selectionEnd
+ if (m_extractedText.selectionStart > m_extractedText.selectionEnd)
+ std::swap(m_extractedText.selectionStart, m_extractedText.selectionEnd);
}
return m_extractedText;
@@ -1176,10 +1306,20 @@ QString QAndroidInputContext::getTextAfterCursor(jint length, jint /*flags*/)
}
}
- // Controls do not report preedit text, so we have to add it
- if (!m_composingText.isEmpty()) {
+ if (focusObjectIsComposing()) {
+ // Controls do not report preedit text, so we have to add it
const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart;
text = m_composingText.midRef(cursorPosInsidePreedit) + text;
+ } else {
+ // We must not return selected text if there is any
+ QSharedPointer<QInputMethodQueryEvent> query =
+ focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAnchorPosition);
+ if (query) {
+ const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
+ const int anchorPos = query->value(Qt::ImAnchorPosition).toInt();
+ if (anchorPos > cursorPos)
+ text.remove(0, anchorPos - cursorPos);
+ }
}
text.truncate(length);
@@ -1206,10 +1346,20 @@ QString QAndroidInputContext::getTextBeforeCursor(jint length, jint /*flags*/)
}
}
- // Controls do not report preedit text, so we have to add it
- if (!m_composingText.isEmpty()) {
+ if (focusObjectIsComposing()) {
+ // Controls do not report preedit text, so we have to add it
const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart;
text += m_composingText.leftRef(cursorPosInsidePreedit);
+ } else {
+ // We must not return selected text if there is any
+ QSharedPointer<QInputMethodQueryEvent> query =
+ focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAnchorPosition);
+ if (query) {
+ const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
+ const int anchorPos = query->value(Qt::ImAnchorPosition).toInt();
+ if (anchorPos < cursorPos)
+ text.chop(cursorPos - anchorPos);
+ }
}
if (text.length() > length)
@@ -1218,11 +1368,13 @@ QString QAndroidInputContext::getTextBeforeCursor(jint length, jint /*flags*/)
}
/*
- Android docs say that this function should remove the current preedit text
- if any, and replace it with the given text. Any selected text should be
- removed. The cursor is then moved to newCursorPosition. If > 0, this is
- relative to the end of the text - 1; if <= 0, this is relative to the start
- of the text.
+ Android docs say that this function should:
+ - remove the current composing text, if there is any
+ - otherwise remove currently selected text, if there is any
+ - insert new text in place of old composing text or, if there was none, at current cursor position
+ - mark the inserted text as composing
+ - move cursor as specified by newCursorPosition: if > 0, it is relative to the end of inserted
+ text - 1; if <= 0, it is relative to the start of inserted text
*/
jboolean QAndroidInputContext::setComposingText(const QString &text, jint newCursorPosition)
@@ -1231,47 +1383,110 @@ jboolean QAndroidInputContext::setComposingText(const QString &text, jint newCur
if (query.isNull())
return JNI_FALSE;
- const int cursorPos = getAbsoluteCursorPosition(query);
- if (newCursorPosition > 0)
- newCursorPosition += text.length() - 1;
+ BatchEditLock batchEditLock(this);
+ const int absoluteCursorPos = getAbsoluteCursorPosition(query);
+ int absoluteAnchorPos = getBlockPosition(query) + query->value(Qt::ImAnchorPosition).toInt();
+
+ // If we have composing region and selection (and therefore focusObjectIsComposing() == false),
+ // we must clear selection so that we won't delete it when we will be replacing composing text
+ if (!m_composingText.isEmpty() && absoluteCursorPos != absoluteAnchorPos) {
+ const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
+ QInputMethodEvent event({}, { { QInputMethodEvent::Selection, cursorPos, 0 } });
+ QGuiApplication::sendEvent(m_focusObject, &event);
+
+ absoluteAnchorPos = absoluteCursorPos;
+ }
+
+ // If we had no composing region, pretend that we had a zero-length composing region at current
+ // cursor position to simplify code. Also account for that we must delete selected text if there
+ // (still) is any.
+ const int effectiveAbsoluteCursorPos = qMin(absoluteCursorPos, absoluteAnchorPos);
+ if (m_composingTextStart == -1)
+ m_composingTextStart = effectiveAbsoluteCursorPos;
+
+ const int oldComposingTextLen = m_composingText.length();
m_composingText = text;
- m_composingTextStart = text.isEmpty() ? -1 : cursorPos;
- m_composingCursor = cursorPos + newCursorPosition;
- QList<QInputMethodEvent::Attribute> attributes;
- attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor,
- newCursorPosition,
- 1));
- // Show compose text underlined
- QTextCharFormat underlined;
- underlined.setFontUnderline(true);
- attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat,0, text.length(),
- QVariant(underlined)));
- QInputMethodEvent event(m_composingText, attributes);
- sendInputMethodEvent(&event);
+ const int newAbsoluteCursorPos =
+ newCursorPosition <= 0
+ ? m_composingTextStart + newCursorPosition
+ : m_composingTextStart + m_composingText.length() + newCursorPosition - 1;
- QMetaObject::invokeMethod(this, "keyDown");
+ const bool focusObjectWasComposing = focusObjectIsComposing();
- updateCursorPosition();
+ // Same checks as in focusObjectStartComposing()
+ if (!m_composingText.isEmpty() && !m_composingText.contains(QLatin1Char('\n'))
+ && newAbsoluteCursorPos >= m_composingTextStart
+ && newAbsoluteCursorPos <= m_composingTextStart + m_composingText.length())
+ m_composingCursor = newAbsoluteCursorPos;
+ else
+ m_composingCursor = -1;
+
+ QInputMethodEvent event;
+ if (focusObjectIsComposing()) {
+ QTextCharFormat underlined;
+ underlined.setFontUnderline(true);
+
+ event = QInputMethodEvent(m_composingText, {
+ { QInputMethodEvent::TextFormat, 0, m_composingText.length(), underlined },
+ { QInputMethodEvent::Cursor, m_composingCursor - m_composingTextStart, 1 }
+ });
+
+ if (oldComposingTextLen > 0 && !focusObjectWasComposing) {
+ event.setCommitString({}, m_composingTextStart - effectiveAbsoluteCursorPos,
+ oldComposingTextLen);
+ }
+ } else {
+ event = QInputMethodEvent({}, {});
+
+ if (focusObjectWasComposing) {
+ event.setCommitString(m_composingText);
+ } else {
+ event.setCommitString(m_composingText,
+ m_composingTextStart - effectiveAbsoluteCursorPos,
+ oldComposingTextLen);
+ }
+ }
+
+ if (m_composingText.isEmpty())
+ clear();
+
+ QGuiApplication::sendEvent(m_focusObject, &event);
+
+ if (!focusObjectIsComposing() && newCursorPosition != 1) {
+ // Move cursor using a separate event because if we have inserted or deleted a newline
+ // character, then we are now inside an another block
+
+ const int newBlockPos = getBlockPosition(
+ focusObjectInputMethodQuery(Qt::ImCursorPosition | Qt::ImAbsolutePosition));
+
+ event = QInputMethodEvent({}, {
+ { QInputMethodEvent::Selection, newAbsoluteCursorPos - newBlockPos, 0 }
+ });
+
+ QGuiApplication::sendEvent(m_focusObject, &event);
+ }
+
+ keyDown();
return JNI_TRUE;
}
// Android docs say:
// * start may be after end, same meaning as if swapped
-// * this function should not trigger updateSelection
+// * this function should not trigger updateSelection, but Android's native EditText does trigger it
// * if start == end then we should stop composing
jboolean QAndroidInputContext::setComposingRegion(jint start, jint end)
{
+ BatchEditLock batchEditLock(this);
+
// Qt will not include the current preedit text in the query results, and interprets all
// parameters relative to the text excluding the preedit. The simplest solution is therefore to
// tell Qt that we commit the text before we set the new region. This may cause a little flicker, but is
// much more robust than trying to keep the two different world views in sync
- bool wasComposing = !m_composingText.isEmpty();
- if (wasComposing)
- finishComposingText();
+ finishComposingText();
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
if (query.isNull())
@@ -1282,54 +1497,42 @@ jboolean QAndroidInputContext::setComposingRegion(jint start, jint end)
if (start > end)
qSwap(start, end);
- /*
- start and end are cursor positions, not character positions,
- i.e. selecting the first character is done by start == 0 and end == 1,
- and start == end means no character selected
-
- Therefore, the length of the region is end - start
- */
-
- int length = end - start;
- int localPos = query->value(Qt::ImCursorPosition).toInt();
- int blockPosition = getBlockPosition(query);
- int localStart = start - blockPosition; // Qt uses position inside block
- int currentCursor = wasComposing ? m_composingCursor : blockPosition + localPos;
-
- ScopedValueChangeBack<bool> svcb(m_blockUpdateSelection, true);
-
QString text = query->value(Qt::ImSurroundingText).toString();
+ int textOffset = getBlockPosition(query);
- m_composingText = text.mid(localStart, length);
- m_composingTextStart = start;
- m_composingCursor = currentCursor;
-
- //in the Qt text controls, the preedit is defined relative to the cursor position
- int relativeStart = localStart - localPos;
+ if (start < textOffset || end > textOffset + text.length()) {
+ const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
- QList<QInputMethodEvent::Attribute> attributes;
+ if (end - textOffset > text.length()) {
+ const QString after = query->value(Qt::ImTextAfterCursor).toString();
+ const int additionalSuffixLen = after.length() - (text.length() - cursorPos);
- // Show compose text underlined
- QTextCharFormat underlined;
- underlined.setFontUnderline(true);
- attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat,0, length,
- QVariant(underlined)));
+ if (additionalSuffixLen > 0)
+ text += after.rightRef(additionalSuffixLen);
+ }
- // Keep the cursor position unchanged (don't move to end of preedit)
- attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, currentCursor - start, 1));
+ if (start < textOffset) {
+ QString before = query->value(Qt::ImTextBeforeCursor).toString();
+ before.chop(cursorPos);
- QInputMethodEvent event(m_composingText, attributes);
- event.setCommitString(QString(), relativeStart, length);
- sendInputMethodEvent(&event);
+ if (!before.isEmpty()) {
+ text = before + text;
+ textOffset -= before.length();
+ }
+ }
+ if (start < textOffset || end - textOffset > text.length()) {
#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
- QSharedPointer<QInputMethodQueryEvent> query2 = focusObjectInputMethodQuery();
- if (!query2.isNull()) {
- qDebug() << "Setting. Prev local cpos:" << localPos << "block pos:" <<blockPosition << "comp.start:" << m_composingTextStart << "rel.start:" << relativeStart << "len:" << length << "cpos attr:" << localPos - localStart;
- qDebug() << "New cursor pos" << getAbsoluteCursorPosition(query2);
- }
+ qWarning("setComposingRegion: failed to retrieve text from composing region");
#endif
+ return JNI_TRUE;
+ }
+ }
+
+ m_composingText = text.mid(start - textOffset, end - start);
+ m_composingTextStart = start;
+
return JNI_TRUE;
}
@@ -1339,15 +1542,18 @@ jboolean QAndroidInputContext::setSelection(jint start, jint end)
if (query.isNull())
return JNI_FALSE;
+ BatchEditLock batchEditLock(this);
+
int blockPosition = getBlockPosition(query);
int localCursorPos = start - blockPosition;
- QList<QInputMethodEvent::Attribute> attributes;
- if (!m_composingText.isEmpty() && start == end) {
+ if (focusObjectIsComposing() && start == end && start >= m_composingTextStart
+ && start <= m_composingTextStart + m_composingText.length()) {
// not actually changing the selection; just moving the
// preedit cursor
int localOldPos = query->value(Qt::ImCursorPosition).toInt();
int pos = localCursorPos - localOldPos;
+ QList<QInputMethodEvent::Attribute> attributes;
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, pos, 1));
//but we have to tell Qt about the compose text all over again
@@ -1359,21 +1565,26 @@ jboolean QAndroidInputContext::setSelection(jint start, jint end)
QVariant(underlined)));
m_composingCursor = start;
+ QInputMethodEvent event(m_composingText, attributes);
+ QGuiApplication::sendEvent(m_focusObject, &event);
} else {
// actually changing the selection
+ focusObjectStopComposing();
+ QList<QInputMethodEvent::Attribute> attributes;
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection,
localCursorPos,
end - start));
+ QInputMethodEvent event({}, attributes);
+ QGuiApplication::sendEvent(m_focusObject, &event);
}
- QInputMethodEvent event(m_composingText, attributes);
- sendInputMethodEvent(&event);
- updateCursorPosition();
return JNI_TRUE;
}
jboolean QAndroidInputContext::selectAll()
{
- finishComposingText();
+ BatchEditLock batchEditLock(this);
+
+ focusObjectStopComposing();
m_handleMode = ShowCursor;
sendShortcut(QKeySequence::SelectAll);
return JNI_TRUE;
@@ -1381,7 +1592,12 @@ jboolean QAndroidInputContext::selectAll()
jboolean QAndroidInputContext::cut()
{
+ BatchEditLock batchEditLock(this);
+
+ // This is probably not what native EditText would do, but normally if there is selection, then
+ // there will be no composing region
finishComposingText();
+
m_handleMode = ShowCursor;
sendShortcut(QKeySequence::Cut);
return JNI_TRUE;
@@ -1389,7 +1605,9 @@ jboolean QAndroidInputContext::cut()
jboolean QAndroidInputContext::copy()
{
- finishComposingText();
+ BatchEditLock batchEditLock(this);
+
+ focusObjectStopComposing();
m_handleMode = ShowCursor;
sendShortcut(QKeySequence::Copy);
return JNI_TRUE;
@@ -1403,7 +1621,11 @@ jboolean QAndroidInputContext::copyURL()
jboolean QAndroidInputContext::paste()
{
+ BatchEditLock batchEditLock(this);
+
+ // TODO: This is not what native EditText does
finishComposingText();
+
m_handleMode = ShowCursor;
sendShortcut(QKeySequence::Paste);
return JNI_TRUE;
@@ -1415,8 +1637,12 @@ void QAndroidInputContext::sendShortcut(const QKeySequence &sequence)
const int keys = sequence[i];
Qt::Key key = Qt::Key(keys & ~Qt::KeyboardModifierMask);
Qt::KeyboardModifiers mod = Qt::KeyboardModifiers(keys & Qt::KeyboardModifierMask);
- QGuiApplication::postEvent(m_focusObject, new QKeyEvent(QEvent::KeyPress, key, mod));
- QGuiApplication::postEvent(m_focusObject, new QKeyEvent(QEvent::KeyRelease, key, mod));
+
+ QKeyEvent pressEvent(QEvent::KeyPress, key, mod);
+ QKeyEvent releaseEvent(QEvent::KeyRelease, key, mod);
+
+ QGuiApplication::sendEvent(m_focusObject, &pressEvent);
+ QGuiApplication::sendEvent(m_focusObject, &releaseEvent);
}
}
diff --git a/src/plugins/platforms/android/qandroidinputcontext.h b/src/plugins/platforms/android/qandroidinputcontext.h
index bd3edb30f0..e9bfb98e66 100644
--- a/src/plugins/platforms/android/qandroidinputcontext.h
+++ b/src/plugins/platforms/android/qandroidinputcontext.h
@@ -151,6 +151,9 @@ private slots:
private:
void sendInputMethodEvent(QInputMethodEvent *event);
QSharedPointer<QInputMethodQueryEvent> focusObjectInputMethodQuery(Qt::InputMethodQueries queries = Qt::ImQueryAll);
+ bool focusObjectIsComposing() const;
+ void focusObjectStartComposing();
+ bool focusObjectStopComposing();
private:
ExtractedText m_extractedText;
@@ -158,9 +161,8 @@ private:
int m_composingTextStart;
int m_composingCursor;
QMetaObject::Connection m_updateCursorPosConnection;
- bool m_blockUpdateSelection;
HandleModes m_handleMode;
- QAtomicInt m_batchEditNestingLevel;
+ int m_batchEditNestingLevel;
QObject *m_focusObject;
QTimer m_hideCursorHandleTimer;
};