diff --git a/autotests/integration/inputmethod_test.cpp b/autotests/integration/inputmethod_test.cpp index 190af0f4f5..46f8017b2f 100644 --- a/autotests/integration/inputmethod_test.cpp +++ b/autotests/integration/inputmethod_test.cpp @@ -66,6 +66,7 @@ private Q_SLOTS: void testFakeEventFallback(); void testOverlayPositioning_data(); void testOverlayPositioning(); + void testV3AutoCommit(); private: void touchNow() @@ -801,6 +802,124 @@ void InputMethodTest::testOverlayPositioning() Test::inputMethod()->setMode(Test::MockInputMethod::Mode::TopLevel); } +void InputMethodTest::testV3AutoCommit() +{ + Test::inputMethod()->setMode(Test::MockInputMethod::Mode::Overlay); + + // Create an xdg_toplevel surface and wait for the compositor to catch up. + std::unique_ptr surface(Test::createSurface()); + std::unique_ptr shellSurface(Test::createXdgToplevelSurface(surface.get())); + Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red); + QVERIFY(window); + QVERIFY(window->isActive()); + + auto textInputV3 = std::make_unique(); + textInputV3->init(Test::waylandTextInputManagerV3()->get_text_input(*(Test::waylandSeat()))); + textInputV3->enable(); + + QSignalSpy textInputPreeditSpy(textInputV3.get(), &Test::TextInputV3::preeditString); + QSignalSpy textInputCommitTextSpy(textInputV3.get(), &Test::TextInputV3::commitString); + + QSignalSpy inputMethodActiveSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged); + QSignalSpy inputMethodActivateSpy(Test::inputMethod(), &Test::MockInputMethod::activate); + // just enabling the text-input should not show it but rather on commit + QVERIFY(!kwinApp()->inputMethod()->isActive()); + textInputV3->commit(); + QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait()); + QVERIFY(kwinApp()->inputMethod()->isActive()); + QVERIFY(inputMethodActivateSpy.wait()); + auto context = Test::inputMethod()->context(); + + zwp_input_method_context_v1_preedit_string(context, 1, "preedit1", "commit1"); + QVERIFY(textInputPreeditSpy.wait()); + QVERIFY(textInputCommitTextSpy.count() == 0); + + // ****************** + // Non-grabbing key press + int timestamp = 0; + Test::keyboardKeyPressed(KEY_A, timestamp++); + Test::keyboardKeyReleased(KEY_A, timestamp++); + QVERIFY(textInputCommitTextSpy.wait()); + QCOMPARE(textInputCommitTextSpy.last()[0].toString(), "commit1"); + QCOMPARE(textInputPreeditSpy.last()[0].toString(), QString()); + + // ****************** + // Grabbing key press + zwp_input_method_context_v1_grab_keyboard(context); + textInputV3->commit(); + zwp_input_method_context_v1_preedit_string(context, 1, "preedit2", "commit2"); + + QVERIFY(textInputPreeditSpy.wait()); + QCOMPARE(textInputPreeditSpy.last()[0].toString(), QString("preedit2")); + + // a key does nothing, it will go to the input method + Test::keyboardKeyPressed(KEY_B, timestamp++); + Test::keyboardKeyReleased(KEY_B, timestamp++); + QVERIFY(!textInputCommitTextSpy.wait()); + + // then the input method forwards the key + zwp_input_method_context_v1_key(context, 2, timestamp, KEY_B, uint32_t(KeyboardKeyState::Pressed)); + zwp_input_method_context_v1_key(context, 2, timestamp, KEY_B, uint32_t(KeyboardKeyState::Released)); + + QVERIFY(textInputCommitTextSpy.wait()); + QCOMPARE(textInputCommitTextSpy.last()[0].toString(), "commit2"); + QCOMPARE(textInputPreeditSpy.last()[0].toString(), QString()); + + // ************** + // Mouse clicks + QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded); + textInputV3->disable(); + textInputV3->enable(); + const QList windows = workspace()->windows(); + auto it = std::find_if(windows.begin(), windows.end(), [](Window *w) { + return w->isInputMethod(); + }); + QVERIFY(it != windows.end()); + auto textInputWindow = *it; + + textInputV3->commit(); + zwp_input_method_context_v1_preedit_string(context, 1, "preedit3", "commit3"); + QVERIFY(textInputPreeditSpy.wait()); + QCOMPARE(textInputPreeditSpy.last()[0].toString(), QString("preedit3")); + + // mouse clicks on a VK does not submit + Test::pointerMotion(textInputWindow->frameGeometry().center(), timestamp++); + Test::pointerButtonPressed(1, timestamp++); + Test::pointerButtonReleased(1, timestamp++); + QVERIFY(!textInputCommitTextSpy.wait(20)); + + // mouse clicks on our main window submits the string + Test::pointerMotion(window->frameGeometry().center(), timestamp++); + Test::pointerButtonPressed(1, timestamp++); + Test::pointerButtonReleased(1, timestamp++); + + QVERIFY(textInputCommitTextSpy.wait()); + QCOMPARE(textInputCommitTextSpy.last()[0].toString(), "commit3"); + QCOMPARE(textInputPreeditSpy.last()[0].toString(), QString()); + + // ***************** + // Change focus + textInputV3->commit(); + zwp_input_method_context_v1_preedit_string(context, 1, "preedit4", "commit4"); + QVERIFY(textInputPreeditSpy.wait()); + QCOMPARE(textInputPreeditSpy.last()[0].toString(), QString("preedit4")); + + std::unique_ptr surface2(Test::createSurface()); + std::unique_ptr shellSurface2(Test::createXdgToplevelSurface(surface2.get())); + Window *window2 = Test::renderAndWaitForShown(surface2.get(), QSize(1280, 1024), Qt::blue); + QVERIFY(window2->isActive()); + + // these variables refer to the old window + QVERIFY(textInputCommitTextSpy.wait()); + QCOMPARE(textInputCommitTextSpy.last()[0].toString(), "commit4"); + QCOMPARE(textInputPreeditSpy.last()[0].toString(), QString()); + + shellSurface.reset(); + QVERIFY(Test::waitForWindowClosed(window)); + shellSurface2.reset(); + QVERIFY(Test::waitForWindowClosed(window2)); +} + WAYLANDTEST_MAIN(InputMethodTest) #include "inputmethod_test.moc" diff --git a/autotests/integration/kwin_wayland_test.h b/autotests/integration/kwin_wayland_test.h index ca7be08d1b..363b98a92b 100644 --- a/autotests/integration/kwin_wayland_test.h +++ b/autotests/integration/kwin_wayland_test.h @@ -158,12 +158,17 @@ public: Q_SIGNALS: void preeditString(const QString &text, int cursor_begin, int cursor_end); + void commitString(const QString &text); protected: void zwp_text_input_v3_preedit_string(const QString &text, int32_t cursor_begin, int32_t cursor_end) override { Q_EMIT preeditString(text, cursor_begin, cursor_end); } + void zwp_text_input_v3_commit_string(const QString &text) override + { + Q_EMIT commitString(text); + } }; class LayerShellV1 : public QtWayland::zwlr_layer_shell_v1 diff --git a/src/input.cpp b/src/input.cpp index db4ffa6423..289bcff776 100644 --- a/src/input.cpp +++ b/src/input.cpp @@ -282,6 +282,7 @@ bool InputEventFilter::passToInputMethod(QKeyEvent *event) keyboardGrab->sendKey(waylandServer()->display()->nextSerial(), event->timestamp(), event->nativeScanCode(), newState); return true; } else { + kwinApp()->inputMethod()->commitPendingText(); return false; } } @@ -1811,13 +1812,47 @@ public: } }; -class InputKeyboardFilter : public InputEventFilter +class InputMethodEventFilter : public InputEventFilter { public: - InputKeyboardFilter() + InputMethodEventFilter() : InputEventFilter(InputFilterOrder::InputMethod) { } + + bool pointerEvent(MouseEvent *event, quint32 nativeButton) override + { + auto inputMethod = kwinApp()->inputMethod(); + if (!inputMethod) { + return false; + } + if (event->type() != QEvent::MouseButtonPress) { + return false; + } + + // clicking on an on screen keyboard shouldn't flush, check we're clicking on our target window + if (input()->pointer()->focus() != inputMethod->activeWindow()) { + return false; + } + + inputMethod->commitPendingText(); + return false; + } + + bool touchDown(qint32 id, const QPointF &point, std::chrono::microseconds time) override + { + auto inputMethod = kwinApp()->inputMethod(); + if (!inputMethod) { + return false; + } + if (input()->findToplevel(point) != inputMethod->activeWindow()) { + return false; + } + + inputMethod->commitPendingText(); + return false; + } + bool keyEvent(KeyEvent *event) override { return passToInputMethod(event); @@ -3035,7 +3070,7 @@ void InputRedirection::setupInputFilters() m_internalWindowFilter = std::make_unique(); installInputEventFilter(m_internalWindowFilter.get()); - m_inputKeyboardFilter = std::make_unique(); + m_inputKeyboardFilter = std::make_unique(); installInputEventFilter(m_inputKeyboardFilter.get()); m_forwardFilter = std::make_unique(); diff --git a/src/inputmethod.cpp b/src/inputmethod.cpp index f43c161321..3d9633b8f8 100644 --- a/src/inputmethod.cpp +++ b/src/inputmethod.cpp @@ -11,6 +11,7 @@ #include "config-kwin.h" #include "input.h" +#include "input_event.h" #include "inputpanelv1window.h" #include "keyboard_input.h" #include "utils/common.h" @@ -21,6 +22,7 @@ #if KWIN_BUILD_SCREENLOCKER #include "screenlockerwatcher.h" #endif +#include "pointer_input.h" #include "tablet_input.h" #include "touch_input.h" #include "wayland/display.h" @@ -122,6 +124,7 @@ void InputMethod::init() new TextInputManagerV2Interface(waylandServer()->display(), this); new TextInputManagerV3Interface(waylandServer()->display(), this); + connect(waylandServer()->seat(), &SeatInterface::focusedKeyboardSurfaceAboutToChange, this, &InputMethod::commitPendingText); connect(waylandServer()->seat(), &SeatInterface::focusedTextInputSurfaceChanged, this, &InputMethod::handleFocusedSurfaceChanged); TextInputV1Interface *textInputV1 = waylandServer()->seat()->textInputV1(); @@ -207,6 +210,18 @@ void InputMethod::refreshActive() setActive(active); } +void InputMethod::commitPendingText() +{ + if (!m_pendingText.isEmpty()) { + commitString(m_serial++, m_pendingText); + m_pendingText = QString(); + auto imContext = waylandServer()->inputMethod()->context(); + if (imContext) { + imContext->sendReset(); + } + } +} + void InputMethod::setActive(bool active) { const bool wasActive = waylandServer()->inputMethod()->context(); @@ -286,6 +301,9 @@ void InputMethod::setTrackedWindow(Window *trackedWindow) void InputMethod::handleFocusedSurfaceChanged() { + resetPendingPreedit(); + m_pendingText = QString(); + auto seat = waylandServer()->seat(); SurfaceInterface *focusedSurface = seat->focusedTextInputSurface(); @@ -447,6 +465,7 @@ void InputMethod::textInputInterfaceV3EnabledChanged() } else { // reset value of preedit when textinput is disabled resetPendingPreedit(); + m_pendingText = QString(); } auto context = waylandServer()->inputMethod()->context(); if (context) { @@ -519,6 +538,9 @@ static quint32 keysymToKeycode(quint32 sym) void InputMethod::keysymReceived(quint32 serial, quint32 time, quint32 sym, bool pressed, quint32 modifiers) { + if (pressed) { + commitPendingText(); + } if (auto t1 = waylandServer()->seat()->textInputV1(); t1 && t1->isEnabled()) { if (pressed) { t1->keysymPressed(time, sym, modifiers); @@ -561,6 +583,7 @@ void InputMethod::commitString(qint32 serial, const QString &text) t2->preEdit({}, {}); return; } else if (auto t3 = waylandServer()->seat()->textInputV3(); t3 && t3->isEnabled()) { + t3->sendPreEditString(QString(), 0, 0); t3->commitString(text); t3->done(); return; @@ -708,6 +731,7 @@ void InputMethod::setPreeditString(uint32_t serial, const QString &text, const Q } auto t3 = waylandServer()->seat()->textInputV3(); if (t3 && t3->isEnabled()) { + m_pendingText = commit; if (!text.isEmpty()) { quint32 cursor = 0, cursorEnd = 0; if (preedit.cursor > 0) { @@ -743,6 +767,9 @@ void InputMethod::setPreeditString(uint32_t serial, const QString &text, const Q void InputMethod::key(quint32 /*serial*/, quint32 /*time*/, quint32 keyCode, bool pressed) { + if (pressed) { + commitPendingText(); + } waylandServer()->seat()->notifyKeyboardKey(keyCode, pressed ? KeyboardKeyState::Pressed : KeyboardKeyState::Released); } @@ -963,6 +990,11 @@ bool InputMethod::isAvailable() const return !m_inputMethodCommand.isEmpty(); } +Window *InputMethod::activeWindow() const +{ + return m_trackedWindow; +} + void InputMethod::resetPendingPreedit() { preedit.cursor = 0; diff --git a/src/inputmethod.h b/src/inputmethod.h index b1001d5c56..d626824cb1 100644 --- a/src/inputmethod.h +++ b/src/inputmethod.h @@ -15,7 +15,7 @@ #include -#include "effect/globals.h" +#include "input_event_spy.h" #include #include @@ -58,6 +58,7 @@ public: void show(); bool isVisible() const; bool isAvailable() const; + Window *activeWindow() const; InputPanelV1Window *panel() const; void setPanel(InputPanelV1Window *panel); @@ -70,6 +71,8 @@ public: bool activeClientSupportsTextInput() const; void forceActivate(); + void commitPendingText(); + Q_SIGNALS: void panelChanged(); void activeChanged(bool active); @@ -119,12 +122,17 @@ private: void resetPendingPreedit(); void refreshActive(); + // buffered till the preedit text is set struct { qint32 cursor = 0; std::vector> highlightRanges; } preedit; + // In some IM cases pre-edit text should be submitted when a user changes focus. In some it should be discarded + // TextInputV3 does not have a flag for this, so we have to handle it in the compositor + QString m_pendingText = QString(); + bool m_enabled = true; quint32 m_serial = 0; QPointer m_panel;