KWin
All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Friends Macros Modules Pages
touch_input_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: 2016 Martin Gräßlin <mgraesslin@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 "pointer_input.h"
13#include "touch_input.h"
14#include "wayland_server.h"
15#include "window.h"
16#include "workspace.h"
17
18#include <KWayland/Client/compositor.h>
19#include <KWayland/Client/connection_thread.h>
20#include <KWayland/Client/seat.h>
21#include <KWayland/Client/surface.h>
22#include <KWayland/Client/touch.h>
23
24#include <QAction>
25#include <QSignalSpy>
26
27namespace KWin
28{
29
30static const QString s_socketName = QStringLiteral("wayland_test_kwin_touch_input-0");
31
32class TouchInputTest : public QObject
33{
34 Q_OBJECT
35private Q_SLOTS:
36 void initTestCase();
37 void init();
38 void cleanup();
39 void testTouchHidesCursor();
40 void testMultipleTouchPoints_data();
41 void testMultipleTouchPoints();
42 void testCancel();
43 void testTouchMouseAction();
44 void testTouchPointCount();
45 void testUpdateFocusOnDecorationDestroy();
46 void testGestureDetection();
47
48private:
49 std::pair<Window *, std::unique_ptr<KWayland::Client::Surface>> showWindow(bool decorated = false);
50 KWayland::Client::Touch *m_touch = nullptr;
51};
52
53void TouchInputTest::initTestCase()
54{
55 qRegisterMetaType<KWin::Window *>();
56 QSignalSpy applicationStartedSpy(kwinApp(), &Application::started);
57 QVERIFY(waylandServer()->init(s_socketName));
59 QRect(0, 0, 1280, 1024),
60 QRect(1280, 0, 1280, 1024),
61 });
62
63 kwinApp()->start();
64 QVERIFY(applicationStartedSpy.wait());
65 const auto outputs = workspace()->outputs();
66 QCOMPARE(outputs.count(), 2);
67 QCOMPARE(outputs[0]->geometry(), QRect(0, 0, 1280, 1024));
68 QCOMPARE(outputs[1]->geometry(), QRect(1280, 0, 1280, 1024));
69}
70
71void TouchInputTest::init()
72{
75 m_touch = Test::waylandSeat()->createTouch(Test::waylandSeat());
76 QVERIFY(m_touch);
77 QVERIFY(m_touch->isValid());
78
79 workspace()->setActiveOutput(QPoint(640, 512));
80 input()->pointer()->warp(QPoint(640, 512));
81}
82
83void TouchInputTest::cleanup()
84{
85 delete m_touch;
86 m_touch = nullptr;
88}
89
90std::pair<Window *, std::unique_ptr<KWayland::Client::Surface>> TouchInputTest::showWindow(bool decorated)
91{
92#define VERIFY(statement) \
93 if (!QTest::qVerify((statement), #statement, "", __FILE__, __LINE__)) \
94 return {nullptr, nullptr};
95#define COMPARE(actual, expected) \
96 if (!QTest::qCompare(actual, expected, #actual, #expected, __FILE__, __LINE__)) \
97 return {nullptr, nullptr};
98
99 std::unique_ptr<KWayland::Client::Surface> surface = Test::createSurface();
100 VERIFY(surface.get());
101 Test::XdgToplevel *shellSurface = Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly, surface.get());
102 VERIFY(shellSurface);
103 if (decorated) {
104 auto decoration = Test::createXdgToplevelDecorationV1(shellSurface, shellSurface);
105 decoration->set_mode(Test::XdgToplevelDecorationV1::mode_server_side);
106 }
107 QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested);
108 surface->commit(KWayland::Client::Surface::CommitFlag::None);
109 VERIFY(surfaceConfigureRequestedSpy.wait());
110 // let's render
111 shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value<quint32>());
112 auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
113
114 VERIFY(window);
115 COMPARE(workspace()->activeWindow(), window);
116
117#undef VERIFY
118#undef COMPARE
119
120 return {window, std::move(surface)};
121}
122
123void TouchInputTest::testTouchHidesCursor()
124{
125 QCOMPARE(Cursors::self()->isCursorHidden(), false);
126 quint32 timestamp = 1;
127 Test::touchDown(1, QPointF(125, 125), timestamp++);
128 QCOMPARE(Cursors::self()->isCursorHidden(), true);
129 Test::touchDown(2, QPointF(130, 125), timestamp++);
130 Test::touchUp(2, timestamp++);
131 Test::touchUp(1, timestamp++);
132
133 // now a mouse event should show the cursor again
134 Test::pointerMotion(QPointF(0, 0), timestamp++);
135 QCOMPARE(Cursors::self()->isCursorHidden(), false);
136
137 // touch should hide again
138 Test::touchDown(1, QPointF(125, 125), timestamp++);
139 Test::touchUp(1, timestamp++);
140 QCOMPARE(Cursors::self()->isCursorHidden(), true);
141
142 // wheel should also show
143 Test::pointerAxisVertical(1.0, timestamp++);
144 QCOMPARE(Cursors::self()->isCursorHidden(), false);
145}
146
147void TouchInputTest::testMultipleTouchPoints_data()
148{
149 QTest::addColumn<bool>("decorated");
150
151 QTest::newRow("undecorated") << false;
152 QTest::newRow("decorated") << true;
153}
154
155void TouchInputTest::testMultipleTouchPoints()
156{
157 QFETCH(bool, decorated);
158 auto [window, surface] = showWindow(decorated);
159 QCOMPARE(window->isDecorated(), decorated);
160 window->move(QPoint(100, 100));
161 QVERIFY(window);
162 QSignalSpy sequenceStartedSpy(m_touch, &KWayland::Client::Touch::sequenceStarted);
163 QSignalSpy pointAddedSpy(m_touch, &KWayland::Client::Touch::pointAdded);
164 QSignalSpy pointMovedSpy(m_touch, &KWayland::Client::Touch::pointMoved);
165 QSignalSpy pointRemovedSpy(m_touch, &KWayland::Client::Touch::pointRemoved);
166 QSignalSpy endedSpy(m_touch, &KWayland::Client::Touch::sequenceEnded);
167
168 quint32 timestamp = 1;
169 Test::touchDown(1, window->mapFromLocal(QPointF(25, 25)), timestamp++);
170 QVERIFY(sequenceStartedSpy.wait());
171 QCOMPARE(sequenceStartedSpy.count(), 1);
172 QCOMPARE(m_touch->sequence().count(), 1);
173 QCOMPARE(m_touch->sequence().first()->isDown(), true);
174 QCOMPARE(m_touch->sequence().first()->position(), QPointF(25, 25));
175 QCOMPARE(pointAddedSpy.count(), 0);
176 QCOMPARE(pointMovedSpy.count(), 0);
177
178 // a point outside the window
179 Test::touchDown(2, window->mapFromLocal(QPointF(-100, -100)), timestamp++);
180 QVERIFY(pointAddedSpy.wait());
181 QCOMPARE(pointAddedSpy.count(), 1);
182 QCOMPARE(m_touch->sequence().count(), 2);
183 QCOMPARE(m_touch->sequence().at(1)->isDown(), true);
184 QCOMPARE(m_touch->sequence().at(1)->position(), QPointF(-100, -100));
185 QCOMPARE(pointMovedSpy.count(), 0);
186
187 // let's move that one
188 Test::touchMotion(2, window->mapFromLocal(QPointF(0, 0)), timestamp++);
189 QVERIFY(pointMovedSpy.wait());
190 QCOMPARE(pointMovedSpy.count(), 1);
191 QCOMPARE(m_touch->sequence().count(), 2);
192 QCOMPARE(m_touch->sequence().at(1)->isDown(), true);
193 QCOMPARE(m_touch->sequence().at(1)->position(), QPointF(0, 0));
194
195 Test::touchUp(1, timestamp++);
196 QVERIFY(pointRemovedSpy.wait());
197 QCOMPARE(pointRemovedSpy.count(), 1);
198 QCOMPARE(m_touch->sequence().count(), 2);
199 QCOMPARE(m_touch->sequence().first()->isDown(), false);
200 QCOMPARE(endedSpy.count(), 0);
201
202 Test::touchUp(2, timestamp++);
203 QVERIFY(pointRemovedSpy.wait());
204 QCOMPARE(pointRemovedSpy.count(), 2);
205 QCOMPARE(m_touch->sequence().count(), 2);
206 QCOMPARE(m_touch->sequence().first()->isDown(), false);
207 QCOMPARE(m_touch->sequence().at(1)->isDown(), false);
208 QCOMPARE(endedSpy.count(), 1);
209}
210
211void TouchInputTest::testCancel()
212{
213 auto [window, surface] = showWindow();
214 window->move(QPoint(100, 100));
215 QVERIFY(window);
216 QSignalSpy sequenceStartedSpy(m_touch, &KWayland::Client::Touch::sequenceStarted);
217 QSignalSpy cancelSpy(m_touch, &KWayland::Client::Touch::sequenceCanceled);
218 QSignalSpy pointRemovedSpy(m_touch, &KWayland::Client::Touch::pointRemoved);
219
220 quint32 timestamp = 1;
221 Test::touchDown(1, QPointF(125, 125), timestamp++);
222 QVERIFY(sequenceStartedSpy.wait());
223 QCOMPARE(sequenceStartedSpy.count(), 1);
224
225 // cancel
227 QVERIFY(cancelSpy.wait());
228 QCOMPARE(cancelSpy.count(), 1);
229}
230
231void TouchInputTest::testTouchMouseAction()
232{
233 // this test verifies that a touch down on an inactive window will activate it
234
235 // create two windows
236 auto [c1, surface] = showWindow();
237 QVERIFY(c1);
238 auto [c2, surface2] = showWindow();
239 QVERIFY(c2);
240
241 QVERIFY(!c1->isActive());
242 QVERIFY(c2->isActive());
243
244 // also create a sequence started spy as the touch event should be passed through
245 QSignalSpy sequenceStartedSpy(m_touch, &KWayland::Client::Touch::sequenceStarted);
246
247 quint32 timestamp = 1;
248 Test::touchDown(1, c1->frameGeometry().center(), timestamp++);
249 QVERIFY(c1->isActive());
250
251 QVERIFY(sequenceStartedSpy.wait());
252 QCOMPARE(sequenceStartedSpy.count(), 1);
253
254 // cleanup
255 input()->touch()->cancel();
256}
257
258void TouchInputTest::testTouchPointCount()
259{
260 QCOMPARE(input()->touch()->touchPointCount(), 0);
261 quint32 timestamp = 1;
262 Test::touchDown(0, QPointF(125, 125), timestamp++);
263 Test::touchDown(1, QPointF(125, 125), timestamp++);
264 Test::touchDown(2, QPointF(125, 125), timestamp++);
265 QCOMPARE(input()->touch()->touchPointCount(), 3);
266
267 Test::touchUp(1, timestamp++);
268 QCOMPARE(input()->touch()->touchPointCount(), 2);
269
270 input()->touch()->cancel();
271 QCOMPARE(input()->touch()->touchPointCount(), 0);
272}
273
274void TouchInputTest::testUpdateFocusOnDecorationDestroy()
275{
276 // This test verifies that a maximized window gets it's touch focus
277 // if decoration was focused and then destroyed on maximize with BorderlessMaximizedWindows option.
278
279 QSignalSpy sequenceEndedSpy(m_touch, &KWayland::Client::Touch::sequenceEnded);
280
281 // Enable the borderless maximized windows option.
282 auto group = kwinApp()->config()->group(QStringLiteral("Windows"));
283 group.writeEntry("BorderlessMaximizedWindows", true);
284 group.sync();
286 QCOMPARE(options->borderlessMaximizedWindows(), true);
287
288 // Create the test window.
289 std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
290 std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get(), Test::CreationSetup::CreateOnly));
291 std::unique_ptr<Test::XdgToplevelDecorationV1> decoration(Test::createXdgToplevelDecorationV1(shellSurface.get()));
292
293 QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested);
294 QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested);
295 QSignalSpy decorationConfigureRequestedSpy(decoration.get(), &Test::XdgToplevelDecorationV1::configureRequested);
296 decoration->set_mode(Test::XdgToplevelDecorationV1::mode_server_side);
297 surface->commit(KWayland::Client::Surface::CommitFlag::None);
298
299 // Wait for the initial configure event.
300 Test::XdgToplevel::States states;
301 QVERIFY(surfaceConfigureRequestedSpy.wait());
302 QCOMPARE(surfaceConfigureRequestedSpy.count(), 1);
303 QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(0, 0));
304 states = toplevelConfigureRequestedSpy.last().at(1).value<Test::XdgToplevel::States>();
305 QVERIFY(!states.testFlag(Test::XdgToplevel::State::Activated));
306 QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized));
307
308 // Map the window.
309 shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value<quint32>());
310 Window *window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
311 QVERIFY(window);
312 QVERIFY(window->isActive());
313 QCOMPARE(window->maximizeMode(), MaximizeMode::MaximizeRestore);
314 QCOMPARE(window->requestedMaximizeMode(), MaximizeMode::MaximizeRestore);
315 QCOMPARE(window->isDecorated(), true);
316
317 // We should receive a configure event when the window becomes active.
318 QVERIFY(surfaceConfigureRequestedSpy.wait());
319 QCOMPARE(surfaceConfigureRequestedSpy.count(), 2);
320 states = toplevelConfigureRequestedSpy.last().at(1).value<Test::XdgToplevel::States>();
321 QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
322 QVERIFY(!states.testFlag(Test::XdgToplevel::State::Maximized));
323
324 // Simulate decoration hover
325 quint32 timestamp = 0;
326 Test::touchDown(1, window->frameGeometry().topLeft(), timestamp++);
327 QVERIFY(input()->touch()->decoration());
328
329 // Maximize when on decoration
331 QVERIFY(surfaceConfigureRequestedSpy.wait());
332 QCOMPARE(surfaceConfigureRequestedSpy.count(), 3);
333 QCOMPARE(toplevelConfigureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024));
334 states = toplevelConfigureRequestedSpy.last().at(1).value<Test::XdgToplevel::States>();
335 QVERIFY(states.testFlag(Test::XdgToplevel::State::Activated));
336 QVERIFY(states.testFlag(Test::XdgToplevel::State::Maximized));
337
338 QSignalSpy frameGeometryChangedSpy(window, &Window::frameGeometryChanged);
339 shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value<quint32>());
340 Test::render(surface.get(), QSize(1280, 1024), Qt::blue);
341 QVERIFY(frameGeometryChangedSpy.wait());
342 QCOMPARE(window->frameGeometry(), QRect(0, 0, 1280, 1024));
343 QCOMPARE(window->maximizeMode(), MaximizeFull);
344 QCOMPARE(window->requestedMaximizeMode(), MaximizeFull);
345 QCOMPARE(window->isDecorated(), false);
346
347 // Window should have focus
348 QVERIFY(!input()->touch()->decoration());
349 Test::touchUp(1, timestamp++);
350 QVERIFY(!sequenceEndedSpy.wait(100));
351 Test::touchDown(2, window->frameGeometry().center(), timestamp++);
352 Test::touchUp(2, timestamp++);
353 QVERIFY(sequenceEndedSpy.wait());
354
355 // Destroy the window.
356 shellSurface.reset();
357 QVERIFY(Test::waitForWindowClosed(window));
358}
359
360void TouchInputTest::testGestureDetection()
361{
362#if !KWIN_BUILD_GLOBALSHORTCUTS
363 QSKIP("Can't test shortcuts without shortcuts");
364 return;
365#endif
366
367 bool callbackTriggered = false;
368 const auto callback = [&callbackTriggered](float progress) {
369 callbackTriggered = true;
370 qWarning() << "progress callback!" << progress;
371 };
372 QAction action;
374
375 // verify that gestures are detected
376
377 quint32 timestamp = 1;
378 Test::touchDown(0, QPointF(500, 125), timestamp++);
379 Test::touchDown(1, QPointF(500, 125), timestamp++);
380 Test::touchDown(2, QPointF(500, 125), timestamp++);
381
382 Test::touchMotion(0, QPointF(100, 125), timestamp++);
383 QVERIFY(callbackTriggered);
384
385 // verify that gestures are canceled properly
386 QSignalSpy gestureCancelled(&action, &QAction::triggered);
387 Test::touchUp(0, timestamp++);
388 QVERIFY(gestureCancelled.wait());
389
390 Test::touchUp(1, timestamp++);
391 Test::touchUp(2, timestamp++);
392
393 callbackTriggered = false;
394
395 // verify that touch points too far apart don't trigger a gesture
396 Test::touchDown(0, QPointF(125, 125), timestamp++);
397 Test::touchDown(1, QPointF(10000, 125), timestamp++);
398 Test::touchDown(2, QPointF(125, 125), timestamp++);
399 QVERIFY(!callbackTriggered);
400
401 Test::touchUp(0, timestamp++);
402 Test::touchUp(1, timestamp++);
403 Test::touchUp(2, timestamp++);
404
405 // verify that touch points triggered too slow don't trigger a gesture
406 Test::touchDown(0, QPointF(125, 125), timestamp++);
407 timestamp += 1000;
408 Test::touchDown(1, QPointF(125, 125), timestamp++);
409 Test::touchDown(2, QPointF(125, 125), timestamp++);
410 QVERIFY(!callbackTriggered);
411
412 Test::touchUp(0, timestamp++);
413 Test::touchUp(1, timestamp++);
414 Test::touchUp(2, timestamp++);
415
416 // verify that after a gesture has been canceled but never initiated, gestures still work
417 Test::touchDown(0, QPointF(500, 125), timestamp++);
418 Test::touchDown(1, QPointF(500, 125), timestamp++);
419 Test::touchDown(2, QPointF(500, 125), timestamp++);
420
421 Test::touchMotion(0, QPointF(100, 125), timestamp++);
422 Test::touchMotion(1, QPointF(100, 125), timestamp++);
423 Test::touchMotion(2, QPointF(100, 125), timestamp++);
424 QVERIFY(callbackTriggered);
425
426 Test::touchUp(0, timestamp++);
427 Test::touchUp(1, timestamp++);
428 Test::touchUp(2, timestamp++);
429}
430}
431
433#include "touch_input_test.moc"
static Cursors * self()
Definition cursor.cpp:35
TouchInputRedirection * touch() const
Definition input.h:228
PointerInputRedirection * pointer() const
Definition input.h:220
void forceRegisterTouchscreenSwipeShortcut(SwipeDirection direction, uint32_t fingerCount, QAction *action, std::function< void(qreal)> progressCallback={})
Definition input.cpp:3356
bool borderlessMaximizedWindows
Definition options.h:172
void warp(const QPointF &pos)
void configureRequested(quint32 serial)
void configureRequested(QtWayland::zxdg_toplevel_decoration_v1::mode mode)
void configureRequested(const QSize &size, KWin::Test::XdgToplevel::States states)
void frameGeometryChanged(const QRectF &oldGeometry)
static Workspace * self()
Definition workspace.h:91
QList< Output * > outputs() const
Definition workspace.h:762
void setActiveOutput(Output *output)
void slotReconfigure()
#define VERIFY(statement)
#define COMPARE(actual, expected)
#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 destroyWaylandConnection()
void setOutputConfig(const QList< QRect > &geometries)
void touchDown(qint32 id, const QPointF &pos, quint32 time)
void pointerAxisVertical(qreal delta, quint32 time, qint32 discreteDelta=0, InputRedirection::PointerAxisSource source=InputRedirection::PointerAxisSourceUnknown)
bool setupWaylandConnection(AdditionalWaylandInterfaces flags=AdditionalWaylandInterfaces())
void touchMotion(qint32 id, const QPointF &pos, quint32 time)
void render(KWayland::Client::Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format=QImage::Format_ARGB32_Premultiplied)
KWayland::Client::Seat * waylandSeat()
bool waitForWaylandTouch()
void pointerMotion(const QPointF &position, quint32 time)
std::unique_ptr< KWayland::Client::Surface > createSurface()
XdgToplevel * createXdgToplevelSurface(KWayland::Client::Surface *surface, QObject *parent=nullptr)
XdgToplevelDecorationV1 * createXdgToplevelDecorationV1(XdgToplevel *toplevel, QObject *parent=nullptr)
void touchUp(qint32 id, quint32 time)
bool waitForWindowClosed(Window *window)
@ MaximizeRestore
The window is not maximized in any direction.
Definition common.h:75
@ MaximizeFull
Equal to MaximizeVertical | MaximizeHorizontal.
Definition common.h:79
WaylandServer * waylandServer()
Workspace * workspace()
Definition workspace.h:830
Options * options
Definition main.cpp:73
InputRedirection * input()
Definition input.h:549