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
parent
d29a750c0c
commit
7e071cebaa
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue