inputmethod: Send pre-defined commit message to client on user interaction

In some IM backends pre-edit text should be submitted on user interaction, in some it should be discarded. 

In TextInputV1 and V2 this was a flag sent to the client along with
the commit string ahead of time.

TextInputV3 does not have a flag for this, so we handle it compositor side.

We flush the text to be committed :
 - when we change keyboard focus, before the current client gets wl_keyboard.leave
 
 - when a mouse is pressed in the relevant surface
 
 - when a key is pressed and the InputMethod doesn't have a grab
 
 - when the InputMethod forwards a key to the client
 (which includes the InputMethod passing on grabbed keys)
This commit is contained in:
David Edmundson 2024-08-02 09:07:46 +00:00
parent 1e9b961761
commit 6675eccf6d
5 changed files with 203 additions and 4 deletions

View file

@ -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<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());
auto textInputV3 = std::make_unique<Test::TextInputV3>();
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<Window *> 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<KWayland::Client::Surface> surface2(Test::createSurface());
std::unique_ptr<Test::XdgToplevel> 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"

View file

@ -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

View file

@ -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<InternalWindowEventFilter>();
installInputEventFilter(m_internalWindowFilter.get());
m_inputKeyboardFilter = std::make_unique<InputKeyboardFilter>();
m_inputKeyboardFilter = std::make_unique<InputMethodEventFilter>();
installInputEventFilter(m_inputKeyboardFilter.get());
m_forwardFilter = std::make_unique<ForwardInputFilter>();

View file

@ -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;

View file

@ -15,7 +15,7 @@
#include <QObject>
#include "effect/globals.h"
#include "input_event_spy.h"
#include <kwin_export.h>
#include <QPointer>
@ -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<std::pair<quint32, quint32>> 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<InputPanelV1Window> m_panel;