diff --git a/autotests/integration/inputmethod_test.cpp b/autotests/integration/inputmethod_test.cpp index 598f67ce04..30ed2838fc 100644 --- a/autotests/integration/inputmethod_test.cpp +++ b/autotests/integration/inputmethod_test.cpp @@ -12,6 +12,7 @@ #include "deleted.h" #include "effects.h" #include "inputmethod.h" +#include "keyboard_input.h" #include "output.h" #include "platform.h" #include "qwayland-input-method-unstable-v1.h" @@ -24,6 +25,7 @@ #include "wayland_server.h" #include "window.h" #include "workspace.h" +#include "xkb.h" #include #include @@ -62,6 +64,7 @@ private Q_SLOTS: void testV3Styling(); void testDisableShowInputPanel(); void testModifierForwarding(); + void testFakeEventFallback(); private: void touchNow() @@ -530,6 +533,77 @@ void InputMethodTest::testModifierForwarding() disconnect(modifiersChangedConnection); } +void InputMethodTest::testFakeEventFallback() +{ + // 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()); + QCOMPARE(window->frameGeometry().size(), QSize(1280, 1024)); + + // Since we don't have a way to communicate with the client, manually activate + // the input method. + QSignalSpy inputMethodActiveSpy(Test::inputMethod(), &Test::MockInputMethod::activate); + kwinApp()->inputMethod()->setActive(true); + QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait()); + + // Without a way to communicate to the client, we send fake key events. This + // means the client needs to be able to receive them, so create a keyboard for + // the client and listen whether it gets the right events. + auto keyboard = Test::waylandSeat()->createKeyboard(window); + QSignalSpy keySpy(keyboard, &KWayland::Client::Keyboard::keyChanged); + + auto context = Test::inputMethod()->context(); + QVERIFY(context); + + // First, send a simple one-character string and check to see if that + // generates a key press followed by a key release on the client side. + zwp_input_method_context_v1_commit_string(context, 0, "a"); + + keySpy.wait(); + QVERIFY(keySpy.count() == 2); + + auto compare = [](const QList &input, quint32 key, Keyboard::KeyState state) { + auto inputKey = input.at(0).toInt(); + auto inputState = input.at(1).value(); + QCOMPARE(inputKey, key); + QCOMPARE(inputState, state); + }; + + compare(keySpy.at(0), KEY_A, Keyboard::KeyState::Pressed); + compare(keySpy.at(1), KEY_A, Keyboard::KeyState::Released); + + keySpy.clear(); + + // Capital letters are recognised and sent as a combination of Shift + the + // letter. + + zwp_input_method_context_v1_commit_string(context, 0, "A"); + + keySpy.wait(); + QVERIFY(keySpy.count() == 4); + + compare(keySpy.at(0), KEY_LEFTSHIFT, Keyboard::KeyState::Pressed); + compare(keySpy.at(1), KEY_A, Keyboard::KeyState::Pressed); + compare(keySpy.at(2), KEY_A, Keyboard::KeyState::Released); + compare(keySpy.at(3), KEY_LEFTSHIFT, Keyboard::KeyState::Released); + + keySpy.clear(); + + // Special keys are not sent through commit_string but instead use keysym. + auto enter = input()->keyboard()->xkb()->toKeysym(KEY_ENTER); + zwp_input_method_context_v1_keysym(context, 0, 0, enter, uint32_t(KWaylandServer::KeyboardKeyState::Pressed), 0); + zwp_input_method_context_v1_keysym(context, 0, 1, enter, uint32_t(KWaylandServer::KeyboardKeyState::Released), 0); + + keySpy.wait(); + QVERIFY(keySpy.count() == 2); + + compare(keySpy.at(0), KEY_ENTER, Keyboard::KeyState::Pressed); + compare(keySpy.at(1), KEY_ENTER, Keyboard::KeyState::Released); +} + WAYLANDTEST_MAIN(InputMethodTest) #include "inputmethod_test.moc" diff --git a/src/inputmethod.cpp b/src/inputmethod.cpp index c25d5c9f02..d7a373cf92 100644 --- a/src/inputmethod.cpp +++ b/src/inputmethod.cpp @@ -33,6 +33,7 @@ #include #include +#include #include #include @@ -49,6 +50,34 @@ using namespace KWaylandServer; namespace KWin { +static std::vector textToKey(const QString &text) +{ + if (text.isEmpty()) { + return {}; + } + + auto sequence = QKeySequence::fromString(text); + if (sequence.isEmpty()) { + return {}; + } + + int sym; + if (!KKeyServer::keyQtToSymX(sequence[0], &sym)) { + return {}; + } + + auto keyCode = KWin::input()->keyboard()->xkb()->keycodeFromKeysym(sym); + if (!keyCode) { + return {}; + } + + if (text.isUpper()) { + return {KEY_LEFTSHIFT, quint32(keyCode.value())}; + } + + return {quint32(keyCode.value())}; +} + InputMethod::InputMethod() { m_enabled = kwinApp()->config()->group("Wayland").readEntry("VirtualKeyboardEnabled", true); @@ -384,17 +413,14 @@ void InputMethod::keysymReceived(quint32 serial, quint32 time, quint32 sym, bool } return; } - auto t3 = waylandServer()->seat()->textInputV3(); - if (t3 && t3->isEnabled()) { - KWaylandServer::KeyboardKeyState state; - if (pressed) { - state = KWaylandServer::KeyboardKeyState::Pressed; - } else { - state = KWaylandServer::KeyboardKeyState::Released; - } - waylandServer()->seat()->notifyKeyboardKey(keysymToKeycode(sym), state); - return; + + KWaylandServer::KeyboardKeyState state; + if (pressed) { + state = KWaylandServer::KeyboardKeyState::Pressed; + } else { + state = KWaylandServer::KeyboardKeyState::Released; } + waylandServer()->seat()->notifyKeyboardKey(keysymToKeycode(sym), state); } void InputMethod::commitString(qint32 serial, const QString &text) @@ -409,7 +435,31 @@ void InputMethod::commitString(qint32 serial, const QString &text) t3->done(); return; } else { - qCWarning(KWIN_VIRTUALKEYBOARD) << "We have nobody to commit to!!!"; + // The application has no way of communicating with the input method. + // So instead, try to convert what we get from the input method into + // keycodes and send those as fake input to the client. + auto keys = textToKey(text); + if (keys.empty()) { + return; + } + + // First, send all the extracted keys as pressed keys to the client. + for (const auto &key : keys) { + waylandServer()->seat()->notifyKeyboardKey(key, KWaylandServer::KeyboardKeyState::Pressed); + } + + // Then, send key release for those keys in reverse. + for (auto itr = keys.rbegin(); itr != keys.rend(); ++itr) { + // Since we are faking key events, we do not have distinct press/release + // events. So instead, just queue the button release so it gets sent + // a few moments after the press. + auto key = *itr; + QMetaObject::invokeMethod( + this, [key]() { + waylandServer()->seat()->notifyKeyboardKey(key, KWaylandServer::KeyboardKeyState::Released); + }, + Qt::QueuedConnection); + } } } @@ -579,12 +629,16 @@ void InputMethod::adoptInputMethodContext() inputContext->sendContentType(t2->contentHints(), t2->contentPurpose()); connect(inputContext, &KWaylandServer::InputMethodContextV1Interface::language, this, &InputMethod::setLanguage); connect(inputContext, &KWaylandServer::InputMethodContextV1Interface::textDirection, this, &InputMethod::setTextDirection); - } - - if (t3 && t3->isEnabled()) { + } else if (t3 && t3->isEnabled()) { inputContext->sendSurroundingText(t3->surroundingText(), t3->surroundingTextCursorPosition(), t3->surroundingTextSelectionAnchor()); inputContext->sendContentType(t3->contentHints(), t3->contentPurpose()); + } else { + // When we have neither text-input-v2 nor text-input-v3 we can only send + // fake key events, not more complex text. So ask the input method to + // only send basic characters without any pre-editing. + inputContext->sendContentType(KWaylandServer::TextInputContentHint::Latin, KWaylandServer::TextInputContentPurpose::Normal); } + inputContext->sendCommitState(m_serial++); connect(inputContext, &KWaylandServer::InputMethodContextV1Interface::keysym, this, &InputMethod::keysymReceived, Qt::UniqueConnection);