Android input method fixes for SwiftKey

Try to conform to the input method protocol in the way that
SwiftKey expects (and the way that the stock android
components actually do).

* Refactor cursor position logic
* fix getExtractedText() so it includes preedit text
* ignore the hintMaxChars parameter to getExtractedText(), since
  it looks like everybody else does
* fix setComposingRegion when preedit is active
* track the start of the preedit and the preedit cursor position,
  since the Qt input method query does not give us this information

Change-Id: I2ed8797abacd97ca749ca152fab2a2d5446ef603
Reviewed-by: Eskil Abrahamsen Blomfeldt <eskil.abrahamsen-blomfeldt@digia.com>
bb10
Paul Olav Tvete 2014-04-03 15:36:27 +02:00 committed by The Qt Project
parent d29a750c0c
commit 7e071cebaa
2 changed files with 144 additions and 42 deletions

View File

@ -1,5 +1,6 @@
/****************************************************************************
**
** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies).
** Copyright (C) 2012 BogDan Vatra <bogdan@kde.org>
** Contact: http://www.qt-project.org/legal
**
@ -338,7 +339,7 @@ static JNINativeMethod methods[] = {
QAndroidInputContext::QAndroidInputContext()
: QPlatformInputContext(), m_blockUpdateSelection(false), m_batchEditNestingLevel(0), m_focusObject(0)
: QPlatformInputContext(), m_composingTextStart(-1), m_blockUpdateSelection(false), m_batchEditNestingLevel(0), m_focusObject(0)
{
QtAndroid::AttachedJNIEnv env;
if (!env.jniEnv)
@ -431,9 +432,24 @@ QAndroidInputContext *QAndroidInputContext::androidInputContext()
return m_androidInputContext;
}
// cursor position getter that also works with editors that have not been updated to the new API
static inline int getAbsoluteCursorPosition(const QSharedPointer<QInputMethodQueryEvent> &query)
{
QVariant absolutePos = query->value(Qt::ImAbsolutePosition);
return absolutePos.isValid() ? absolutePos.toInt() : query->value(Qt::ImCursorPosition).toInt();
}
// position of the start of the current block
static inline int getBlockPosition(const QSharedPointer<QInputMethodQueryEvent> &query)
{
QVariant absolutePos = query->value(Qt::ImAbsolutePosition);
return absolutePos.isValid() ? absolutePos.toInt() - query->value(Qt::ImCursorPosition).toInt() : 0;
}
void QAndroidInputContext::reset()
{
clear();
m_batchEditNestingLevel = 0;
if (qGuiApp->focusObject())
QtAndroidInput::resetSoftwareKeyboard();
else
@ -449,13 +465,20 @@ void QAndroidInputContext::updateCursorPosition()
{
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
if (!query.isNull() && !m_blockUpdateSelection && !m_batchEditNestingLevel) {
// make sure it also works with editors that have not been updated to the new API
QVariant absolutePos = query->value(Qt::ImAbsolutePosition);
const int cursorPos = absolutePos.isValid() ? absolutePos.toInt() : query->value(Qt::ImCursorPosition).toInt();
const int cursorPos = getAbsoluteCursorPosition(query);
const int composeLength = m_composingText.length();
const int composeStart = composeLength ? cursorPos : -1;
QtAndroidInput::updateSelection(cursorPos + composeLength, cursorPos + composeLength, //empty selection
composeStart, composeStart + composeLength); // pre-edit text
//Q_ASSERT(m_composingText.isEmpty() == (m_composingTextStart == -1));
if (m_composingText.isEmpty() != (m_composingTextStart == -1))
qWarning() << "Input method out of sync" << m_composingText << m_composingTextStart;
// Qt's idea of the cursor position is the start of the preedit area, so we have to maintain our own preedit cursor pos
int realCursorPosition = cursorPos;
if (!m_composingText.isEmpty())
realCursorPosition = m_composingCursor;
QtAndroidInput::updateSelection(realCursorPosition, realCursorPosition, //empty selection
m_composingTextStart, m_composingTextStart + composeLength); // pre-edit text
}
}
@ -529,6 +552,7 @@ bool QAndroidInputContext::isComposing() const
void QAndroidInputContext::clear()
{
m_composingText.clear();
m_composingTextStart = -1;
m_extractedText.clear();
}
@ -569,8 +593,18 @@ jboolean QAndroidInputContext::endBatchEdit()
jboolean QAndroidInputContext::commitText(const QString &text, jint /*newCursorPosition*/)
{
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
if (query.isNull())
return JNI_FALSE;
const int cursorPos = getAbsoluteCursorPosition(query);
m_composingText = text;
return finishComposingText();
m_composingTextStart = cursorPos;
m_composingCursor = cursorPos + text.length();
finishComposingText();
//### move cursor to newCursorPosition and call updateCursorPosition()
return JNI_TRUE;
}
jboolean QAndroidInputContext::deleteSurroundingText(jint leftLength, jint rightLength)
@ -580,6 +614,7 @@ jboolean QAndroidInputContext::deleteSurroundingText(jint leftLength, jint right
return JNI_TRUE;
m_composingText.clear();
m_composingTextStart = -1;
QInputMethodEvent event;
event.setCommitString(QString(), -leftLength, leftLength+rightLength);
@ -617,7 +652,9 @@ jint QAndroidInputContext::getCursorCapsMode(jint /*reqModes*/)
return res;
}
const QAndroidInputContext::ExtractedText &QAndroidInputContext::getExtractedText(jint hintMaxChars, jint /*hintMaxLines*/, jint /*flags*/)
const QAndroidInputContext::ExtractedText &QAndroidInputContext::getExtractedText(jint /*hintMaxChars*/, jint /*hintMaxLines*/, jint /*flags*/)
{
// Note to self: "if the GET_EXTRACTED_TEXT_MONITOR flag is set, you should be calling
// updateExtractedText(View, int, ExtractedText) whenever you call
@ -628,28 +665,37 @@ const QAndroidInputContext::ExtractedText &QAndroidInputContext::getExtractedTex
return m_extractedText;
int localPos = query->value(Qt::ImCursorPosition).toInt(); //position before pre-edit text relative to the current block
QVariant absolutePos = query->value(Qt::ImAbsolutePosition);
int blockPos = absolutePos.isValid() ? absolutePos.toInt() - localPos : 0; // position of the start of the current block
QString blockText = query->value(Qt::ImSurroundingText).toString() + m_composingText;
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.left(localComposePos) + m_composingText + blockText.mid(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 (hintMaxChars) {
if (cpos > hintMaxChars)
localOffset = cpos - hintMaxChars;
m_extractedText.text = blockText.mid(localOffset, hintMaxChars);
}
m_extractedText.startOffset = blockPos + localOffset; // "The offset in the overall text at which the extracted text starts."
// It is documented that we should try to return hintMaxChars
// characters, but that's not what the standard Android controls do, 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;
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 {
} else if (composeLength > 0) {
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;
}
@ -668,6 +714,7 @@ QString QAndroidInputContext::getSelectedText(jint /*flags*/)
QString QAndroidInputContext::getTextAfterCursor(jint length, jint /*flags*/)
{
//### the preedit text could theoretically be after the cursor
QVariant textAfter = queryFocusObjectThreadSafe(Qt::ImTextAfterCursor, QVariant(length));
if (textAfter.isValid()) {
return textAfter.toString().left(length);
@ -703,15 +750,34 @@ QString QAndroidInputContext::getTextBeforeCursor(jint length, jint /*flags*/)
if (!text.length())
return text;
const int wordLeftPos = cursorPos - length;
return text.mid(wordLeftPos > 0 ? wordLeftPos : 0, cursorPos) + m_composingText;
//### the preedit text does not need to be immediately before the cursor
if (cursorPos <= length)
return text.left(cursorPos) + m_composingText;
else
return text.mid(cursorPos - length, length) + m_composingText;
}
/*
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.
*/
jboolean QAndroidInputContext::setComposingText(const QString &text, jint newCursorPosition)
{
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
if (query.isNull())
return JNI_FALSE;
const int cursorPos = getAbsoluteCursorPosition(query);
if (newCursorPosition > 0)
newCursorPosition += text.length() - 1;
m_composingText = text;
m_composingTextStart = text.isEmpty() ? -1 : cursorPos;
m_composingCursor = cursorPos + newCursorPosition;
QList<QInputMethodEvent::Attribute> attributes;
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor,
newCursorPosition,
@ -726,23 +792,26 @@ jboolean QAndroidInputContext::setComposingText(const QString &text, jint newCur
QInputMethodEvent event(m_composingText, attributes);
sendInputMethodEvent(&event);
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
if (!query.isNull() && !m_blockUpdateSelection && !m_batchEditNestingLevel) {
QVariant absolutePos = query->value(Qt::ImAbsolutePosition);
const int cursorPos = absolutePos.isValid() ? absolutePos.toInt() : query->value(Qt::ImCursorPosition).toInt();
const int preeditLength = text.length();
QtAndroidInput::updateSelection(cursorPos+preeditLength, cursorPos+preeditLength, -1, -1);
}
updateCursorPosition();
return JNI_TRUE;
}
// Android docs say:
// * start may be after end, same meaning as if swapped
// * this function must not trigger updateSelection
// * this function should not trigger updateSelection
// * if start == end then we should stop composing
jboolean QAndroidInputContext::setComposingRegion(jint start, jint end)
{
// 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();
QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery();
if (query.isNull())
return JNI_FALSE;
@ -757,19 +826,23 @@ jboolean QAndroidInputContext::setComposingRegion(jint start, jint end)
Therefore, the length of the region is end - start
*/
int length = end - start;
int localPos = query->value(Qt::ImCursorPosition).toInt();
QVariant absolutePos = query->value(Qt::ImAbsolutePosition);
int blockPosition = absolutePos.isValid() ? absolutePos.toInt() - localPos : 0;
int blockPosition = getBlockPosition(query);
int localStart = start - blockPosition; // Qt uses position inside block
int currentCursor = wasComposing ? m_composingCursor : blockPosition + localPos;
bool updateSelectionWasBlocked = m_blockUpdateSelection;
m_blockUpdateSelection = true;
QString text = query->value(Qt::ImSurroundingText).toString();
m_composingText = text.mid(localStart, length);
//in the Qt text controls, the cursor position is the start of the preedit
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;
QList<QInputMethodEvent::Attribute> attributes;
@ -781,13 +854,22 @@ jboolean QAndroidInputContext::setComposingRegion(jint start, jint end)
QVariant(underlined)));
// Keep the cursor position unchanged (don't move to end of preedit)
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, localPos - localStart, length, QVariant()));
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, currentCursor - start, 1, QVariant()));
QInputMethodEvent event(m_composingText, attributes);
event.setCommitString(QString(), relativeStart, length);
sendInputMethodEvent(&event);
m_blockUpdateSelection = updateSelectionWasBlocked;
#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);
}
#endif
return JNI_TRUE;
}
@ -797,18 +879,36 @@ jboolean QAndroidInputContext::setSelection(jint start, jint end)
if (query.isNull())
return JNI_FALSE;
int localPos = query->value(Qt::ImCursorPosition).toInt();
QVariant absolutePos = query->value(Qt::ImAbsolutePosition);
int blockPosition = absolutePos.isValid() ? absolutePos.toInt() - localPos : 0;
int blockPosition = getBlockPosition(query);
int localCursorPos = start - blockPosition;
QList<QInputMethodEvent::Attribute> attributes;
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection,
start - blockPosition,
end - start,
QVariant()));
if (!m_composingText.isEmpty() && start == end) {
// not actually changing the selection; just moving the
// preedit cursor
int localOldPos = query->value(Qt::ImCursorPosition).toInt();
int pos = localCursorPos - localOldPos;
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Cursor, pos, 1, QVariant()));
QInputMethodEvent event(QString(), attributes);
//but we have to tell Qt about the compose text all over again
// Show compose text underlined
QTextCharFormat underlined;
underlined.setFontUnderline(true);
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::TextFormat,0, m_composingText.length(),
QVariant(underlined)));
m_composingCursor = start;
} else {
// actually changing the selection
attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection,
localCursorPos,
end - start,
QVariant()));
}
QInputMethodEvent event(m_composingText, attributes);
sendInputMethodEvent(&event);
updateCursorPosition();
return JNI_TRUE;
}

View File

@ -134,6 +134,8 @@ private slots:
private:
ExtractedText m_extractedText;
QString m_composingText;
int m_composingTextStart;
int m_composingCursor;
QMetaObject::Connection m_updateCursorPosConnection;
bool m_blockUpdateSelection;
int m_batchEditNestingLevel;