KWin
Loading...
Searching...
No Matches
inputmethod_test.cpp
Go to the documentation of this file.
1/*
2 KWin - the KDE window manager
3 This file is part of the KDE project.
4
5 SPDX-FileCopyrightText: 2020 Marco Martin <mart@kde.org>
6
7 SPDX-License-Identifier: GPL-2.0-or-later
8*/
9#include "kwin_wayland_test.h"
10
11#include "core/output.h"
12#include "inputmethod.h"
13#include "inputpanelv1window.h"
14#include "keyboard_input.h"
15#include "pointer_input.h"
16#include "qwayland-input-method-unstable-v1.h"
17#include "qwayland-text-input-unstable-v3.h"
20#include "wayland/display.h"
21#include "wayland/seat.h"
22#include "wayland/surface.h"
23#include "wayland_server.h"
24#include "window.h"
25#include "workspace.h"
26#include "xkb.h"
27
28#include <QDBusConnection>
29#include <QDBusMessage>
30#include <QDBusPendingReply>
31#include <QSignalSpy>
32#include <QTest>
33
34#include <KWayland/Client/compositor.h>
35#include <KWayland/Client/keyboard.h>
36#include <KWayland/Client/output.h>
37#include <KWayland/Client/region.h>
38#include <KWayland/Client/seat.h>
39#include <KWayland/Client/surface.h>
40#include <KWayland/Client/textinput.h>
41#include <linux/input-event-codes.h>
42
43using namespace KWin;
45
46static const QString s_socketName = QStringLiteral("wayland_test_kwin_inputmethod-0");
47
48class InputMethodTest : public QObject
49{
50 Q_OBJECT
51private Q_SLOTS:
52 void initTestCase();
53 void init();
54 void cleanup();
55
56 void testOpenClose();
57 void testEnableDisableV3();
58 void testEnableActive();
59 void testHidePanel();
60 void testReactivateFocus();
61 void testSwitchFocusedSurfaces();
62 void testV2V3SameClient();
63 void testV3Styling();
64 void testDisableShowInputPanel();
65 void testModifierForwarding();
66 void testFakeEventFallback();
67 void testOverlayPositioning_data();
68 void testOverlayPositioning();
69
70private:
71 void touchNow()
72 {
73 static int time = 0;
74 Test::touchDown(0, {100, 100}, ++time);
75 Test::touchUp(0, ++time);
76 }
77};
78
79void InputMethodTest::initTestCase()
80{
81 QDBusConnection::sessionBus().registerService(QStringLiteral("org.kde.kwin.testvirtualkeyboard"));
82
83 qRegisterMetaType<KWin::Window *>();
84 qRegisterMetaType<KWayland::Client::Output *>();
85
86 QSignalSpy applicationStartedSpy(kwinApp(), &Application::started);
87 QVERIFY(waylandServer()->init(s_socketName));
89 QRect(0, 0, 1280, 1024),
90 QRect(1280, 0, 1280, 1024),
91 });
92
93 static_cast<WaylandTestApplication *>(kwinApp())->setInputMethodServerToStart("internal");
94 kwinApp()->start();
95 QVERIFY(applicationStartedSpy.wait());
96 const auto outputs = workspace()->outputs();
97 QCOMPARE(outputs.count(), 2);
98 QCOMPARE(outputs[0]->geometry(), QRect(0, 0, 1280, 1024));
99 QCOMPARE(outputs[1]->geometry(), QRect(1280, 0, 1280, 1024));
100}
101
102void InputMethodTest::init()
103{
104 workspace()->setActiveOutput(QPoint(640, 512));
105 KWin::input()->pointer()->warp(QPoint(640, 512));
106
107 touchNow();
108
109 QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::TextInputManagerV2 | Test::AdditionalWaylandInterface::InputMethodV1 | Test::AdditionalWaylandInterface::TextInputManagerV3));
110
111 kwinApp()->inputMethod()->setEnabled(true);
112}
113
114void InputMethodTest::cleanup()
115{
117}
118
119void InputMethodTest::testOpenClose()
120{
121 QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded);
122 QSignalSpy windowRemovedSpy(workspace(), &Workspace::windowRemoved);
123
124 // Create an xdg_toplevel surface and wait for the compositor to catch up.
125 std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
126 std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
127 Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red);
128 QVERIFY(window);
129 QVERIFY(window->isActive());
130 QCOMPARE(window->frameGeometry().size(), QSize(1280, 1024));
131 QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged);
132 QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested);
133 QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested);
134 QVERIFY(surfaceConfigureRequestedSpy.wait());
135
136 std::unique_ptr<KWayland::Client::TextInput> textInput(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat()));
137 QVERIFY(textInput != nullptr);
138
139 // Show the keyboard
140 touchNow();
141 textInput->enable(surface.get());
142 textInput->showInputPanel();
143 QSignalSpy paneladded(kwinApp()->inputMethod(), &KWin::InputMethod::panelChanged);
144 QVERIFY(windowAddedSpy.wait());
145 QCOMPARE(paneladded.count(), 1);
146
147 Window *keyboardClient = windowAddedSpy.last().first().value<Window *>();
148 QVERIFY(keyboardClient);
149 QVERIFY(keyboardClient->isInputMethod());
150
151 // Do the actual resize
152 QVERIFY(surfaceConfigureRequestedSpy.wait());
153
154 Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().value<QSize>(), Qt::red);
155 QVERIFY(frameGeometryChangedSpy.wait());
156
157 QCOMPARE(window->frameGeometry().height(), 1024 - keyboardClient->frameGeometry().height());
158
159 // Hide the keyboard
160 textInput->hideInputPanel();
161
162 QVERIFY(surfaceConfigureRequestedSpy.wait());
163 Test::render(surface.get(), toplevelConfigureRequestedSpy.last().first().value<QSize>(), Qt::red);
164 QVERIFY(frameGeometryChangedSpy.wait());
165
166 QCOMPARE(window->frameGeometry().height(), 1024);
167
168 // show the keyboard again
169 touchNow();
170 textInput->enable(surface.get());
171 textInput->showInputPanel();
172
173 QVERIFY(surfaceConfigureRequestedSpy.wait());
174 QVERIFY(keyboardClient->isShown());
175
176 // Destroy the test window.
177 shellSurface.reset();
178 QVERIFY(Test::waitForWindowClosed(window));
179}
180
181void InputMethodTest::testEnableDisableV3()
182{
183 // Create an xdg_toplevel surface and wait for the compositor to catch up.
184 std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
185 std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
186 Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red);
187 QVERIFY(window);
188 QVERIFY(window->isActive());
189 QCOMPARE(window->frameGeometry().size(), QSize(1280, 1024));
190
191 auto textInputV3 = std::make_unique<Test::TextInputV3>();
192 textInputV3->init(Test::waylandTextInputManagerV3()->get_text_input(*(Test::waylandSeat())));
193
194 // Show the keyboard
195 touchNow();
196 textInputV3->enable();
197
198 QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded);
199 QSignalSpy inputMethodActiveSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged);
200 // just enabling the text-input should not show it but rather on commit
201 QVERIFY(!kwinApp()->inputMethod()->isActive());
202 textInputV3->commit();
203 QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait());
204 QVERIFY(kwinApp()->inputMethod()->isActive());
205
206 QVERIFY(windowAddedSpy.wait());
207 Window *keyboardClient = windowAddedSpy.last().first().value<Window *>();
208 QVERIFY(keyboardClient);
209 QVERIFY(keyboardClient->isInputMethod());
210 QVERIFY(keyboardClient->isShown());
211
212 // Text input v3 doesn't have hideInputPanel, just simiulate the hide from dbus call
213 kwinApp()->inputMethod()->hide();
214 QVERIFY(!keyboardClient->isShown());
215
216 QSignalSpy windowShownSpy(keyboardClient, &Window::windowShown);
217 // Force enable the text input object. This is what's done by Gtk.
218 textInputV3->enable();
219 textInputV3->commit();
220
221 windowShownSpy.wait();
222 QVERIFY(keyboardClient->isShown());
223
224 // disable text input and ensure that it is not hiding input panel without commit
225 inputMethodActiveSpy.clear();
226 QVERIFY(kwinApp()->inputMethod()->isActive());
227 textInputV3->disable();
228 textInputV3->commit();
229 QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait());
230 QVERIFY(!kwinApp()->inputMethod()->isActive());
231}
232
233void InputMethodTest::testEnableActive()
234{
235 // This test verifies that enabling text-input twice won't change the active input method status.
236 QVERIFY(!kwinApp()->inputMethod()->isActive());
237
238 // Create an xdg_toplevel surface and wait for the compositor to catch up.
239 std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
240 std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
241 Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red);
242 QVERIFY(window);
243 QVERIFY(window->isActive());
244
245 // Show the keyboard
246 QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded);
247 std::unique_ptr<KWayland::Client::TextInput> textInput(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat()));
248 textInput->enable(surface.get());
249 QSignalSpy paneladded(kwinApp()->inputMethod(), &KWin::InputMethod::panelChanged);
250 QVERIFY(paneladded.wait());
251 textInput->showInputPanel();
252 QVERIFY(windowAddedSpy.wait());
253 QVERIFY(kwinApp()->inputMethod()->isActive());
254
255 // Ask the keyboard to be shown again.
256 QSignalSpy activateSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged);
257 textInput->enable(surface.get());
258 textInput->showInputPanel();
259 activateSpy.wait(200);
260 QVERIFY(activateSpy.isEmpty());
261 QVERIFY(kwinApp()->inputMethod()->isActive());
262
263 // Destroy the test window.
264 shellSurface.reset();
265 QVERIFY(Test::waitForWindowClosed(window));
266}
267
268void InputMethodTest::testHidePanel()
269{
270 QVERIFY(!kwinApp()->inputMethod()->isActive());
271
272 touchNow();
273 QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded);
274 QSignalSpy windowRemovedSpy(workspace(), &Workspace::windowRemoved);
275
276 QSignalSpy activateSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged);
277 std::unique_ptr<KWayland::Client::TextInput> textInput(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat()));
278
279 // Create an xdg_toplevel surface and wait for the compositor to catch up.
280 std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
281 std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
282 Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red);
283 waylandServer()->seat()->setFocusedTextInputSurface(window->surface());
284
285 textInput->enable(surface.get());
286 QSignalSpy paneladded(kwinApp()->inputMethod(), &KWin::InputMethod::panelChanged);
287 QVERIFY(paneladded.wait());
288 textInput->showInputPanel();
289 QVERIFY(windowAddedSpy.wait());
290
291 QCOMPARE(workspace()->activeWindow(), window);
292
293 QCOMPARE(windowAddedSpy.count(), 2);
294 QVERIFY(activateSpy.count() || activateSpy.wait());
295 QVERIFY(kwinApp()->inputMethod()->isActive());
296
297 auto keyboardWindow = kwinApp()->inputMethod()->panel();
298 auto ipsurface = Test::inputPanelSurface();
299 QVERIFY(keyboardWindow);
300 windowRemovedSpy.clear();
301 delete ipsurface;
302 QVERIFY(kwinApp()->inputMethod()->isVisible());
303 QVERIFY(windowRemovedSpy.count() || windowRemovedSpy.wait());
304 QVERIFY(!kwinApp()->inputMethod()->isVisible());
305
306 // Destroy the test window.
307 shellSurface.reset();
308 QVERIFY(Test::waitForWindowClosed(window));
309}
310
311void InputMethodTest::testReactivateFocus()
312{
313 touchNow();
314 QVERIFY(!kwinApp()->inputMethod()->isActive());
315
316 std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
317 std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
318 Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red);
319 QVERIFY(window);
320 QVERIFY(window->isActive());
321 QCOMPARE(window->frameGeometry().size(), QSize(1280, 1024));
322
323 // Show the keyboard
324 QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded);
325 std::unique_ptr<KWayland::Client::TextInput> textInput(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat()));
326 textInput->enable(surface.get());
327 QSignalSpy paneladded(kwinApp()->inputMethod(), &KWin::InputMethod::panelChanged);
328 QVERIFY(paneladded.wait());
329 textInput->showInputPanel();
330 QVERIFY(windowAddedSpy.wait());
331 QVERIFY(kwinApp()->inputMethod()->isActive());
332
333 QSignalSpy activeSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged);
334
335 // Hide keyboard like keyboardToggle button on navigation panel
336 kwinApp()->inputMethod()->setActive(false);
337 activeSpy.wait(200);
338 QVERIFY(!kwinApp()->inputMethod()->isActive());
339
340 // Reactivate
341 textInput->enable(surface.get());
342 textInput->showInputPanel();
343 activeSpy.wait(200);
344 QVERIFY(kwinApp()->inputMethod()->isActive());
345
346 // Destroy the test window
347 shellSurface.reset();
348 QVERIFY(Test::waitForWindowClosed(window));
349}
350
351void InputMethodTest::testSwitchFocusedSurfaces()
352{
353 touchNow();
354 QVERIFY(!kwinApp()->inputMethod()->isActive());
355
356 QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded);
357 QSignalSpy windowRemovedSpy(workspace(), &Workspace::windowRemoved);
358
359 QSignalSpy activateSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged);
360 std::unique_ptr<KWayland::Client::TextInput> textInput(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat()));
361
362 QList<Window *> windows;
363 std::vector<std::unique_ptr<KWayland::Client::Surface>> surfaces;
364 QList<Test::XdgToplevel *> toplevels;
365 // We create 3 surfaces
366 for (int i = 0; i < 3; ++i) {
367 std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
368 auto shellSurface = Test::createXdgToplevelSurface(surface.get());
369 windows += Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red);
370 QCOMPARE(workspace()->activeWindow(), windows.constLast());
371 surfaces.push_back(std::move(surface));
372 toplevels += shellSurface;
373 }
374 QCOMPARE(windowAddedSpy.count(), 3);
375 waylandServer()->seat()->setFocusedTextInputSurface(windows.constFirst()->surface());
376
377 QVERIFY(!kwinApp()->inputMethod()->isActive());
378 textInput->enable(surfaces.back().get());
379 QVERIFY(!kwinApp()->inputMethod()->isActive());
380 waylandServer()->seat()->setFocusedTextInputSurface(windows.first()->surface());
381 QVERIFY(!kwinApp()->inputMethod()->isActive());
382 activateSpy.clear();
383 waylandServer()->seat()->setFocusedTextInputSurface(windows.last()->surface());
384 QVERIFY(activateSpy.count() || activateSpy.wait());
385 QVERIFY(kwinApp()->inputMethod()->isActive());
386
387 activateSpy.clear();
388 waylandServer()->seat()->setFocusedTextInputSurface(windows.first()->surface());
389 QVERIFY(activateSpy.count() || activateSpy.wait());
390 QVERIFY(!kwinApp()->inputMethod()->isActive());
391
392 // Destroy the test window.
393 for (int i = 0; i < windows.count(); ++i) {
394 delete toplevels[i];
395 QVERIFY(Test::waitForWindowClosed(windows[i]));
396 }
397}
398
399void InputMethodTest::testV2V3SameClient()
400{
401 touchNow();
402 QVERIFY(!kwinApp()->inputMethod()->isActive());
403
404 QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded);
405 QSignalSpy windowRemovedSpy(workspace(), &Workspace::windowRemoved);
406
407 QSignalSpy activateSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged);
408 std::unique_ptr<KWayland::Client::TextInput> textInput(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat()));
409
410 auto textInputV3 = std::make_unique<Test::TextInputV3>();
411 textInputV3->init(Test::waylandTextInputManagerV3()->get_text_input(*(Test::waylandSeat())));
412
413 std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
414 std::unique_ptr<Test::XdgToplevel> toplevel(Test::createXdgToplevelSurface(surface.get()));
415 Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red);
416 QCOMPARE(workspace()->activeWindow(), window);
417 QCOMPARE(windowAddedSpy.count(), 1);
418 waylandServer()->seat()->setFocusedTextInputSurface(window->surface());
419 QVERIFY(!kwinApp()->inputMethod()->isActive());
420
421 // Enable and disable v2
422 textInput->enable(surface.get());
423 QVERIFY(activateSpy.count() || activateSpy.wait());
424 QVERIFY(kwinApp()->inputMethod()->isActive());
425
426 activateSpy.clear();
427 textInput->disable(surface.get());
428 QVERIFY(activateSpy.count() || activateSpy.wait());
429 QVERIFY(!kwinApp()->inputMethod()->isActive());
430
431 // Enable and disable v3
432 activateSpy.clear();
433 textInputV3->enable();
434 textInputV3->commit();
435 QVERIFY(activateSpy.count() || activateSpy.wait());
436 QVERIFY(kwinApp()->inputMethod()->isActive());
437
438 activateSpy.clear();
439 textInputV3->disable();
440 textInputV3->commit();
441 activateSpy.clear();
442 QVERIFY(activateSpy.count() || activateSpy.wait());
443 QVERIFY(!kwinApp()->inputMethod()->isActive());
444
445 // Enable v2 and v3
446 activateSpy.clear();
447 textInputV3->enable();
448 textInputV3->commit();
449 textInput->enable(surface.get());
450 QVERIFY(activateSpy.count() || activateSpy.wait());
451 QVERIFY(kwinApp()->inputMethod()->isActive());
452
453 // Disable v3, should still be active since v2 is active.
454 activateSpy.clear();
455 textInputV3->disable();
456 textInputV3->commit();
457 activateSpy.wait(200);
458 QVERIFY(kwinApp()->inputMethod()->isActive());
459
460 // Disable v2
461 activateSpy.clear();
462 textInput->disable(surface.get());
463 QVERIFY(activateSpy.count() || activateSpy.wait());
464 QVERIFY(!kwinApp()->inputMethod()->isActive());
465
466 toplevel.reset();
467 QVERIFY(Test::waitForWindowClosed(window));
468}
469
470void InputMethodTest::testV3Styling()
471{
472 // Create an xdg_toplevel surface and wait for the compositor to catch up.
473 std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
474 std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
475 Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red);
476 QVERIFY(window);
477 QVERIFY(window->isActive());
478 QCOMPARE(window->frameGeometry().size(), QSize(1280, 1024));
479
480 auto textInputV3 = std::make_unique<Test::TextInputV3>();
481 textInputV3->init(Test::waylandTextInputManagerV3()->get_text_input(*(Test::waylandSeat())));
482 textInputV3->enable();
483
484 QSignalSpy inputMethodActiveSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged);
485 QSignalSpy inputMethodActivateSpy(Test::inputMethod(), &Test::MockInputMethod::activate);
486 // just enabling the text-input should not show it but rather on commit
487 QVERIFY(!kwinApp()->inputMethod()->isActive());
488 textInputV3->commit();
489 QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait());
490 QVERIFY(kwinApp()->inputMethod()->isActive());
491 QVERIFY(inputMethodActivateSpy.wait());
492 auto context = Test::inputMethod()->context();
493 QSignalSpy textInputPreeditSpy(textInputV3.get(), &Test::TextInputV3::preeditString);
494 zwp_input_method_context_v1_preedit_cursor(context, 0);
495 zwp_input_method_context_v1_preedit_styling(context, 0, 3, 7);
496 zwp_input_method_context_v1_preedit_string(context, 0, "ABCD", "ABCD");
497 QVERIFY(textInputPreeditSpy.wait());
498 QCOMPARE(textInputPreeditSpy.last().at(0), QString("ABCD"));
499 QCOMPARE(textInputPreeditSpy.last().at(1), 0);
500 QCOMPARE(textInputPreeditSpy.last().at(2), 0);
501
502 zwp_input_method_context_v1_preedit_cursor(context, 1);
503 zwp_input_method_context_v1_preedit_styling(context, 0, 3, 7);
504 zwp_input_method_context_v1_preedit_string(context, 0, "ABCDE", "ABCDE");
505 QVERIFY(textInputPreeditSpy.wait());
506 QCOMPARE(textInputPreeditSpy.last().at(0), QString("ABCDE"));
507 QCOMPARE(textInputPreeditSpy.last().at(1), 1);
508 QCOMPARE(textInputPreeditSpy.last().at(2), 1);
509
510 zwp_input_method_context_v1_preedit_cursor(context, 2);
511 // Use selection for [2, 2+2)
512 zwp_input_method_context_v1_preedit_styling(context, 2, 2, 6);
513 // Use high light for [3, 3+3)
514 zwp_input_method_context_v1_preedit_styling(context, 3, 3, 4);
515 zwp_input_method_context_v1_preedit_string(context, 0, "ABCDEF", "ABCDEF");
516 QVERIFY(textInputPreeditSpy.wait());
517 QCOMPARE(textInputPreeditSpy.last().at(0), QString("ABCDEF"));
518 // Merged range should be [2, 6)
519 QCOMPARE(textInputPreeditSpy.last().at(1), 2);
520 QCOMPARE(textInputPreeditSpy.last().at(2), 6);
521
522 zwp_input_method_context_v1_preedit_cursor(context, 2);
523 // Use selection for [0, 0+2)
524 zwp_input_method_context_v1_preedit_styling(context, 0, 2, 6);
525 // Use high light for [3, 3+3)
526 zwp_input_method_context_v1_preedit_styling(context, 3, 3, 4);
527 zwp_input_method_context_v1_preedit_string(context, 0, "ABCDEF", "ABCDEF");
528 QVERIFY(textInputPreeditSpy.wait());
529 QCOMPARE(textInputPreeditSpy.last().at(0), QString("ABCDEF"));
530 // Merged range should be none, because of the disjunction highlight.
531 QCOMPARE(textInputPreeditSpy.last().at(1), 2);
532 QCOMPARE(textInputPreeditSpy.last().at(2), 2);
533
534 zwp_input_method_context_v1_preedit_cursor(context, 1);
535 // Use selection for [0, 0+2)
536 zwp_input_method_context_v1_preedit_styling(context, 0, 2, 6);
537 // Use high light for [2, 2+3)
538 zwp_input_method_context_v1_preedit_styling(context, 2, 3, 4);
539 zwp_input_method_context_v1_preedit_string(context, 0, "ABCDEF", "ABCDEF");
540 QVERIFY(textInputPreeditSpy.wait());
541 QCOMPARE(textInputPreeditSpy.last().at(0), QString("ABCDEF"));
542 // Merged range should be none, starting offset does not match.
543 QCOMPARE(textInputPreeditSpy.last().at(1), 1);
544 QCOMPARE(textInputPreeditSpy.last().at(2), 1);
545
546 // Use different order of styling and cursor
547 // Use high light for [3, 3+3)
548 zwp_input_method_context_v1_preedit_styling(context, 3, 3, 4);
549 zwp_input_method_context_v1_preedit_cursor(context, 1);
550 // Use selection for [1, 1+2)
551 zwp_input_method_context_v1_preedit_styling(context, 1, 2, 6);
552 zwp_input_method_context_v1_preedit_string(context, 0, "ABCDEF", "ABCDEF");
553 QVERIFY(textInputPreeditSpy.wait());
554 QCOMPARE(textInputPreeditSpy.last().at(0), QString("ABCDEF"));
555 // Merged range should be [1,6).
556 QCOMPARE(textInputPreeditSpy.last().at(1), 1);
557 QCOMPARE(textInputPreeditSpy.last().at(2), 6);
558
559 shellSurface.reset();
560 QVERIFY(Test::waitForWindowClosed(window));
561 QVERIFY(!kwinApp()->inputMethod()->isActive());
562}
563
564void InputMethodTest::testDisableShowInputPanel()
565{
566 // Create an xdg_toplevel surface and wait for the compositor to catch up.
567 std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
568 std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
569 Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red);
570 QVERIFY(window);
571 QVERIFY(window->isActive());
572 QCOMPARE(window->frameGeometry().size(), QSize(1280, 1024));
573
574 std::unique_ptr<KWayland::Client::TextInput> textInputV2(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat()));
575
576 QSignalSpy inputMethodActiveSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged);
577 // just enabling the text-input should not show it but rather on commit
578 QVERIFY(!kwinApp()->inputMethod()->isActive());
579 textInputV2->enable(surface.get());
580 QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait());
581 QVERIFY(kwinApp()->inputMethod()->isActive());
582
583 // disable text input and ensure that it is not hiding input panel without commit
584 inputMethodActiveSpy.clear();
585 QVERIFY(kwinApp()->inputMethod()->isActive());
586 textInputV2->disable(surface.get());
587 QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait());
588 QVERIFY(!kwinApp()->inputMethod()->isActive());
589
590 QSignalSpy requestShowInputPanelSpy(waylandServer()->seat()->textInputV2(), &TextInputV2Interface::requestShowInputPanel);
591 textInputV2->showInputPanel();
592 QVERIFY(requestShowInputPanelSpy.count() || requestShowInputPanelSpy.wait());
593 QVERIFY(!kwinApp()->inputMethod()->isActive());
594
595 shellSurface.reset();
596 QVERIFY(Test::waitForWindowClosed(window));
597}
598
599void InputMethodTest::testModifierForwarding()
600{
601 // Create an xdg_toplevel surface and wait for the compositor to catch up.
602 std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
603 std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
604 Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red);
605 QVERIFY(window);
606 QVERIFY(window->isActive());
607 QCOMPARE(window->frameGeometry().size(), QSize(1280, 1024));
608
609 auto textInputV3 = std::make_unique<Test::TextInputV3>();
610 textInputV3->init(Test::waylandTextInputManagerV3()->get_text_input(*(Test::waylandSeat())));
611 textInputV3->enable();
612
613 QSignalSpy inputMethodActiveSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged);
614 QSignalSpy inputMethodActivateSpy(Test::inputMethod(), &Test::MockInputMethod::activate);
615 // just enabling the text-input should not show it but rather on commit
616 QVERIFY(!kwinApp()->inputMethod()->isActive());
617 textInputV3->commit();
618 QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait());
619 QVERIFY(kwinApp()->inputMethod()->isActive());
620 QVERIFY(inputMethodActivateSpy.wait());
621 auto context = Test::inputMethod()->context();
622 std::unique_ptr<KWayland::Client::Keyboard> keyboardGrab(new KWayland::Client::Keyboard);
623 keyboardGrab->setup(zwp_input_method_context_v1_grab_keyboard(context));
624 QSignalSpy modifierSpy(keyboardGrab.get(), &KWayland::Client::Keyboard::modifiersChanged);
625 // Wait for initial modifiers update
626 QVERIFY(modifierSpy.wait());
627
628 quint32 timestamp = 1;
629
630 QSignalSpy keySpy(keyboardGrab.get(), &KWayland::Client::Keyboard::keyChanged);
631 bool keyChanged = false;
632 bool modifiersChanged = false;
633 // We want to verify the order of two signals, so SignalSpy is not very useful here.
634 auto keyChangedConnection = connect(keyboardGrab.get(), &KWayland::Client::Keyboard::keyChanged, [&keyChanged, &modifiersChanged]() {
635 QVERIFY(!modifiersChanged);
636 keyChanged = true;
637 });
638 auto modifiersChangedConnection = connect(keyboardGrab.get(), &KWayland::Client::Keyboard::modifiersChanged, [&keyChanged, &modifiersChanged]() {
639 QVERIFY(keyChanged);
640 modifiersChanged = true;
641 });
642 Test::keyboardKeyPressed(KEY_LEFTCTRL, timestamp++);
643 QVERIFY(keySpy.count() == 1 || keySpy.wait());
644 QVERIFY(modifierSpy.count() == 2 || modifierSpy.wait());
645 disconnect(keyChangedConnection);
646 disconnect(modifiersChangedConnection);
647
648 Test::keyboardKeyPressed(KEY_A, timestamp++);
649 QVERIFY(keySpy.count() == 2 || keySpy.wait());
650 QVERIFY(modifierSpy.count() == 2 || modifierSpy.wait());
651
652 // verify the order of key and modifiers again. Key first, then modifiers.
653 keyChanged = false;
654 modifiersChanged = false;
655 keyChangedConnection = connect(keyboardGrab.get(), &KWayland::Client::Keyboard::keyChanged, [&keyChanged, &modifiersChanged]() {
656 QVERIFY(!modifiersChanged);
657 keyChanged = true;
658 });
659 modifiersChangedConnection = connect(keyboardGrab.get(), &KWayland::Client::Keyboard::modifiersChanged, [&keyChanged, &modifiersChanged]() {
660 QVERIFY(keyChanged);
661 modifiersChanged = true;
662 });
663 Test::keyboardKeyReleased(KEY_LEFTCTRL, timestamp++);
664 QVERIFY(keySpy.count() == 3 || keySpy.wait());
665 QVERIFY(modifierSpy.count() == 3 || modifierSpy.wait());
666 disconnect(keyChangedConnection);
667 disconnect(modifiersChangedConnection);
668
669 shellSurface.reset();
670 QVERIFY(Test::waitForWindowClosed(window));
671 QVERIFY(!kwinApp()->inputMethod()->isActive());
672}
673
674void InputMethodTest::testFakeEventFallback()
675{
676 // Create an xdg_toplevel surface and wait for the compositor to catch up.
677 std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
678 std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
679 Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1280, 1024), Qt::red);
680 QVERIFY(window);
681 QVERIFY(window->isActive());
682 QCOMPARE(window->frameGeometry().size(), QSize(1280, 1024));
683
684 // Since we don't have a way to communicate with the client, manually activate
685 // the input method.
686 QSignalSpy inputMethodActiveSpy(Test::inputMethod(), &Test::MockInputMethod::activate);
687 kwinApp()->inputMethod()->setActive(true);
688 QVERIFY(inputMethodActiveSpy.count() || inputMethodActiveSpy.wait());
689
690 // Without a way to communicate to the client, we send fake key events. This
691 // means the client needs to be able to receive them, so create a keyboard for
692 // the client and listen whether it gets the right events.
693 auto keyboard = Test::waylandSeat()->createKeyboard(window);
694 QSignalSpy keySpy(keyboard, &KWayland::Client::Keyboard::keyChanged);
695
696 auto context = Test::inputMethod()->context();
697 QVERIFY(context);
698
699 // First, send a simple one-character string and check to see if that
700 // generates a key press followed by a key release on the client side.
701 zwp_input_method_context_v1_commit_string(context, 0, "a");
702
703 keySpy.wait();
704 QVERIFY(keySpy.count() == 2);
705
706 auto compare = [](const QList<QVariant> &input, quint32 key, KWayland::Client::Keyboard::KeyState state) {
707 auto inputKey = input.at(0).toInt();
708 auto inputState = input.at(1).value<KWayland::Client::Keyboard::KeyState>();
709 QCOMPARE(inputKey, key);
710 QCOMPARE(inputState, state);
711 };
712
713 compare(keySpy.at(0), KEY_A, KWayland::Client::Keyboard::KeyState::Pressed);
714 compare(keySpy.at(1), KEY_A, KWayland::Client::Keyboard::KeyState::Released);
715
716 keySpy.clear();
717
718 // Capital letters are recognised and sent as a combination of Shift + the
719 // letter.
720
721 zwp_input_method_context_v1_commit_string(context, 0, "A");
722
723 keySpy.wait();
724 QVERIFY(keySpy.count() == 4);
725
726 compare(keySpy.at(0), KEY_LEFTSHIFT, KWayland::Client::Keyboard::KeyState::Pressed);
727 compare(keySpy.at(1), KEY_A, KWayland::Client::Keyboard::KeyState::Pressed);
728 compare(keySpy.at(2), KEY_A, KWayland::Client::Keyboard::KeyState::Released);
729 compare(keySpy.at(3), KEY_LEFTSHIFT, KWayland::Client::Keyboard::KeyState::Released);
730
731 keySpy.clear();
732
733 // Special keys are not sent through commit_string but instead use keysym.
734 auto enter = input()->keyboard()->xkb()->toKeysym(KEY_ENTER);
735 zwp_input_method_context_v1_keysym(context, 0, 0, enter, uint32_t(KeyboardKeyState::Pressed), 0);
736 zwp_input_method_context_v1_keysym(context, 0, 1, enter, uint32_t(KeyboardKeyState::Released), 0);
737
738 keySpy.wait();
739 QVERIFY(keySpy.count() == 2);
740
741 compare(keySpy.at(0), KEY_ENTER, KWayland::Client::Keyboard::KeyState::Pressed);
742 compare(keySpy.at(1), KEY_ENTER, KWayland::Client::Keyboard::KeyState::Released);
743
744 shellSurface.reset();
745 QVERIFY(Test::waitForWindowClosed(window));
746 kwinApp()->inputMethod()->setActive(false);
747 QVERIFY(!kwinApp()->inputMethod()->isActive());
748}
749
750void InputMethodTest::testOverlayPositioning_data()
751{
752 QTest::addColumn<QRect>("cursorRectangle");
753 QTest::addColumn<QRect>("result");
754
755 QTest::newRow("regular") << QRect(10, 20, 30, 40) << QRect(60, 160, 200, 50);
756 QTest::newRow("offscreen-left") << QRect(-200, 40, 30, 40) << QRect(0, 180, 200, 50);
757 QTest::newRow("offscreen-right") << QRect(1200, 40, 30, 40) << QRect(1080, 180, 200, 50);
758 QTest::newRow("offscreen-top") << QRect(1200, -400, 30, 40) << QRect(1080, 0, 200, 50);
759 // Check it is flipped near the bottom of screen (anchor point 844 + 100 + 40 = 1024 - 40)
760 QTest::newRow("offscreen-bottom-flip") << QRect(1200, 844, 30, 40) << QRect(1080, 894, 200, 50);
761 // Top is (screen height 1024 - window height 50) = 984
762 QTest::newRow("offscreen-bottom-slide") << QRect(1200, 1200, 30, 40) << QRect(1080, 974, 200, 50);
763}
764
765void InputMethodTest::testOverlayPositioning()
766{
767 QFETCH(QRect, cursorRectangle);
768 QFETCH(QRect, result);
769 Test::inputMethod()->setMode(Test::MockInputMethod::Mode::Overlay);
770 QVERIFY(!kwinApp()->inputMethod()->isActive());
771
772 touchNow();
773 QSignalSpy windowAddedSpy(workspace(), &Workspace::windowAdded);
774 QSignalSpy windowRemovedSpy(workspace(), &Workspace::windowRemoved);
775
776 QSignalSpy activateSpy(kwinApp()->inputMethod(), &InputMethod::activeChanged);
777 std::unique_ptr<KWayland::Client::TextInput> textInput(Test::waylandTextInputManager()->createTextInput(Test::waylandSeat()));
778
779 // Create an xdg_toplevel surface and wait for the compositor to catch up.
780 std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
781 std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
782 // Make the window smaller than the screen and move it.
783 Window *window = Test::renderAndWaitForShown(surface.get(), QSize(1080, 824), Qt::red);
784 window->move(QPointF(50, 100));
785 waylandServer()->seat()->setFocusedTextInputSurface(window->surface());
786
787 textInput->setCursorRectangle(cursorRectangle);
788 textInput->enable(surface.get());
789 // Overlay is shown upon activate
790 QVERIFY(windowAddedSpy.wait());
791
792 QCOMPARE(workspace()->activeWindow(), window);
793
794 QCOMPARE(windowAddedSpy.count(), 2);
795 QVERIFY(activateSpy.count() || activateSpy.wait());
796 QVERIFY(kwinApp()->inputMethod()->isActive());
797
798 auto keyboardWindow = kwinApp()->inputMethod()->panel();
799 QVERIFY(keyboardWindow);
800 // Check the overlay window is placed with cursor rectangle + window position.
801 QCOMPARE(keyboardWindow->frameGeometry(), result);
802
803 // Destroy the test window.
804 shellSurface.reset();
805 QVERIFY(Test::waitForWindowClosed(window));
806
807 Test::inputMethod()->setMode(Test::MockInputMethod::Mode::TopLevel);
808}
809
811
812#include "inputmethod_test.moc"
void activeChanged(bool active)
KeyboardInputRedirection * keyboard() const
Definition input.h:216
PointerInputRedirection * pointer() const
Definition input.h:220
void warp(const QPointF &pos)
void setFocusedTextInputSurface(SurfaceInterface *surface)
Definition seat.cpp:1231
void preeditString(const QString &text, int cursor_begin, int cursor_end)
void configureRequested(quint32 serial)
void configureRequested(const QSize &size, KWin::Test::XdgToplevel::States states)
SeatInterface * seat() const
void windowRemoved(KWin::Window *)
void windowAdded(KWin::Window *)
QList< Output * > outputs() const
Definition workspace.h:762
void setActiveOutput(Output *output)
xkb_keysym_t toKeysym(uint32_t key)
Definition xkb.cpp:555
#define WAYLANDTEST_MAIN(TestObject)
Window * renderAndWaitForShown(KWayland::Client::Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format=QImage::Format_ARGB32, int timeout=5000)
void keyboardKeyReleased(quint32 key, quint32 time)
void destroyWaylandConnection()
void setOutputConfig(const QList< QRect > &geometries)
KWayland::Client::Seat * seat
void touchDown(qint32 id, const QPointF &pos, quint32 time)
void keyboardKeyPressed(quint32 key, quint32 time)
TextInputManagerV3 * waylandTextInputManagerV3()
bool setupWaylandConnection(AdditionalWaylandInterfaces flags=AdditionalWaylandInterfaces())
void render(KWayland::Client::Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format=QImage::Format_ARGB32_Premultiplied)
KWayland::Client::Seat * waylandSeat()
MockInputMethod * inputMethod()
KWayland::Client::Surface * inputPanelSurface()
QList< KWayland::Client::Output * > outputs
std::unique_ptr< KWayland::Client::Surface > createSurface()
XdgToplevel * createXdgToplevelSurface(KWayland::Client::Surface *surface, QObject *parent=nullptr)
KWayland::Client::TextInputManager * waylandTextInputManager()
void touchUp(qint32 id, quint32 time)
bool waitForWindowClosed(Window *window)
WaylandServer * waylandServer()
Workspace * workspace()
Definition workspace.h:830
InputRedirection * input()
Definition input.h:549