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
This commit is contained in:
Arjen Hiemstra 2022-05-17 13:53:13 +02:00
parent 076203c926
commit 345736735e
2 changed files with 142 additions and 14 deletions

View file

@ -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 <QDBusConnection>
#include <QDBusMessage>
@ -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<KWayland::Client::Surface> surface = Test::createSurface();
std::unique_ptr<Test::XdgToplevel> 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<QVariant> &input, quint32 key, Keyboard::KeyState state) {
auto inputKey = input.at(0).toInt();
auto inputState = input.at(1).value<Keyboard::KeyState>();
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"

View file

@ -33,6 +33,7 @@
#include <KLocalizedString>
#include <KShell>
#include <KKeyServer>
#include <QDBusConnection>
#include <QDBusMessage>
@ -49,6 +50,34 @@ using namespace KWaylandServer;
namespace KWin
{
static std::vector<quint32> 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);