From 7e071cebaa731b3f635ef515627985e27726aba1 Mon Sep 17 00:00:00 2001 From: Paul Olav Tvete Date: Thu, 3 Apr 2014 15:36:27 +0200 Subject: 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 --- .../platforms/android/qandroidinputcontext.cpp | 182 ++++++++++++++++----- .../platforms/android/qandroidinputcontext.h | 2 + 2 files changed, 143 insertions(+), 41 deletions(-) (limited to 'src') 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 ** 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 &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 &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 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 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 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 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 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 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 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 query2 = focusObjectInputMethodQuery(); + if (!query2.isNull()) { + qDebug() << "Setting. Prev local cpos:" << localPos << "block pos:" <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 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; -- cgit v1.2.3