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>
This commit is contained in:
Vova Mshanetskiy 2019-05-31 16:25:14 +03:00
parent e5f2be256f
commit 1ade5ea41a
2 changed files with 430 additions and 202 deletions

View File

@ -64,29 +64,33 @@
QT_BEGIN_NAMESPACE QT_BEGIN_NAMESPACE
template <typename T> namespace {
class ScopedValueChangeBack
class BatchEditLock
{ {
public: public:
ScopedValueChangeBack(T &variable, T newValue)
: m_oldValue(variable), explicit BatchEditLock(QAndroidInputContext *context)
m_variable(variable) : m_context(context)
{ {
m_variable = newValue; m_context->beginBatchEdit();
} }
inline void setOldValue()
~BatchEditLock()
{ {
m_variable = m_oldValue; m_context->endBatchEdit();
}
~ScopedValueChangeBack()
{
setOldValue();
} }
BatchEditLock(const BatchEditLock &) = delete;
BatchEditLock &operator=(const BatchEditLock &) = delete;
private: private:
T m_oldValue;
T &m_variable; QAndroidInputContext *m_context;
}; };
} // namespace anonymous
static QAndroidInputContext *m_androidInputContext = 0; static QAndroidInputContext *m_androidInputContext = 0;
static char const *const QtNativeInputConnectionClassName = "org/qtproject/qt5/android/QtNativeInputConnection"; static char const *const QtNativeInputConnectionClassName = "org/qtproject/qt5/android/QtNativeInputConnection";
static char const *const QtExtractedTextClassName = "org/qtproject/qt5/android/QtExtractedText"; static char const *const QtExtractedTextClassName = "org/qtproject/qt5/android/QtExtractedText";
@ -423,8 +427,12 @@ static QRect inputItemRectangle()
} }
QAndroidInputContext::QAndroidInputContext() QAndroidInputContext::QAndroidInputContext()
: QPlatformInputContext(), m_composingTextStart(-1), m_blockUpdateSelection(false), : QPlatformInputContext()
m_handleMode(Hidden), m_batchEditNestingLevel(0), m_focusObject(0) , m_composingTextStart(-1)
, m_composingCursor(-1)
, m_handleMode(Hidden)
, m_batchEditNestingLevel(0)
, m_focusObject(0)
{ {
jclass clazz = QJNIEnvironmentPrivate::findClass(QtNativeInputConnectionClassName); jclass clazz = QJNIEnvironmentPrivate::findClass(QtNativeInputConnectionClassName);
if (Q_UNLIKELY(!clazz)) { if (Q_UNLIKELY(!clazz)) {
@ -565,13 +573,13 @@ void QAndroidInputContext::reset()
void QAndroidInputContext::commit() void QAndroidInputContext::commit()
{ {
finishComposingText(); focusObjectStopComposing();
} }
void QAndroidInputContext::updateCursorPosition() void QAndroidInputContext::updateCursorPosition()
{ {
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(); QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
if (!query.isNull() && !m_blockUpdateSelection && !m_batchEditNestingLevel) { if (!query.isNull() && m_batchEditNestingLevel == 0) {
const int cursorPos = getAbsoluteCursorPosition(query); const int cursorPos = getAbsoluteCursorPosition(query);
const int composeLength = m_composingText.length(); const int composeLength = m_composingText.length();
@ -579,24 +587,29 @@ void QAndroidInputContext::updateCursorPosition()
if (m_composingText.isEmpty() != (m_composingTextStart == -1)) if (m_composingText.isEmpty() != (m_composingTextStart == -1))
qWarning() << "Input method out of sync" << m_composingText << m_composingTextStart; qWarning() << "Input method out of sync" << m_composingText << m_composingTextStart;
int realCursorPosition = cursorPos; int realSelectionStart = cursorPos;
int realAnchorPosition = cursorPos; int realSelectionEnd = cursorPos;
int cpos = query->value(Qt::ImCursorPosition).toInt(); int cpos = query->value(Qt::ImCursorPosition).toInt();
int anchor = query->value(Qt::ImAnchorPosition).toInt(); int anchor = query->value(Qt::ImAnchorPosition).toInt();
if (cpos != anchor) { if (cpos != anchor) {
if (!m_composingText.isEmpty()) { if (!m_composingText.isEmpty()) {
qWarning("Selecting text while preediting may give unpredictable results."); qWarning("Selecting text while preediting may give unpredictable results.");
finishComposingText(); focusObjectStopComposing();
} }
int blockPos = getBlockPosition(query); int blockPos = getBlockPosition(query);
realCursorPosition = blockPos + cpos; realSelectionStart = blockPos + cpos;
realAnchorPosition = blockPos + anchor; 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 // 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()) if (focusObjectIsComposing())
realCursorPosition = realAnchorPosition = m_composingCursor; realSelectionStart = realSelectionEnd = m_composingCursor;
QtAndroidInput::updateSelection(realCursorPosition, realAnchorPosition,
// 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 m_composingTextStart, m_composingTextStart + composeLength); // pre-edit text
} }
} }
@ -666,7 +679,7 @@ void QAndroidInputContext::updateSelectionHandles()
*/ */
void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y) void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y)
{ {
if (m_batchEditNestingLevel.load() || m_blockUpdateSelection) { if (m_batchEditNestingLevel != 0) {
qWarning() << "QAndroidInputContext::handleLocationChanged returned"; qWarning() << "QAndroidInputContext::handleLocationChanged returned";
return; return;
} }
@ -741,15 +754,15 @@ void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y)
} }
// Check if handle has been dragged far enough // Check if handle has been dragged far enough
if (m_composingText.isEmpty() && newCpos == cpos && newAnchor == anchor) if (!focusObjectIsComposing() && newCpos == cpos && newAnchor == anchor)
return; return;
/* /*
If there is composing text, we have to compare newCpos with m_composingCursor instead of cpos. If the editor is currently in composing state, we have to compare newCpos with
And since there is nothing to compare with newAnchor, we perform the check only when user m_composingCursor instead of cpos. And since there is nothing to compare with newAnchor, we
drags the cursor handle. 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); int absoluteCpos = query.value(Qt::ImAbsolutePosition).toInt(&ok);
if (!ok) if (!ok)
absoluteCpos = cpos; absoluteCpos = cpos;
@ -759,7 +772,9 @@ void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y)
return; return;
} }
finishComposingText(); BatchEditLock batchEditLock(this);
focusObjectStopComposing();
QList<QInputMethodEvent::Attribute> attributes; QList<QInputMethodEvent::Attribute> attributes;
attributes.append({ QInputMethodEvent::Selection, newAnchor, newCpos - newAnchor }); attributes.append({ QInputMethodEvent::Selection, newAnchor, newCpos - newAnchor });
@ -777,7 +792,7 @@ void QAndroidInputContext::touchDown(int x, int y)
m_handleMode = ShowCursor; m_handleMode = ShowCursor;
// The VK will appear in a moment, stop the timer // The VK will appear in a moment, stop the timer
m_hideCursorHandleTimer.stop(); m_hideCursorHandleTimer.stop();
finishComposingText(); focusObjectStopComposing();
updateSelectionHandles(); updateSelectionHandles();
} }
} }
@ -789,13 +804,19 @@ void QAndroidInputContext::longPress(int x, int y)
return; return;
if (m_focusObject && inputItemRectangle().contains(x, y)) { if (m_focusObject && inputItemRectangle().contains(x, y)) {
finishComposingText(); BatchEditLock batchEditLock(this);
focusObjectStopComposing();
// Release left button, otherwise the following events will cancel the menu popup // Release left button, otherwise the following events will cancel the menu popup
QtAndroidInput::releaseMouse(x, y); QtAndroidInput::releaseMouse(x, y);
handleLocationChanged(1, x, y); const double pixelDensity =
ScopedValueChangeBack<bool> svcb(m_blockUpdateSelection, true); 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); QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition | Qt::ImTextBeforeCursor | Qt::ImTextAfterCursor);
QCoreApplication::sendEvent(m_focusObject, &query); QCoreApplication::sendEvent(m_focusObject, &query);
@ -934,6 +955,7 @@ void QAndroidInputContext::clear()
{ {
m_composingText.clear(); m_composingText.clear();
m_composingTextStart = -1; m_composingTextStart = -1;
m_composingCursor = -1;
m_extractedText.clear(); m_extractedText.clear();
} }
@ -941,9 +963,8 @@ void QAndroidInputContext::clear()
void QAndroidInputContext::setFocusObject(QObject *object) void QAndroidInputContext::setFocusObject(QObject *object)
{ {
if (object != m_focusObject) { if (object != m_focusObject) {
focusObjectStopComposing();
m_focusObject = object; m_focusObject = object;
if (!m_composingText.isEmpty())
finishComposingText();
reset(); reset();
} }
QPlatformInputContext::setFocusObject(object); QPlatformInputContext::setFocusObject(object);
@ -958,78 +979,135 @@ jboolean QAndroidInputContext::beginBatchEdit()
jboolean QAndroidInputContext::endBatchEdit() jboolean QAndroidInputContext::endBatchEdit()
{ {
if (--m_batchEditNestingLevel == 0 && !m_blockUpdateSelection) //ending batch edit mode if (--m_batchEditNestingLevel == 0) { //ending batch edit mode
focusObjectStartComposing();
updateCursorPosition(); updateCursorPosition();
}
return JNI_TRUE; return JNI_TRUE;
} }
/* /*
Android docs say: If composing, replace compose text with \a text. Android docs say: This behaves like calling setComposingText(text, newCursorPosition) then
Otherwise insert \a text at current cursor position. finishComposingText().
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.
*/ */
jboolean QAndroidInputContext::commitText(const QString &text, jint newCursorPosition) jboolean QAndroidInputContext::commitText(const QString &text, jint newCursorPosition)
{ {
ScopedValueChangeBack<bool> svcb(m_blockUpdateSelection, true); BatchEditLock batchEditLock(this);
QInputMethodEvent event; return setComposingText(text, newCursorPosition) && finishComposingText();
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;
} }
jboolean QAndroidInputContext::deleteSurroundingText(jint leftLength, jint rightLength) jboolean QAndroidInputContext::deleteSurroundingText(jint leftLength, jint rightLength)
{ {
BatchEditLock batchEditLock(this);
focusObjectStopComposing();
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(); QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
if (query.isNull()) if (query.isNull())
return JNI_TRUE; return JNI_TRUE;
m_composingText.clear();
m_composingTextStart = -1;
if (leftLength < 0) { if (leftLength < 0) {
rightLength += -leftLength; rightLength += -leftLength;
leftLength = 0; 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 textBeforeCursor = query->value(Qt::ImTextBeforeCursor);
QVariant textAfterCursor = query->value(Qt::ImTextAfterCursor); QVariant textAfterCursor = query->value(Qt::ImTextAfterCursor);
if (textBeforeCursor.isValid() && textAfterCursor.isValid()) { if (textBeforeCursor.isValid() && textAfterCursor.isValid()) {
leftLength = qMin(leftLength, textBeforeCursor.toString().length()); textBeforeCursorLen = textBeforeCursor.toString().length();
rightLength = qMin(rightLength, textAfterCursor.toString().length()); textAfterCursorLen = textAfterCursor.toString().length();
} else { } else {
int cursorPos = query->value(Qt::ImCursorPosition).toInt(); textBeforeCursorLen = initialCursorPos - initialBlockPos;
leftLength = qMin(leftLength, cursorPos); textAfterCursorLen =
rightLength = qMin(rightLength, query->value(Qt::ImSurroundingText).toString().length() - cursorPos); 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) if (leftLength == 0 && rightLength == 0)
return JNI_TRUE; return JNI_TRUE;
QInputMethodEvent event; if (leftEnd == rightBegin) {
event.setCommitString(QString(), -leftLength, leftLength+rightLength); // We have no selection and no composing region; we can do everything using one event
sendInputMethodEvent(&event); QInputMethodEvent event;
clear(); 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; return JNI_TRUE;
} }
@ -1037,16 +1115,70 @@ jboolean QAndroidInputContext::deleteSurroundingText(jint leftLength, jint right
// Android docs say the cursor must not move // Android docs say the cursor must not move
jboolean QAndroidInputContext::finishComposingText() jboolean QAndroidInputContext::finishComposingText()
{ {
if (m_composingText.isEmpty()) BatchEditLock batchEditLock(this);
return JNI_TRUE; // not composing
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(); QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
if (query.isNull()) if (query.isNull())
return JNI_FALSE; return false;
const int blockPos = getBlockPosition(query); const int blockPos = getBlockPosition(query);
const int localCursorPos = m_composingCursor - blockPos; const int localCursorPos = m_composingCursor - blockPos;
m_composingCursor = -1;
// Moving Qt's cursor to where the preedit cursor used to be // Moving Qt's cursor to where the preedit cursor used to be
QList<QInputMethodEvent::Attribute> attributes; QList<QInputMethodEvent::Attribute> attributes;
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection, localCursorPos, 0)); attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection, localCursorPos, 0));
@ -1054,9 +1186,8 @@ jboolean QAndroidInputContext::finishComposingText()
QInputMethodEvent event(QString(), attributes); QInputMethodEvent event(QString(), attributes);
event.setCommitString(m_composingText); event.setCommitString(m_composingText);
sendInputMethodEvent(&event); sendInputMethodEvent(&event);
clear();
return JNI_TRUE; return true;
} }
jint QAndroidInputContext::getCursorCapsMode(jint /*reqModes*/) jint QAndroidInputContext::getCursorCapsMode(jint /*reqModes*/)
@ -1096,52 +1227,51 @@ const QAndroidInputContext::ExtractedText &QAndroidInputContext::getExtractedTex
// updateExtractedText(View, int, ExtractedText) whenever you call // updateExtractedText(View, int, ExtractedText) whenever you call
// updateSelection(View, int, int, int, int)." QTBUG-37980 // 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()) if (query.isNull())
return m_extractedText; return m_extractedText;
int localPos = query->value(Qt::ImCursorPosition).toInt(); //position before pre-edit text relative to the current block const int cursorPos = getAbsoluteCursorPosition(query);
int blockPos = getBlockPosition(query); const 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;
}
}
// It is documented that we should try to return hintMaxChars // 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 // there are input methods out there that (surprise) seem to depend on
// what happens in reality rather than what's documented. // what happens in reality rather than what's documented.
m_extractedText.text = blockText; QVariant textBeforeCursor = QInputMethod::queryFocusObject(Qt::ImTextBeforeCursor, INT_MAX);
m_extractedText.startOffset = blockPos + localOffset; 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(); m_extractedText.startOffset = qMax(0, cursorPos - textBeforeCursor.toString().length());
const int selLen = selection.length(); } else {
if (selLen) { m_extractedText.text = focusObjectInputMethodQuery(Qt::ImSurroundingText)
m_extractedText.selectionStart = query->value(Qt::ImAnchorPosition).toInt() - localOffset; ->value(Qt::ImSurroundingText).toString();
m_extractedText.selectionEnd = m_extractedText.selectionStart + selLen;
} else if (composeLength > 0) { 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.selectionStart = m_composingCursor - m_extractedText.startOffset;
m_extractedText.selectionEnd = m_composingCursor - m_extractedText.startOffset; m_extractedText.selectionEnd = m_extractedText.selectionStart;
} else { } else {
m_extractedText.selectionStart = cpos - localOffset; m_extractedText.selectionStart = cursorPos - m_extractedText.startOffset;
m_extractedText.selectionEnd = cpos - localOffset; 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; 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 (focusObjectIsComposing()) {
if (!m_composingText.isEmpty()) { // Controls do not report preedit text, so we have to add it
const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart; const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart;
text = m_composingText.midRef(cursorPosInsidePreedit) + text; 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); 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 (focusObjectIsComposing()) {
if (!m_composingText.isEmpty()) { // Controls do not report preedit text, so we have to add it
const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart; const int cursorPosInsidePreedit = m_composingCursor - m_composingTextStart;
text += m_composingText.leftRef(cursorPosInsidePreedit); 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) 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 Android docs say that this function should:
if any, and replace it with the given text. Any selected text should be - remove the current composing text, if there is any
removed. The cursor is then moved to newCursorPosition. If > 0, this is - otherwise remove currently selected text, if there is any
relative to the end of the text - 1; if <= 0, this is relative to the start - insert new text in place of old composing text or, if there was none, at current cursor position
of the text. - 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) jboolean QAndroidInputContext::setComposingText(const QString &text, jint newCursorPosition)
@ -1231,47 +1383,110 @@ jboolean QAndroidInputContext::setComposingText(const QString &text, jint newCur
if (query.isNull()) if (query.isNull())
return JNI_FALSE; return JNI_FALSE;
const int cursorPos = getAbsoluteCursorPosition(query); BatchEditLock batchEditLock(this);
if (newCursorPosition > 0)
newCursorPosition += text.length() - 1;
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_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); const int newAbsoluteCursorPos =
sendInputMethodEvent(&event); 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; return JNI_TRUE;
} }
// Android docs say: // Android docs say:
// * start may be after end, same meaning as if swapped // * 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 // * if start == end then we should stop composing
jboolean QAndroidInputContext::setComposingRegion(jint start, jint end) 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 // 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 // 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 // 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 // much more robust than trying to keep the two different world views in sync
bool wasComposing = !m_composingText.isEmpty(); finishComposingText();
if (wasComposing)
finishComposingText();
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(); QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
if (query.isNull()) if (query.isNull())
@ -1282,54 +1497,42 @@ jboolean QAndroidInputContext::setComposingRegion(jint start, jint end)
if (start > end) if (start > end)
qSwap(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(); QString text = query->value(Qt::ImSurroundingText).toString();
int textOffset = getBlockPosition(query);
m_composingText = text.mid(localStart, length); if (start < textOffset || end > textOffset + text.length()) {
m_composingTextStart = start; const int cursorPos = query->value(Qt::ImCursorPosition).toInt();
m_composingCursor = currentCursor;
//in the Qt text controls, the preedit is defined relative to the cursor position if (end - textOffset > text.length()) {
int relativeStart = localStart - localPos; const QString after = query->value(Qt::ImTextAfterCursor).toString();
const int additionalSuffixLen = after.length() - (text.length() - cursorPos);
QList<QInputMethodEvent::Attribute> attributes; if (additionalSuffixLen > 0)
text += after.rightRef(additionalSuffixLen);
}
// Show compose text underlined if (start < textOffset) {
QTextCharFormat underlined; QString before = query->value(Qt::ImTextBeforeCursor).toString();
underlined.setFontUnderline(true); before.chop(cursorPos);
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat,0, length,
QVariant(underlined)));
// Keep the cursor position unchanged (don't move to end of preedit) if (!before.isEmpty()) {
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, currentCursor - start, 1)); text = before + text;
textOffset -= before.length();
QInputMethodEvent event(m_composingText, attributes); }
event.setCommitString(QString(), relativeStart, length); }
sendInputMethodEvent(&event);
if (start < textOffset || end - textOffset > text.length()) {
#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL
QSharedPointer<QInputMethodQueryEvent> query2 = focusObjectInputMethodQuery(); qWarning("setComposingRegion: failed to retrieve text from composing region");
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);
}
#endif #endif
return JNI_TRUE;
}
}
m_composingText = text.mid(start - textOffset, end - start);
m_composingTextStart = start;
return JNI_TRUE; return JNI_TRUE;
} }
@ -1339,15 +1542,18 @@ jboolean QAndroidInputContext::setSelection(jint start, jint end)
if (query.isNull()) if (query.isNull())
return JNI_FALSE; return JNI_FALSE;
BatchEditLock batchEditLock(this);
int blockPosition = getBlockPosition(query); int blockPosition = getBlockPosition(query);
int localCursorPos = start - blockPosition; int localCursorPos = start - blockPosition;
QList<QInputMethodEvent::Attribute> attributes; if (focusObjectIsComposing() && start == end && start >= m_composingTextStart
if (!m_composingText.isEmpty() && start == end) { && start <= m_composingTextStart + m_composingText.length()) {
// not actually changing the selection; just moving the // not actually changing the selection; just moving the
// preedit cursor // preedit cursor
int localOldPos = query->value(Qt::ImCursorPosition).toInt(); int localOldPos = query->value(Qt::ImCursorPosition).toInt();
int pos = localCursorPos - localOldPos; int pos = localCursorPos - localOldPos;
QList<QInputMethodEvent::Attribute> attributes;
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, pos, 1)); attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, pos, 1));
//but we have to tell Qt about the compose text all over again //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))); QVariant(underlined)));
m_composingCursor = start; m_composingCursor = start;
QInputMethodEvent event(m_composingText, attributes);
QGuiApplication::sendEvent(m_focusObject, &event);
} else { } else {
// actually changing the selection // actually changing the selection
focusObjectStopComposing();
QList<QInputMethodEvent::Attribute> attributes;
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection, attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection,
localCursorPos, localCursorPos,
end - start)); end - start));
QInputMethodEvent event({}, attributes);
QGuiApplication::sendEvent(m_focusObject, &event);
} }
QInputMethodEvent event(m_composingText, attributes);
sendInputMethodEvent(&event);
updateCursorPosition();
return JNI_TRUE; return JNI_TRUE;
} }
jboolean QAndroidInputContext::selectAll() jboolean QAndroidInputContext::selectAll()
{ {
finishComposingText(); BatchEditLock batchEditLock(this);
focusObjectStopComposing();
m_handleMode = ShowCursor; m_handleMode = ShowCursor;
sendShortcut(QKeySequence::SelectAll); sendShortcut(QKeySequence::SelectAll);
return JNI_TRUE; return JNI_TRUE;
@ -1381,7 +1592,12 @@ jboolean QAndroidInputContext::selectAll()
jboolean QAndroidInputContext::cut() 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(); finishComposingText();
m_handleMode = ShowCursor; m_handleMode = ShowCursor;
sendShortcut(QKeySequence::Cut); sendShortcut(QKeySequence::Cut);
return JNI_TRUE; return JNI_TRUE;
@ -1389,7 +1605,9 @@ jboolean QAndroidInputContext::cut()
jboolean QAndroidInputContext::copy() jboolean QAndroidInputContext::copy()
{ {
finishComposingText(); BatchEditLock batchEditLock(this);
focusObjectStopComposing();
m_handleMode = ShowCursor; m_handleMode = ShowCursor;
sendShortcut(QKeySequence::Copy); sendShortcut(QKeySequence::Copy);
return JNI_TRUE; return JNI_TRUE;
@ -1403,7 +1621,11 @@ jboolean QAndroidInputContext::copyURL()
jboolean QAndroidInputContext::paste() jboolean QAndroidInputContext::paste()
{ {
BatchEditLock batchEditLock(this);
// TODO: This is not what native EditText does
finishComposingText(); finishComposingText();
m_handleMode = ShowCursor; m_handleMode = ShowCursor;
sendShortcut(QKeySequence::Paste); sendShortcut(QKeySequence::Paste);
return JNI_TRUE; return JNI_TRUE;
@ -1415,8 +1637,12 @@ void QAndroidInputContext::sendShortcut(const QKeySequence &sequence)
const int keys = sequence[i]; const int keys = sequence[i];
Qt::Key key = Qt::Key(keys & ~Qt::KeyboardModifierMask); Qt::Key key = Qt::Key(keys & ~Qt::KeyboardModifierMask);
Qt::KeyboardModifiers mod = Qt::KeyboardModifiers(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);
} }
} }

View File

@ -151,6 +151,9 @@ private slots:
private: private:
void sendInputMethodEvent(QInputMethodEvent *event); void sendInputMethodEvent(QInputMethodEvent *event);
QSharedPointer<QInputMethodQueryEvent> focusObjectInputMethodQuery(Qt::InputMethodQueries queries = Qt::ImQueryAll); QSharedPointer<QInputMethodQueryEvent> focusObjectInputMethodQuery(Qt::InputMethodQueries queries = Qt::ImQueryAll);
bool focusObjectIsComposing() const;
void focusObjectStartComposing();
bool focusObjectStopComposing();
private: private:
ExtractedText m_extractedText; ExtractedText m_extractedText;
@ -158,9 +161,8 @@ private:
int m_composingTextStart; int m_composingTextStart;
int m_composingCursor; int m_composingCursor;
QMetaObject::Connection m_updateCursorPosConnection; QMetaObject::Connection m_updateCursorPosConnection;
bool m_blockUpdateSelection;
HandleModes m_handleMode; HandleModes m_handleMode;
QAtomicInt m_batchEditNestingLevel; int m_batchEditNestingLevel;
QObject *m_focusObject; QObject *m_focusObject;
QTimer m_hideCursorHandleTimer; QTimer m_hideCursorHandleTimer;
}; };