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:
parent
1e9b961761
commit
6675eccf6d
5 changed files with 203 additions and 4 deletions
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue