diff --git a/autotests/integration/inputmethod_test.cpp b/autotests/integration/inputmethod_test.cpp index 5a7af44b28..855dc035cf 100644 --- a/autotests/integration/inputmethod_test.cpp +++ b/autotests/integration/inputmethod_test.cpp @@ -57,6 +57,7 @@ private Q_SLOTS: void testEnableActive(); void testHidePanel(); void testSwitchFocusedSurfaces(); + void testV3Styling(); private: void touchNow() { @@ -339,6 +340,96 @@ void InputMethodTest::testSwitchFocusedSurfaces() } } +void InputMethodTest::testV3Styling() +{ + // Create an xdg_toplevel surface and wait for the compositor to catch up. + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgToplevelSurface(surface.data())); + AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::red); + QVERIFY(client); + QVERIFY(client->isActive()); + QCOMPARE(client->frameGeometry().size(), QSize(1280, 1024)); + + Test::TextInputV3 *textInputV3 = new Test::TextInputV3(); + textInputV3->init(Test::waylandTextInputManagerV3()->get_text_input(*(Test::waylandSeat()))); + textInputV3->enable(); + + QSignalSpy inputMethodActiveSpy(InputMethod::self(), &InputMethod::activeChanged); + QSignalSpy inputMethodActivateSpy(Test::inputMethod(), &Test::MockInputMethod::activate); + // just enabling the text-input should not show it but rather on commit + QVERIFY(!InputMethod::self()->isActive()); + textInputV3->commit(); + QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait()); + QVERIFY(InputMethod::self()->isActive()); + QVERIFY(inputMethodActivateSpy.wait()); + auto context = Test::inputMethod()->context(); + QSignalSpy textInputPreeditSpy(textInputV3, &Test::TextInputV3::preeditString); + zwp_input_method_context_v1_preedit_cursor(context, 0); + zwp_input_method_context_v1_preedit_styling(context, 0, 3, 7); + zwp_input_method_context_v1_preedit_string(context, 0, "ABCD", "ABCD"); + QVERIFY(textInputPreeditSpy.wait()); + QCOMPARE(textInputPreeditSpy.last().at(0), QString("ABCD")); + QCOMPARE(textInputPreeditSpy.last().at(1), 0); + QCOMPARE(textInputPreeditSpy.last().at(2), 0); + + zwp_input_method_context_v1_preedit_cursor(context, 1); + zwp_input_method_context_v1_preedit_styling(context, 0, 3, 7); + zwp_input_method_context_v1_preedit_string(context, 0, "ABCDE", "ABCDE"); + QVERIFY(textInputPreeditSpy.wait()); + QCOMPARE(textInputPreeditSpy.last().at(0), QString("ABCDE")); + QCOMPARE(textInputPreeditSpy.last().at(1), 1); + QCOMPARE(textInputPreeditSpy.last().at(2), 1); + + zwp_input_method_context_v1_preedit_cursor(context, 2); + // Use selection for [2, 2+2) + zwp_input_method_context_v1_preedit_styling(context, 2, 2, 6); + // Use high light for [3, 3+3) + zwp_input_method_context_v1_preedit_styling(context, 3, 3, 4); + zwp_input_method_context_v1_preedit_string(context, 0, "ABCDEF", "ABCDEF"); + QVERIFY(textInputPreeditSpy.wait()); + QCOMPARE(textInputPreeditSpy.last().at(0), QString("ABCDEF")); + // Merged range should be [2, 6) + QCOMPARE(textInputPreeditSpy.last().at(1), 2); + QCOMPARE(textInputPreeditSpy.last().at(2), 6); + + zwp_input_method_context_v1_preedit_cursor(context, 2); + // Use selection for [0, 0+2) + zwp_input_method_context_v1_preedit_styling(context, 0, 2, 6); + // Use high light for [3, 3+3) + zwp_input_method_context_v1_preedit_styling(context, 3, 3, 4); + zwp_input_method_context_v1_preedit_string(context, 0, "ABCDEF", "ABCDEF"); + QVERIFY(textInputPreeditSpy.wait()); + QCOMPARE(textInputPreeditSpy.last().at(0), QString("ABCDEF")); + // Merged range should be none, because of the disjunction highlight. + QCOMPARE(textInputPreeditSpy.last().at(1), 2); + QCOMPARE(textInputPreeditSpy.last().at(2), 2); + + zwp_input_method_context_v1_preedit_cursor(context, 1); + // Use selection for [0, 0+2) + zwp_input_method_context_v1_preedit_styling(context, 0, 2, 6); + // Use high light for [2, 2+3) + zwp_input_method_context_v1_preedit_styling(context, 2, 3, 4); + zwp_input_method_context_v1_preedit_string(context, 0, "ABCDEF", "ABCDEF"); + QVERIFY(textInputPreeditSpy.wait()); + QCOMPARE(textInputPreeditSpy.last().at(0), QString("ABCDEF")); + // Merged range should be none, starting offset does not match. + QCOMPARE(textInputPreeditSpy.last().at(1), 1); + QCOMPARE(textInputPreeditSpy.last().at(2), 1); + + // Use different order of styling and cursor + // Use high light for [3, 3+3) + zwp_input_method_context_v1_preedit_styling(context, 3, 3, 4); + zwp_input_method_context_v1_preedit_cursor(context, 1); + // Use selection for [1, 1+2) + zwp_input_method_context_v1_preedit_styling(context, 1, 2, 6); + zwp_input_method_context_v1_preedit_string(context, 0, "ABCDEF", "ABCDEF"); + QVERIFY(textInputPreeditSpy.wait()); + QCOMPARE(textInputPreeditSpy.last().at(0), QString("ABCDEF")); + // Merged range should be [1,6). + QCOMPARE(textInputPreeditSpy.last().at(1), 1); + QCOMPARE(textInputPreeditSpy.last().at(2), 6); +} + WAYLANDTEST_MAIN(InputMethodTest) #include "inputmethod_test.moc" diff --git a/autotests/integration/kwin_wayland_test.h b/autotests/integration/kwin_wayland_test.h index b2c30ae6f3..798c1cf357 100644 --- a/autotests/integration/kwin_wayland_test.h +++ b/autotests/integration/kwin_wayland_test.h @@ -9,18 +9,22 @@ #ifndef KWIN_WAYLAND_TEST_H #define KWIN_WAYLAND_TEST_H +#include "abstract_client.h" #include "main.h" // Qt #include +#include + #include "qwayland-idle-inhibit-unstable-v1.h" -#include "qwayland-wlr-layer-shell-unstable-v1.h" -#include "qwayland-text-input-unstable-v3.h" -#include "qwayland-xdg-decoration-unstable-v1.h" -#include "qwayland-xdg-shell.h" +#include "qwayland-input-method-unstable-v1.h" #include "qwayland-kde-output-device-v2.h" #include "qwayland-kde-output-management-v2.h" +#include "qwayland-text-input-unstable-v3.h" +#include "qwayland-wlr-layer-shell-unstable-v1.h" +#include "qwayland-xdg-decoration-unstable-v1.h" +#include "qwayland-xdg-shell.h" namespace KWayland { @@ -93,9 +97,20 @@ public: ~TextInputManagerV3() override { destroy(); } }; -class TextInputV3 : public QtWayland::zwp_text_input_v3 +class TextInputV3 : public QObject, public QtWayland::zwp_text_input_v3 { + Q_OBJECT +public: ~TextInputV3() override { destroy(); } + +Q_SIGNALS: + void preeditString(const QString &text, int cursor_begin, int cursor_end); + +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); + } }; class LayerShellV1 : public QtWayland::zwlr_layer_shell_v1 @@ -393,6 +408,40 @@ private: uint32_t m_rgbRange; }; +class MockInputMethod : public QObject, QtWayland::zwp_input_method_v1 +{ + Q_OBJECT +public: + MockInputMethod(struct wl_registry *registry, int id, int version); + ~MockInputMethod(); + + AbstractClient *client() const + { + return m_client; + } + KWayland::Client::Surface *inputPanelSurface() const + { + return m_inputSurface; + } + auto *context() const + { + return m_context; + } + +Q_SIGNALS: + void activate(); + +protected: + void zwp_input_method_v1_activate(struct ::zwp_input_method_context_v1 *context) override; + void zwp_input_method_v1_deactivate(struct ::zwp_input_method_context_v1 *context) override; + +private: + QPointer m_inputSurface; + QtWayland::zwp_input_panel_surface_v1 *m_inputMethodSurface = nullptr; + QPointer m_client; + struct ::zwp_input_method_context_v1 *m_context = nullptr; +}; + enum class AdditionalWaylandInterface { Seat = 1 << 0, Decoration = 1 << 1, @@ -528,6 +577,7 @@ bool unlockScreen(); void initWaylandWorkspace(); AbstractClient *inputPanelClient(); +MockInputMethod *inputMethod(); KWayland::Client::Surface *inputPanelSurface(); } diff --git a/autotests/integration/test_helpers.cpp b/autotests/integration/test_helpers.cpp index 9f10e4f372..cf13333142 100644 --- a/autotests/integration/test_helpers.cpp +++ b/autotests/integration/test_helpers.cpp @@ -7,11 +7,9 @@ SPDX-License-Identifier: GPL-2.0-or-later */ #include "kwin_wayland_test.h" -#include "abstract_client.h" #include "screenlockerwatcher.h" #include "wayland_server.h" #include "workspace.h" -#include "qwayland-input-method-unstable-v1.h" #include "inputmethod.h" #include @@ -249,30 +247,16 @@ static struct { TextInputManagerV3 *textInputManagerV3 = nullptr; } s_waylandConnection; -class MockInputMethod : public QtWayland::zwp_input_method_v1 -{ -public: - MockInputMethod(struct wl_registry *registry, int id, int version); - ~MockInputMethod(); - - AbstractClient *client() const { return m_client; } - KWayland::Client::Surface *inputPanelSurface() const { return m_inputSurface; } - -protected: - void zwp_input_method_v1_activate(struct ::zwp_input_method_context_v1 *context) override; - void zwp_input_method_v1_deactivate(struct ::zwp_input_method_context_v1 *context) override; - -private: - QPointer m_inputSurface; - QtWayland::zwp_input_panel_surface_v1 *m_inputMethodSurface = nullptr; - QPointer m_client; -}; - AbstractClient *inputPanelClient() { return s_waylandConnection.inputMethodV1->client(); } +MockInputMethod *inputMethod() +{ + return s_waylandConnection.inputMethodV1; +} + KWayland::Client::Surface *inputPanelSurface() { return s_waylandConnection.inputMethodV1->inputPanelSurface(); @@ -295,11 +279,16 @@ void MockInputMethod::zwp_input_method_v1_activate(struct ::zwp_input_method_con m_inputMethodSurface = Test::createInputPanelSurfaceV1(m_inputSurface, s_waylandConnection.outputs.first()); } m_client = Test::renderAndWaitForShown(m_inputSurface, QSize(1280, 400), Qt::blue); + m_context = context; + + Q_EMIT activate(); } void MockInputMethod::zwp_input_method_v1_deactivate(struct ::zwp_input_method_context_v1 *context) { + QCOMPARE(context, m_context); zwp_input_method_context_v1_destroy(context); + m_context = nullptr; if (m_inputSurface) { m_inputSurface->release(); diff --git a/src/inputmethod.cpp b/src/inputmethod.cpp index 060d1bc03d..cc87b0ac9b 100644 --- a/src/inputmethod.cpp +++ b/src/inputmethod.cpp @@ -300,9 +300,7 @@ void InputMethod::textInputInterfaceV3EnabledChanged() setActive(t3->isEnabled()); if (!t3->isEnabled()) { // reset value of preedit when textinput is disabled - preedit.text = QString(); - preedit.begin = 0; - preedit.end = 0; + resetPendingPreedit(); } auto context = waylandServer()->inputMethod()->context(); if (context) { @@ -479,8 +477,7 @@ void InputMethod::setPreeditCursor(qint32 index) } auto t3 = waylandServer()->seat()->textInputV3(); if (t3 && t3->isEnabled()) { - preedit.begin = index; - preedit.end = index; + preedit.cursor = index; } } @@ -490,6 +487,13 @@ void InputMethod::setPreeditStyling(quint32 index, quint32 length, quint32 style if (t2 && t2->isEnabled()) { t2->preEditStyling(index, length, style); } + auto t3 = waylandServer()->seat()->textInputV3(); + if (t3 && t3->isEnabled()) { + // preedit style: highlight(4) or selection(6) + if (style == 4 || style == 6) { + preedit.highlightRanges.emplace_back(index, index + length); + } + } } void InputMethod::setPreeditString(uint32_t serial, const QString &text, const QString &commit) @@ -503,10 +507,36 @@ void InputMethod::setPreeditString(uint32_t serial, const QString &text, const Q if (t3 && t3->isEnabled()) { preedit.text = text; if (!preedit.text.isEmpty()) { - t3->sendPreEditString(preedit.text, preedit.begin, preedit.end); + quint32 cursor = 0, cursorEnd = 0; + if (preedit.cursor > 0) { + cursor = cursorEnd = preedit.cursor; + } + // Check if we can convert highlight style to a range of selection. + if (!preedit.highlightRanges.empty()) { + std::sort(preedit.highlightRanges.begin(), preedit.highlightRanges.end()); + // Check if starting point matches. + if (preedit.highlightRanges.front().first == cursor) { + quint32 end = preedit.highlightRanges.front().second; + bool nonContinousHighlight = false; + for (size_t i = 1 ; i < preedit.highlightRanges.size(); i ++) { + if (end >= preedit.highlightRanges[i].first) { + end = std::max(end, preedit.highlightRanges[i].second); + } else { + nonContinousHighlight = true; + break; + } + } + if (!nonContinousHighlight) { + cursorEnd = end; + } + } + } + + t3->sendPreEditString(preedit.text, cursor, cursorEnd); } t3->done(); } + resetPendingPreedit(); } void InputMethod::key(quint32 /*serial*/, quint32 /*time*/, quint32 keyCode, bool pressed) @@ -712,4 +742,10 @@ bool InputMethod::isAvailable() const return !m_inputMethodCommand.isEmpty(); } +void InputMethod::resetPendingPreedit() { + preedit.text = QString(); + preedit.cursor = 0; + preedit.highlightRanges.clear(); +} + } diff --git a/src/inputmethod.h b/src/inputmethod.h index e29481ada4..c2c00fc260 100644 --- a/src/inputmethod.h +++ b/src/inputmethod.h @@ -9,6 +9,9 @@ #ifndef KWIN_VIRTUAL_KEYBOARD_H #define KWIN_VIRTUAL_KEYBOARD_H +#include +#include + #include #include @@ -99,11 +102,12 @@ private: bool touchEventTriggered() const; void forwardModifiers(); + void resetPendingPreedit(); struct { QString text = QString(); - quint32 begin = 0; - quint32 end = 0; + qint32 cursor = 0; + std::vector> highlightRanges; } preedit; bool m_enabled = true;