From 3f9ad1efd61c76fe0671d28aac9e4886951856ec Mon Sep 17 00:00:00 2001 From: Paul Olav Tvete Date: Fri, 11 Apr 2014 13:46:48 +0200 Subject: Android: Reset input method when focus changes Qt Quick does not have the widgets workaround of explicitly hiding the input method on focus out. This fix copies what happens in the iOS port: Commit the current preedit and reset the IM when we see that the focus object changes. Task-number: QTBUG-38047 Change-Id: I30805265286dc650b3734e2a24807cdc8bfbcd16 Reviewed-by: Eskil Abrahamsen Blomfeldt --- src/plugins/platforms/android/qandroidinputcontext.cpp | 14 +++++++++++++- src/plugins/platforms/android/qandroidinputcontext.h | 2 ++ 2 files changed, 15 insertions(+), 1 deletion(-) (limited to 'src/plugins/platforms/android') diff --git a/src/plugins/platforms/android/qandroidinputcontext.cpp b/src/plugins/platforms/android/qandroidinputcontext.cpp index 3324d9ba49..12005d8fce 100644 --- a/src/plugins/platforms/android/qandroidinputcontext.cpp +++ b/src/plugins/platforms/android/qandroidinputcontext.cpp @@ -338,7 +338,7 @@ static JNINativeMethod methods[] = { QAndroidInputContext::QAndroidInputContext() - : QPlatformInputContext(), m_blockUpdateSelection(false), m_batchEditNestingLevel(0) + : QPlatformInputContext(), m_blockUpdateSelection(false), m_batchEditNestingLevel(0), m_focusObject(0) { QtAndroid::AttachedJNIEnv env; if (!env.jniEnv) @@ -532,6 +532,18 @@ void QAndroidInputContext::clear() m_extractedText.clear(); } + +void QAndroidInputContext::setFocusObject(QObject *object) +{ + if (object != m_focusObject) { + m_focusObject = object; + if (!m_composingText.isEmpty()) + finishComposingText(); + reset(); + } + QPlatformInputContext::setFocusObject(object); +} + void QAndroidInputContext::sendEvent(QObject *receiver, QInputMethodEvent *event) { QCoreApplication::sendEvent(receiver, event); diff --git a/src/plugins/platforms/android/qandroidinputcontext.h b/src/plugins/platforms/android/qandroidinputcontext.h index f7b29a855f..3ce141ae15 100644 --- a/src/plugins/platforms/android/qandroidinputcontext.h +++ b/src/plugins/platforms/android/qandroidinputcontext.h @@ -95,6 +95,7 @@ public: bool isComposing() const; void clear(); + void setFocusObject(QObject *object); //---------------// jboolean beginBatchEdit(); @@ -136,6 +137,7 @@ private: QMetaObject::Connection m_updateCursorPosConnection; bool m_blockUpdateSelection; int m_batchEditNestingLevel; + QObject *m_focusObject; }; QT_END_NAMESPACE -- cgit v1.2.3 From 6e26bd5fa26489034fe5d035bb4c5034455aa24d Mon Sep 17 00:00:00 2001 From: Paul Olav Tvete Date: Thu, 3 Apr 2014 09:47:32 +0200 Subject: Android input method fix Let textBeforeCursor return the text immediately before the cursor, and not the text at the beginning of the paragraph, even if that is also technically before the cursor. (Apparently I do not know the difference between left and right.) Change-Id: I6043ebe53838e68880b6407dbb9e5370bc785c1b Reviewed-by: Eskil Abrahamsen Blomfeldt --- src/plugins/platforms/android/qandroidinputcontext.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/plugins/platforms/android') diff --git a/src/plugins/platforms/android/qandroidinputcontext.cpp b/src/plugins/platforms/android/qandroidinputcontext.cpp index 12005d8fce..02fda19d76 100644 --- a/src/plugins/platforms/android/qandroidinputcontext.cpp +++ b/src/plugins/platforms/android/qandroidinputcontext.cpp @@ -690,7 +690,7 @@ QString QAndroidInputContext::getTextBeforeCursor(jint length, jint /*flags*/) { QVariant textBefore = queryFocusObjectThreadSafe(Qt::ImTextBeforeCursor, QVariant(length)); if (textBefore.isValid()) { - return textBefore.toString().left(length) + m_composingText; + return textBefore.toString().right(length) + m_composingText; } //compatibility code for old controls that do not implement the new API -- cgit v1.2.3 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/plugins/platforms/android') 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