From 345736735e6668ff5dd91f2e40bfad70482ffe2d Mon Sep 17 00:00:00 2001 From: Arjen Hiemstra Date: Tue, 17 May 2022 13:53:13 +0200 Subject: [PATCH] Add a fallback path for input when there is no text-input An application that does not support text-input has no way of communicating with the input method, so even if you show the input method the application receives nothing. As a fallback, instead send fake key events so the application still gets something at least. The key events are synthesised based on the text string that the input method sends, which may result in things that do not actually correspond to real keys. Unfortunately I do not see a way around that. CCBUG: 439911 --- autotests/integration/inputmethod_test.cpp | 74 +++++++++++++++++++ src/inputmethod.cpp | 82 ++++++++++++++++++---- 2 files changed, 142 insertions(+), 14 deletions(-) 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);