summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorPaul Olav Tvete <paul.tvete@digia.com>2014-04-03 15:36:27 +0200
committerThe Qt Project <gerrit-noreply@qt-project.org>2014-04-25 06:34:08 +0200
commit7e071cebaa731b3f635ef515627985e27726aba1 (patch)
treefc4709bf31d265526243d8ee1fe2e69c3804f9f3 /src
parentd29a750c0cc423bf65171338eeaa767f272d4ef0 (diff)
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>
Diffstat (limited to 'src')
-rw-r--r--src/plugins/platforms/android/qandroidinputcontext.cpp182
-rw-r--r--src/plugins/platforms/android/qandroidinputcontext.h2
2 files changed, 143 insertions, 41 deletions
diff --git a/src/plugins/platforms/android/qandroidinputcontext.cpp b/src/plugins/platforms/android/qandroidinputcontext.cpp
index 02fda19d76..e255a49ac7 100644
--- a/src/plugins/platforms/android/qandroidinputcontext.cpp
+++ b/src/plugins/platforms/android/qandroidinputcontext.cpp
@@ -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);
+ m_composingTextStart = start;
+ m_composingCursor = currentCursor;
- //in the Qt text controls, the cursor position is the start of the preedit
+ //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()));
+
+ //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;
- QInputMethodEvent event(QString(), attributes);
+ } 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;
}
diff --git a/src/plugins/platforms/android/qandroidinputcontext.h b/src/plugins/platforms/android/qandroidinputcontext.h
index 3ce141ae15..a467e4849e 100644
--- a/src/plugins/platforms/android/qandroidinputcontext.h
+++ b/src/plugins/platforms/android/qandroidinputcontext.h
@@ -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;