diff options
author | Peter Varga <pvarga@inf.u-szeged.hu> | 2018-04-18 16:46:31 +0200 |
---|---|---|
committer | Peter Varga <pvarga@inf.u-szeged.hu> | 2018-04-26 14:11:38 +0000 |
commit | 216240a31baae6e54e38de8157332f272ddf57a7 (patch) | |
tree | f9c14206827960bc8e91c9d1704398568012fb07 | |
parent | b3b95a5bf3f04c18182fcc4519ec8285e290037d (diff) |
Fix finishing IME composition
Call RenderWidgetHostImpl::ImeCommitText() instead of
RenderWidgetHostImpl::ImeFinishComposingText() to trigger the necessary
JavaScript events on composing.
This fixes IME composition (eg. for dead keys) on web pages which use
custom JavaScript IME handler like facebook.
Task-number: QTBUG-66046
Change-Id: Ibc177995ba6e85eca42ae333decacfe6e788ce41
Reviewed-by: Allan Sandfeld Jensen <allan.jensen@qt.io>
-rw-r--r-- | src/core/render_widget_host_view_qt.cpp | 98 | ||||
-rw-r--r-- | src/core/render_widget_host_view_qt.h | 2 | ||||
-rw-r--r-- | tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp | 136 |
3 files changed, 185 insertions, 51 deletions
diff --git a/src/core/render_widget_host_view_qt.cpp b/src/core/render_widget_host_view_qt.cpp index 66d9b819e..ec3add2f6 100644 --- a/src/core/render_widget_host_view_qt.cpp +++ b/src/core/render_widget_host_view_qt.cpp @@ -327,7 +327,7 @@ RenderWidgetHostViewQt::RenderWidgetHostViewQt(content::RenderWidgetHost *widget , m_adapterClient(0) , m_rendererCompositorFrameSink(0) , m_imeInProgress(false) - , m_receivedEmptyImeText(false) + , m_receivedEmptyImeEvent(false) , m_initPending(false) , m_beginFrameSource(nullptr) , m_needsBeginFrames(false) @@ -1219,22 +1219,20 @@ void RenderWidgetHostViewQt::handleKeyEvent(QKeyEvent *ev) if (IsMouseLocked() && ev->key() == Qt::Key_Escape && ev->type() == QEvent::KeyRelease) UnlockMouse(); - if (m_receivedEmptyImeText) { + if (m_receivedEmptyImeEvent) { // IME composition was not finished with a valid commit string. // We're getting the composition result in a key event. if (ev->key() != 0) { // The key event is not a result of an IME composition. Cancel IME. m_host->ImeCancelComposition(); - m_receivedEmptyImeText = false; + m_receivedEmptyImeEvent = false; } else { if (ev->type() == QEvent::KeyRelease) { - m_receivedEmptyImeText = false; - m_host->ImeSetComposition(toString16(ev->text()), - std::vector<ui::ImeTextSpan>(), - gfx::Range::InvalidRange(), - gfx::Range::InvalidRange().start(), - gfx::Range::InvalidRange().end()); - m_host->ImeFinishComposingText(false); + m_host->ImeCommitText(toString16(ev->text()), + std::vector<ui::ImeTextSpan>(), + gfx::Range::InvalidRange(), + 0); + m_receivedEmptyImeEvent = false; m_imeInProgress = false; } return; @@ -1365,54 +1363,54 @@ void RenderWidgetHostViewQt::handleInputMethodEvent(QInputMethodEvent *ev) } } - auto setCompositionString = [&](const QString &compositionString){ - m_host->ImeSetComposition(toString16(compositionString), - underlines, - replacementRange, - selectionRange.start(), - selectionRange.end()); - }; - - if (!commitString.isEmpty() || replacementLength > 0) { - setCompositionString(commitString); - m_host->ImeFinishComposingText(false); - - // We might get a commit string and a pre-edit string in a single event, which means - // we need to confirm thećlast composition, and start a new composition. - if (!preeditString.isEmpty()) { - setCompositionString(preeditString); - m_imeInProgress = true; - } else { - m_imeInProgress = false; - } - m_receivedEmptyImeText = commitString.isEmpty(); - } else if (!preeditString.isEmpty()) { - setCompositionString(preeditString); - m_imeInProgress = true; - m_receivedEmptyImeText = false; - } else { - // There are so-far two known cases, when an empty QInputMethodEvent is received. - // First one happens when backspace is used to remove the last character in the pre-edit - // string, thus signaling the end of the composition. - // The second one happens (on Windows) when a Korean char gets composed, but instead of - // the event having a commit string, both strings are empty, and the actual char is received - // as a QKeyEvent after the QInputMethodEvent is processed. - // In lieu of the second case, we can't simply cancel the composition on an empty event, - // and then add the Korean char when QKeyEvent is received, because that leads to text - // flickering in the textarea (or any other element). - // Instead we postpone the processing of the empty QInputMethodEvent by posting it - // to the same focused object, and cancelling the composition on the next event loop tick. - if (!m_receivedEmptyImeText && m_imeInProgress && !hasSelection) { - m_receivedEmptyImeText = true; + // There are so-far two known cases, when an empty QInputMethodEvent is received. + // First one happens when backspace is used to remove the last character in the pre-edit + // string, thus signaling the end of the composition. + // The second one happens (on Windows) when a Korean char gets composed, but instead of + // the event having a commit string, both strings are empty, and the actual char is received + // as a QKeyEvent after the QInputMethodEvent is processed. + // In lieu of the second case, we can't simply cancel the composition on an empty event, + // and then add the Korean char when QKeyEvent is received, because that leads to text + // flickering in the textarea (or any other element). + // Instead we postpone the processing of the empty QInputMethodEvent by posting it + // to the same focused object, and cancelling the composition on the next event loop tick. + if (commitString.isEmpty() && preeditString.isEmpty() && replacementLength == 0) { + if (!m_receivedEmptyImeEvent && m_imeInProgress && !hasSelection) { + m_receivedEmptyImeEvent = true; QInputMethodEvent *eventCopy = new QInputMethodEvent(*ev); QGuiApplication::postEvent(qApp->focusObject(), eventCopy); } else { - m_receivedEmptyImeText = false; + m_receivedEmptyImeEvent = false; if (m_imeInProgress) { m_imeInProgress = false; m_host->ImeCancelComposition(); } } + + return; + } + + m_receivedEmptyImeEvent = false; + + // Finish compostion: insert or erase text. + if (!commitString.isEmpty() || replacementLength > 0) { + m_host->ImeCommitText(toString16(commitString), + underlines, + replacementRange, + 0); + m_imeInProgress = false; + } + + // Update or start new composition. + // Be aware of that, we might get a commit string and a pre-edit string in a single event and + // this means a new composition. + if (!preeditString.isEmpty()) { + m_host->ImeSetComposition(toString16(preeditString), + underlines, + replacementRange, + selectionRange.start(), + selectionRange.end()); + m_imeInProgress = true; } } diff --git a/src/core/render_widget_host_view_qt.h b/src/core/render_widget_host_view_qt.h index db68e5232..2a1485510 100644 --- a/src/core/render_widget_host_view_qt.h +++ b/src/core/render_widget_host_view_qt.h @@ -253,7 +253,7 @@ private: viz::mojom::CompositorFrameSinkClient *m_rendererCompositorFrameSink; bool m_imeInProgress; - bool m_receivedEmptyImeText; + bool m_receivedEmptyImeEvent; QPoint m_previousMousePosition; bool m_initPending; diff --git a/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp b/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp index 207836bef..24e581870 100644 --- a/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp +++ b/tests/auto/widgets/qwebengineview/tst_qwebengineview.cpp @@ -173,6 +173,7 @@ private Q_SLOTS: void imeCompositionQueryEvent_data(); void imeCompositionQueryEvent(); void newlineInTextarea(); + void imeJSInputEvents(); void mouseLeave(); @@ -2249,6 +2250,141 @@ void tst_QWebEngineView::newlineInTextarea() QTRY_COMPARE(view.focusProxy()->inputMethodQuery(Qt::ImSurroundingText).toString(), QString("\n\nthird line")); } +void tst_QWebEngineView::imeJSInputEvents() +{ + QWebEngineView view; + view.settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, true); + view.show(); + + auto logLines = [&view]() -> QStringList { + return evaluateJavaScriptSync(view.page(), "log.textContent").toString().split("\n").filter(QRegExp(".+")); + }; + + QSignalSpy loadFinishedSpy(&view, SIGNAL(loadFinished(bool))); + view.page()->setHtml("<html>" + "<head><script>" + " var input, log;" + " function verboseEvent(ev) {" + " log.textContent += ev + ' ' + ev.type + ' ' + ev.data + '\\n';" + " }" + " function clear(ev) {" + " log.textContent = '';" + " input.textContent = '';" + " }" + " function init() {" + " input = document.getElementById('input');" + " log = document.getElementById('log');" + " events = [ 'textInput', 'beforeinput', 'input', 'compositionstart', 'compositionupdate', 'compositionend' ];" + " for (var e in events)" + " input.addEventListener(events[e], verboseEvent);" + " }" + "</script></head>" + "<body onload='init()'>" + " <div id='input' contenteditable='true' style='border-style: solid;'></div>" + " <pre id='log'></pre>" + "</body></html>"); + QVERIFY(loadFinishedSpy.wait()); + + evaluateJavaScriptSync(view.page(), "document.getElementById('input').focus()"); + QTRY_COMPARE(evaluateJavaScriptSync(view.page(), "document.activeElement.id").toString(), QStringLiteral("input")); + + // 1. Commit text (this is how dead keys work on Linux). + { + QList<QInputMethodEvent::Attribute> attributes; + QInputMethodEvent event("", attributes); + event.setCommitString("commit"); + QApplication::sendEvent(view.focusProxy(), &event); + qApp->processEvents(); + } + + // Simply committing text should not trigger any JS composition event. + QTRY_COMPARE(logLines().count(), 3); + QCOMPARE(logLines()[0], "[object InputEvent] beforeinput commit"); + QCOMPARE(logLines()[1], "[object TextEvent] textInput commit"); + QCOMPARE(logLines()[2], "[object InputEvent] input commit"); + + evaluateJavaScriptSync(view.page(), "clear()"); + QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "log.textContent + input.textContent").toString().isEmpty()); + + // 2. Start composition then commit text (this is how dead keys work on macOS). + { + QList<QInputMethodEvent::Attribute> attributes; + QInputMethodEvent event("preedit", attributes); + QApplication::sendEvent(view.focusProxy(), &event); + qApp->processEvents(); + } + + QTRY_COMPARE(logLines().count(), 4); + QCOMPARE(logLines()[0], "[object CompositionEvent] compositionstart "); + QCOMPARE(logLines()[1], "[object InputEvent] beforeinput preedit"); + QCOMPARE(logLines()[2], "[object CompositionEvent] compositionupdate preedit"); + QCOMPARE(logLines()[3], "[object InputEvent] input preedit"); + + { + QList<QInputMethodEvent::Attribute> attributes; + QInputMethodEvent event("", attributes); + event.setCommitString("commit"); + QApplication::sendEvent(view.focusProxy(), &event); + qApp->processEvents(); + } + + QTRY_COMPARE(logLines().count(), 9); + QCOMPARE(logLines()[4], "[object InputEvent] beforeinput commit"); + QCOMPARE(logLines()[5], "[object CompositionEvent] compositionupdate commit"); + QCOMPARE(logLines()[6], "[object TextEvent] textInput commit"); + QCOMPARE(logLines()[7], "[object InputEvent] input commit"); + QCOMPARE(logLines()[8], "[object CompositionEvent] compositionend commit"); + + evaluateJavaScriptSync(view.page(), "clear()"); + QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "log.textContent + input.textContent").toString().isEmpty()); + + // 3. Start composition then cancel it with an empty IME event. + { + QList<QInputMethodEvent::Attribute> attributes; + QInputMethodEvent event("preedit", attributes); + QApplication::sendEvent(view.focusProxy(), &event); + qApp->processEvents(); + } + + QTRY_COMPARE(logLines().count(), 4); + QCOMPARE(logLines()[0], "[object CompositionEvent] compositionstart "); + QCOMPARE(logLines()[1], "[object InputEvent] beforeinput preedit"); + QCOMPARE(logLines()[2], "[object CompositionEvent] compositionupdate preedit"); + QCOMPARE(logLines()[3], "[object InputEvent] input preedit"); + + { + QList<QInputMethodEvent::Attribute> attributes; + QInputMethodEvent event("", attributes); + QApplication::sendEvent(view.focusProxy(), &event); + qApp->processEvents(); + } + + QTRY_COMPARE(logLines().count(), 9); + QCOMPARE(logLines()[4], "[object InputEvent] beforeinput "); + QCOMPARE(logLines()[5], "[object CompositionEvent] compositionupdate "); + QCOMPARE(logLines()[6], "[object TextEvent] textInput "); + QCOMPARE(logLines()[7], "[object InputEvent] input null"); + QCOMPARE(logLines()[8], "[object CompositionEvent] compositionend "); + + evaluateJavaScriptSync(view.page(), "clear()"); + QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "log.textContent + input.textContent").toString().isEmpty()); + + // 4. Send empty IME event. + { + QList<QInputMethodEvent::Attribute> attributes; + QInputMethodEvent event("", attributes); + QApplication::sendEvent(view.focusProxy(), &event); + qApp->processEvents(); + } + + // No JS event is expected. + QTest::qWait(100); + QVERIFY(logLines().isEmpty()); + + evaluateJavaScriptSync(view.page(), "clear()"); + QTRY_VERIFY(evaluateJavaScriptSync(view.page(), "log.textContent + input.textContent").toString().isEmpty()); +} + void tst_QWebEngineView::imeCompositionQueryEvent_data() { QTest::addColumn<QString>("receiverObjectName"); |